Fix reflow logic
This commit is contained in:
@@ -75,6 +75,23 @@ uint16_t measureWordWidth(const GfxRenderer& renderer, const int fontId, const s
|
||||
return renderer.getTextAdvanceX(fontId, sanitized.c_str(), style);
|
||||
}
|
||||
|
||||
std::string buildLinePreview(const std::vector<std::string>& words, const std::vector<bool>& continuesVec,
|
||||
const size_t start, const size_t endExclusive, const size_t maxLen = 120) {
|
||||
std::string preview;
|
||||
for (size_t idx = start; idx < endExclusive; ++idx) {
|
||||
if (idx > start && idx < continuesVec.size() && !continuesVec[idx]) {
|
||||
preview.push_back(' ');
|
||||
}
|
||||
preview += words[idx];
|
||||
if (preview.size() >= maxLen) {
|
||||
preview.resize(maxLen);
|
||||
preview += "...";
|
||||
break;
|
||||
}
|
||||
}
|
||||
return preview;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void ParsedText::addWord(std::string word, const EpdFontFamily::Style fontStyle, const bool underline,
|
||||
@@ -128,7 +145,11 @@ void ParsedText::layoutAndExtractLines(
|
||||
fontId, lineEndedWithHyphenation, false);
|
||||
|
||||
if (result == LineProcessResult::RetryWithoutHyphenation && lineEndedWithHyphenation) {
|
||||
LOG_DBG("PTX", "Line %u requested rerender without hyphenation", static_cast<unsigned>(i));
|
||||
const size_t lineStart = i > 0 ? lineBreakIndices[i - 1] : 0;
|
||||
const size_t lineEnd = i < lineBreakIndices.size() ? lineBreakIndices[i] : lineStart;
|
||||
const std::string firstAttemptPreview = buildLinePreview(words, wordContinues, lineStart, lineEnd);
|
||||
LOG_DBG("PTX", "Line %u requested rerender without hyphenation, first attempt: %s", static_cast<unsigned>(i),
|
||||
firstAttemptPreview.c_str());
|
||||
// Undo the split used to end this line so it can be relaid without hyphenation.
|
||||
const int splitPrefixIndex = i < splitPrefixWordIndexes.size() ? splitPrefixWordIndexes[i] : -1;
|
||||
if (splitPrefixIndex >= 0 && static_cast<size_t>(splitPrefixIndex + 1) < words.size()) {
|
||||
@@ -152,9 +173,42 @@ void ParsedText::layoutAndExtractLines(
|
||||
lineCount = includeLastLine ? lineBreakIndices.size() : lineBreakIndices.size() - 1;
|
||||
|
||||
if (i < lineCount) {
|
||||
LOG_DBG("PTX", "Rerendering line %u with hyphenation suppressed", static_cast<unsigned>(i));
|
||||
const size_t retryLineStart = i > 0 ? lineBreakIndices[i - 1] : 0;
|
||||
const size_t retryLineEnd = i < lineBreakIndices.size() ? lineBreakIndices[i] : retryLineStart;
|
||||
const std::string retryPreview = buildLinePreview(words, wordContinues, retryLineStart, retryLineEnd);
|
||||
LOG_DBG("PTX", "Rerendering line %u with hyphenation suppressed, retry attempt: %s", static_cast<unsigned>(i),
|
||||
retryPreview.c_str());
|
||||
extractLine(i, pageWidth, wordWidths, wordContinues, lineBreakIndices, processLine, renderer, fontId, false,
|
||||
true);
|
||||
|
||||
// Continue with regular hyphenation for subsequent lines only.
|
||||
const size_t resumeIndex = lineBreakIndices[i];
|
||||
std::vector<bool> suffixLineEndsWithHyphenatedWord;
|
||||
std::vector<int> suffixSplitPrefixWordIndexes;
|
||||
std::vector<bool> suffixSplitInsertedHyphen;
|
||||
const auto hyphenatedSuffixBreaks = computeHyphenatedLineBreaksFromIndex(
|
||||
renderer, fontId, pageWidth, wordWidths, wordContinues, resumeIndex, suffixLineEndsWithHyphenatedWord,
|
||||
suffixSplitPrefixWordIndexes, suffixSplitInsertedHyphen);
|
||||
|
||||
lineBreakIndices.resize(i + 1);
|
||||
lineEndsWithHyphenatedWord.resize(i + 1);
|
||||
splitPrefixWordIndexes.resize(i + 1);
|
||||
splitInsertedHyphen.resize(i + 1);
|
||||
|
||||
lineEndsWithHyphenatedWord[i] = false;
|
||||
splitPrefixWordIndexes[i] = -1;
|
||||
splitInsertedHyphen[i] = false;
|
||||
|
||||
lineBreakIndices.insert(lineBreakIndices.end(), hyphenatedSuffixBreaks.begin(), hyphenatedSuffixBreaks.end());
|
||||
lineEndsWithHyphenatedWord.insert(lineEndsWithHyphenatedWord.end(), suffixLineEndsWithHyphenatedWord.begin(),
|
||||
suffixLineEndsWithHyphenatedWord.end());
|
||||
splitPrefixWordIndexes.insert(splitPrefixWordIndexes.end(), suffixSplitPrefixWordIndexes.begin(),
|
||||
suffixSplitPrefixWordIndexes.end());
|
||||
splitInsertedHyphen.insert(splitInsertedHyphen.end(), suffixSplitInsertedHyphen.begin(),
|
||||
suffixSplitInsertedHyphen.end());
|
||||
|
||||
lineCount = includeLastLine ? lineBreakIndices.size() : lineBreakIndices.size() - 1;
|
||||
LOG_DBG("PTX", "Resumed regular hyphenation after rerendered line %u", static_cast<unsigned>(i));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -431,6 +485,94 @@ std::vector<size_t> ParsedText::computeHyphenatedLineBreaks(const GfxRenderer& r
|
||||
return lineBreakIndices;
|
||||
}
|
||||
|
||||
std::vector<size_t> ParsedText::computeHyphenatedLineBreaksFromIndex(
|
||||
const GfxRenderer& renderer, const int fontId, const int pageWidth, std::vector<uint16_t>& wordWidths,
|
||||
std::vector<bool>& continuesVec, const size_t startIndex, std::vector<bool>& lineEndsWithHyphenatedWord,
|
||||
std::vector<int>& splitPrefixWordIndexes, std::vector<bool>& splitInsertedHyphen) {
|
||||
if (startIndex >= wordWidths.size()) {
|
||||
lineEndsWithHyphenatedWord.clear();
|
||||
splitPrefixWordIndexes.clear();
|
||||
splitInsertedHyphen.clear();
|
||||
return {};
|
||||
}
|
||||
|
||||
std::vector<int> interWordGaps(wordWidths.size(), 0);
|
||||
for (size_t j = 1; j < wordWidths.size(); ++j) {
|
||||
if (!continuesVec[j]) {
|
||||
interWordGaps[j] =
|
||||
renderer.getSpaceAdvance(fontId, lastCodepoint(words[j - 1]), firstCodepoint(words[j]), wordStyles[j - 1]);
|
||||
} else {
|
||||
interWordGaps[j] =
|
||||
renderer.getKerning(fontId, lastCodepoint(words[j - 1]), firstCodepoint(words[j]), wordStyles[j - 1]);
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<size_t> lineBreakIndices;
|
||||
lineEndsWithHyphenatedWord.clear();
|
||||
splitPrefixWordIndexes.clear();
|
||||
splitInsertedHyphen.clear();
|
||||
|
||||
size_t currentIndex = startIndex;
|
||||
while (currentIndex < wordWidths.size()) {
|
||||
const size_t lineStart = currentIndex;
|
||||
int lineWidth = 0;
|
||||
bool lineEndedWithHyphenation = false;
|
||||
int splitPrefixIndex = -1;
|
||||
bool splitNeedsInsertedHyphen = false;
|
||||
|
||||
while (currentIndex < wordWidths.size()) {
|
||||
const bool isFirstWord = currentIndex == lineStart;
|
||||
const int spacing = isFirstWord ? 0 : interWordGaps[currentIndex];
|
||||
const int candidateWidth = spacing + wordWidths[currentIndex];
|
||||
|
||||
if (lineWidth + candidateWidth <= pageWidth) {
|
||||
lineWidth += candidateWidth;
|
||||
++currentIndex;
|
||||
continue;
|
||||
}
|
||||
|
||||
const int availableWidth = pageWidth - lineWidth - spacing;
|
||||
const bool allowFallbackBreaks = isFirstWord;
|
||||
|
||||
bool insertedHyphen = false;
|
||||
if (availableWidth > 0 && hyphenateWordAtIndex(currentIndex, availableWidth, renderer, fontId, wordWidths,
|
||||
allowFallbackBreaks, &insertedHyphen)) {
|
||||
interWordGaps.insert(interWordGaps.begin() + currentIndex + 1, 0);
|
||||
lineEndedWithHyphenation = true;
|
||||
splitPrefixIndex = static_cast<int>(currentIndex);
|
||||
splitNeedsInsertedHyphen = insertedHyphen;
|
||||
lineWidth += spacing + wordWidths[currentIndex];
|
||||
++currentIndex;
|
||||
break;
|
||||
}
|
||||
|
||||
if (currentIndex == lineStart) {
|
||||
lineWidth += candidateWidth;
|
||||
++currentIndex;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
while (currentIndex > lineStart + 1 && currentIndex < wordWidths.size() && continuesVec[currentIndex]) {
|
||||
--currentIndex;
|
||||
}
|
||||
|
||||
if (lineEndedWithHyphenation &&
|
||||
(splitPrefixIndex < static_cast<int>(lineStart) || splitPrefixIndex >= static_cast<int>(currentIndex))) {
|
||||
lineEndedWithHyphenation = false;
|
||||
splitPrefixIndex = -1;
|
||||
splitNeedsInsertedHyphen = false;
|
||||
}
|
||||
|
||||
lineBreakIndices.push_back(currentIndex);
|
||||
lineEndsWithHyphenatedWord.push_back(lineEndedWithHyphenation);
|
||||
splitPrefixWordIndexes.push_back(splitPrefixIndex);
|
||||
splitInsertedHyphen.push_back(splitNeedsInsertedHyphen);
|
||||
}
|
||||
|
||||
return lineBreakIndices;
|
||||
}
|
||||
|
||||
// Splits words[wordIndex] into prefix (adding a hyphen only when needed) and remainder when a legal breakpoint fits the
|
||||
// available width.
|
||||
bool ParsedText::hyphenateWordAtIndex(const size_t wordIndex, const int availableWidth, const GfxRenderer& renderer,
|
||||
@@ -612,9 +754,8 @@ ParsedText::LineProcessResult ParsedText::extractLine(
|
||||
}
|
||||
}
|
||||
|
||||
// Build line data by moving from the original vectors using index range
|
||||
std::vector<std::string> lineWords(std::make_move_iterator(words.begin() + lastBreakAt),
|
||||
std::make_move_iterator(words.begin() + lineBreak));
|
||||
// Copy line words; keep source intact so retry paths can safely inspect/merge tokens.
|
||||
std::vector<std::string> lineWords(words.begin() + lastBreakAt, words.begin() + lineBreak);
|
||||
std::vector<EpdFontFamily::Style> lineWordStyles(wordStyles.begin() + lastBreakAt, wordStyles.begin() + lineBreak);
|
||||
|
||||
for (auto& word : lineWords) {
|
||||
|
||||
@@ -35,6 +35,12 @@ class ParsedText {
|
||||
std::vector<bool>& lineEndsWithHyphenatedWord,
|
||||
std::vector<int>& splitPrefixWordIndexes,
|
||||
std::vector<bool>& splitInsertedHyphen);
|
||||
std::vector<size_t> computeHyphenatedLineBreaksFromIndex(const GfxRenderer& renderer, int fontId, int pageWidth,
|
||||
std::vector<uint16_t>& wordWidths,
|
||||
std::vector<bool>& continuesVec, size_t startIndex,
|
||||
std::vector<bool>& lineEndsWithHyphenatedWord,
|
||||
std::vector<int>& splitPrefixWordIndexes,
|
||||
std::vector<bool>& splitInsertedHyphen);
|
||||
bool hyphenateWordAtIndex(size_t wordIndex, int availableWidth, const GfxRenderer& renderer, int fontId,
|
||||
std::vector<uint16_t>& wordWidths, bool allowFallbackBreaks,
|
||||
bool* outInsertedHyphen = nullptr);
|
||||
|
||||
@@ -79,6 +79,27 @@ bool isTableStructuralTag(const char* name) {
|
||||
return strcmp(name, "table") == 0 || strcmp(name, "tr") == 0 || strcmp(name, "td") == 0 || strcmp(name, "th") == 0;
|
||||
}
|
||||
|
||||
std::string buildTextBlockPreview(const std::shared_ptr<TextBlock>& line, const size_t maxLen = 120) {
|
||||
if (!line) {
|
||||
return {};
|
||||
}
|
||||
|
||||
std::string preview;
|
||||
const auto& words = line->getWords();
|
||||
for (size_t i = 0; i < words.size(); ++i) {
|
||||
if (i > 0) {
|
||||
preview.push_back(' ');
|
||||
}
|
||||
preview += words[i];
|
||||
if (preview.size() >= maxLen) {
|
||||
preview.resize(maxLen);
|
||||
preview += "...";
|
||||
break;
|
||||
}
|
||||
}
|
||||
return preview;
|
||||
}
|
||||
|
||||
// Calibre sometimes injects empty <p style="margin:0; border:0; height:0">...</p>
|
||||
// spacers inside running prose. Keep them as paragraph boundaries, but ignore
|
||||
// their inner text payload (usually NBSP) to avoid no-break-space glue artifacts.
|
||||
@@ -1396,7 +1417,9 @@ ParsedText::LineProcessResult ChapterHtmlSlimParser::addLineToPage(std::shared_p
|
||||
const bool noRoomForAnotherLine =
|
||||
currentPageNextY + lineHeight <= viewportHeight && currentPageNextY + (lineHeight * 2) > viewportHeight;
|
||||
if (lineEndsWithHyphenatedWord && !suppressHyphenationRetry && noRoomForAnotherLine) {
|
||||
LOG_DBG("EHP", "Requesting line rerender without hyphenation to avoid page-break split word");
|
||||
const std::string linePreview = buildTextBlockPreview(line);
|
||||
LOG_DBG("EHP", "Requesting line rerender without hyphenation to avoid page-break split word: %s",
|
||||
linePreview.c_str());
|
||||
return ParsedText::LineProcessResult::RetryWithoutHyphenation;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user