From 65474d40662e2e01ad689839e8f179e5ad97c819 Mon Sep 17 00:00:00 2001 From: jpirnay Date: Sun, 5 Apr 2026 11:18:19 +0200 Subject: [PATCH] Fix alignment issues --- lib/Epub/Epub/ParsedText.cpp | 131 ++++++++++++++++++++++++++++------- lib/Epub/Epub/ParsedText.h | 7 ++ 2 files changed, 112 insertions(+), 26 deletions(-) diff --git a/lib/Epub/Epub/ParsedText.cpp b/lib/Epub/Epub/ParsedText.cpp index 2d155848..3c2964ca 100644 --- a/lib/Epub/Epub/ParsedText.cpp +++ b/lib/Epub/Epub/ParsedText.cpp @@ -8,6 +8,7 @@ #include #include #include +#include #include #include "hyphenation/Hyphenator.h" @@ -77,6 +78,8 @@ uint16_t measureWordWidth(const GfxRenderer& renderer, const int fontId, const s std::string buildLinePreview(const std::vector& words, const std::vector& continuesVec, const size_t start, const size_t endExclusive, const size_t maxLen = 120) { + // Build a readable line preview while preserving continuation semantics + // (no synthetic spaces before attached tokens). std::string preview; for (size_t idx = start; idx < endExclusive; ++idx) { if (idx > start && idx < continuesVec.size() && !continuesVec[idx]) { @@ -150,26 +153,55 @@ void ParsedText::layoutAndExtractLines( 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()) { - std::string merged = words[splitPrefixIndex]; - if (i < splitInsertedHyphen.size() && splitInsertedHyphen[i] && !merged.empty() && merged.back() == '-') { + // Undo precomputed splits from this line onward so the retry starts from + // clean, unsplit tokens and cannot inherit future hyphenation artifacts. + std::set> splitIndexesToUndo; + for (size_t lineIdx = i; lineIdx < splitPrefixWordIndexes.size(); ++lineIdx) { + const int splitIndex = splitPrefixWordIndexes[lineIdx]; + if (splitIndex >= 0) { + splitIndexesToUndo.insert(splitIndex); + } + } + for (const int splitIndex : splitIndexesToUndo) { + if (splitIndex < 0 || static_cast(splitIndex + 1) >= words.size()) { + continue; + } + bool removeInsertedHyphen = false; + for (size_t lineIdx = i; lineIdx < splitPrefixWordIndexes.size(); ++lineIdx) { + if (splitPrefixWordIndexes[lineIdx] == splitIndex && lineIdx < splitInsertedHyphen.size()) { + removeInsertedHyphen = splitInsertedHyphen[lineIdx]; + break; + } + } + + std::string merged = words[splitIndex]; + if (removeInsertedHyphen && !merged.empty() && merged.back() == '-') { merged.pop_back(); } - merged += words[splitPrefixIndex + 1]; - words[splitPrefixIndex] = std::move(merged); - words.erase(words.begin() + splitPrefixIndex + 1); - wordStyles.erase(wordStyles.begin() + splitPrefixIndex + 1); - wordContinues.erase(wordContinues.begin() + splitPrefixIndex + 1); + merged += words[splitIndex + 1]; + words[splitIndex] = std::move(merged); + words.erase(words.begin() + splitIndex + 1); + wordStyles.erase(wordStyles.begin() + splitIndex + 1); + wordContinues.erase(wordContinues.begin() + splitIndex + 1); } - // Re-layout remaining output without hyphenation for this pass. + // Recompute widths after restoring unsplit words. wordWidths = calculateWordWidths(renderer, fontId); - lineBreakIndices = computeLineBreaks(renderer, fontId, pageWidth, wordWidths, wordContinues); - lineEndsWithHyphenatedWord.assign(lineBreakIndices.size(), false); - splitPrefixWordIndexes.assign(lineBreakIndices.size(), -1); - splitInsertedHyphen.assign(lineBreakIndices.size(), false); + + // Keep previous lines fixed; recompute only this specific line without hyphenation. + // Suppression is intentionally line-local. + const size_t retryBreak = + computeSingleLineBreakNoHyphen(renderer, fontId, pageWidth, wordWidths, wordContinues, lineStart); + + lineBreakIndices.resize(i + 1); + lineEndsWithHyphenatedWord.resize(i + 1); + splitPrefixWordIndexes.resize(i + 1); + splitInsertedHyphen.resize(i + 1); + + lineBreakIndices[i] = retryBreak; + lineEndsWithHyphenatedWord[i] = false; + splitPrefixWordIndexes[i] = -1; + splitInsertedHyphen[i] = false; lineCount = includeLastLine ? lineBreakIndices.size() : lineBreakIndices.size() - 1; if (i < lineCount) { @@ -181,7 +213,7 @@ void ParsedText::layoutAndExtractLines( extractLine(i, pageWidth, wordWidths, wordContinues, lineBreakIndices, processLine, renderer, fontId, false, true); - // Continue with regular hyphenation for subsequent lines only. + // Resume regular hyphenation from the first word after the retried line. const size_t resumeIndex = lineBreakIndices[i]; std::vector suffixLineEndsWithHyphenatedWord; std::vector suffixSplitPrefixWordIndexes; @@ -190,15 +222,6 @@ void ParsedText::layoutAndExtractLines( 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()); @@ -359,6 +382,58 @@ std::vector ParsedText::computeLineBreaks(const GfxRenderer& renderer, c return lineBreakIndices; } +size_t ParsedText::computeSingleLineBreakNoHyphen(const GfxRenderer& renderer, const int fontId, const int pageWidth, + const std::vector& wordWidths, + const std::vector& continuesVec, + const size_t lineStartIndex) const { + // One-line non-hyphenating breaker used by the page-boundary retry path. + if (lineStartIndex >= wordWidths.size()) { + return lineStartIndex; + } + + const int firstLineIndent = + lineStartIndex == 0 && blockStyle.textIndentDefined && (blockStyle.textIndent < 0 || !extraParagraphSpacing) && + (blockStyle.alignment == CssTextAlign::Justify || blockStyle.alignment == CssTextAlign::Left) + ? blockStyle.textIndent + : 0; + const int effectivePageWidth = pageWidth - firstLineIndent; + + size_t currentIndex = lineStartIndex; + int lineWidth = 0; + + while (currentIndex < wordWidths.size()) { + const bool isFirstWord = currentIndex == lineStartIndex; + int spacing = 0; + if (!isFirstWord) { + if (!continuesVec[currentIndex]) { + spacing = renderer.getSpaceAdvance(fontId, lastCodepoint(words[currentIndex - 1]), + firstCodepoint(words[currentIndex]), wordStyles[currentIndex - 1]); + } else { + spacing = renderer.getKerning(fontId, lastCodepoint(words[currentIndex - 1]), + firstCodepoint(words[currentIndex]), wordStyles[currentIndex - 1]); + } + } + + const int candidateWidth = spacing + wordWidths[currentIndex]; + if (lineWidth + candidateWidth <= effectivePageWidth) { + lineWidth += candidateWidth; + ++currentIndex; + continue; + } + + if (currentIndex == lineStartIndex) { + ++currentIndex; + } + break; + } + + while (currentIndex > lineStartIndex + 1 && currentIndex < wordWidths.size() && continuesVec[currentIndex]) { + --currentIndex; + } + + return currentIndex; +} + void ParsedText::applyParagraphIndent() { if (extraParagraphSpacing || words.empty()) { return; @@ -489,6 +564,7 @@ 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) { + // Same greedy hyphenating breaker as the full pass, but scoped to a suffix. if (startIndex >= wordWidths.size()) { lineEndsWithHyphenatedWord.clear(); splitPrefixWordIndexes.clear(); @@ -707,7 +783,10 @@ ParsedText::LineProcessResult ParsedText::extractLine( // Calculate spacing (account for indent reducing effective page width on first line) const int effectivePageWidth = pageWidth - firstLineIndent; - const bool isLastLine = breakIndex == lineBreakIndices.size() - 1; + // A line is only truly last when it consumes all paragraph words. + // During single-line retry we may temporarily pass a truncated break vector, + // so relying only on breakIndex would incorrectly disable justification. + const bool isLastLine = lineBreak == words.size(); // For justified text, compute per-gap extra to distribute remaining space evenly const int spareSpace = effectivePageWidth - lineWordWidthSum - totalNaturalGaps; diff --git a/lib/Epub/Epub/ParsedText.h b/lib/Epub/Epub/ParsedText.h index 96410752..6aa7e3cc 100644 --- a/lib/Epub/Epub/ParsedText.h +++ b/lib/Epub/Epub/ParsedText.h @@ -35,12 +35,19 @@ class ParsedText { std::vector& lineEndsWithHyphenatedWord, std::vector& splitPrefixWordIndexes, std::vector& splitInsertedHyphen); + // Recompute hyphenated breaks for a suffix that starts at startIndex. + // Used after a single-line retry so later lines keep normal hyphenation. 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); + // Compute exactly one line break without hyphenating words. + // Used only for the page-boundary retry line. + size_t computeSingleLineBreakNoHyphen(const GfxRenderer& renderer, int fontId, int pageWidth, + const std::vector& wordWidths, const std::vector& continuesVec, + size_t lineStartIndex) const; bool hyphenateWordAtIndex(size_t wordIndex, int availableWidth, const GfxRenderer& renderer, int fontId, std::vector& wordWidths, bool allowFallbackBreaks, bool* outInsertedHyphen = nullptr);