Fix reflow logic

This commit is contained in:
jpirnay
2026-04-05 11:01:40 +02:00
parent 1509af8729
commit 01503f9c84
3 changed files with 176 additions and 6 deletions
+146 -5
View File
@@ -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) {
+6
View File
@@ -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;
}