diff --git a/lib/Epub/Epub/ParsedText.cpp b/lib/Epub/Epub/ParsedText.cpp index 496d6908..2d155848 100644 --- a/lib/Epub/Epub/ParsedText.cpp +++ b/lib/Epub/Epub/ParsedText.cpp @@ -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& words, const std::vector& 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(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(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(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(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(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 suffixLineEndsWithHyphenatedWord; + std::vector suffixSplitPrefixWordIndexes; + std::vector 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(i)); } } } @@ -431,6 +485,94 @@ std::vector ParsedText::computeHyphenatedLineBreaks(const GfxRenderer& r return lineBreakIndices; } +std::vector ParsedText::computeHyphenatedLineBreaksFromIndex( + const GfxRenderer& renderer, const int fontId, const int pageWidth, std::vector& wordWidths, + std::vector& continuesVec, const size_t startIndex, std::vector& lineEndsWithHyphenatedWord, + std::vector& splitPrefixWordIndexes, std::vector& splitInsertedHyphen) { + if (startIndex >= wordWidths.size()) { + lineEndsWithHyphenatedWord.clear(); + splitPrefixWordIndexes.clear(); + splitInsertedHyphen.clear(); + return {}; + } + + std::vector 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 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(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(lineStart) || splitPrefixIndex >= static_cast(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 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 lineWords(words.begin() + lastBreakAt, words.begin() + lineBreak); std::vector lineWordStyles(wordStyles.begin() + lastBreakAt, wordStyles.begin() + lineBreak); for (auto& word : lineWords) { diff --git a/lib/Epub/Epub/ParsedText.h b/lib/Epub/Epub/ParsedText.h index 6546e2be..96410752 100644 --- a/lib/Epub/Epub/ParsedText.h +++ b/lib/Epub/Epub/ParsedText.h @@ -35,6 +35,12 @@ class ParsedText { std::vector& lineEndsWithHyphenatedWord, std::vector& splitPrefixWordIndexes, std::vector& splitInsertedHyphen); + std::vector computeHyphenatedLineBreaksFromIndex(const GfxRenderer& renderer, int fontId, int pageWidth, + std::vector& wordWidths, + std::vector& continuesVec, size_t startIndex, + std::vector& lineEndsWithHyphenatedWord, + std::vector& splitPrefixWordIndexes, + std::vector& splitInsertedHyphen); bool hyphenateWordAtIndex(size_t wordIndex, int availableWidth, const GfxRenderer& renderer, int fontId, std::vector& wordWidths, bool allowFallbackBreaks, bool* outInsertedHyphen = nullptr); diff --git a/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp b/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp index 90850d8d..a7982a53 100644 --- a/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp +++ b/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp @@ -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& 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

...

// 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; }