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);
|
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
|
} // namespace
|
||||||
|
|
||||||
void ParsedText::addWord(std::string word, const EpdFontFamily::Style fontStyle, const bool underline,
|
void ParsedText::addWord(std::string word, const EpdFontFamily::Style fontStyle, const bool underline,
|
||||||
@@ -128,7 +145,11 @@ void ParsedText::layoutAndExtractLines(
|
|||||||
fontId, lineEndedWithHyphenation, false);
|
fontId, lineEndedWithHyphenation, false);
|
||||||
|
|
||||||
if (result == LineProcessResult::RetryWithoutHyphenation && lineEndedWithHyphenation) {
|
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.
|
// Undo the split used to end this line so it can be relaid without hyphenation.
|
||||||
const int splitPrefixIndex = i < splitPrefixWordIndexes.size() ? splitPrefixWordIndexes[i] : -1;
|
const int splitPrefixIndex = i < splitPrefixWordIndexes.size() ? splitPrefixWordIndexes[i] : -1;
|
||||||
if (splitPrefixIndex >= 0 && static_cast<size_t>(splitPrefixIndex + 1) < words.size()) {
|
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;
|
lineCount = includeLastLine ? lineBreakIndices.size() : lineBreakIndices.size() - 1;
|
||||||
|
|
||||||
if (i < lineCount) {
|
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,
|
extractLine(i, pageWidth, wordWidths, wordContinues, lineBreakIndices, processLine, renderer, fontId, false,
|
||||||
true);
|
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;
|
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
|
// Splits words[wordIndex] into prefix (adding a hyphen only when needed) and remainder when a legal breakpoint fits the
|
||||||
// available width.
|
// available width.
|
||||||
bool ParsedText::hyphenateWordAtIndex(const size_t wordIndex, const int availableWidth, const GfxRenderer& renderer,
|
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
|
// Copy line words; keep source intact so retry paths can safely inspect/merge tokens.
|
||||||
std::vector<std::string> lineWords(std::make_move_iterator(words.begin() + lastBreakAt),
|
std::vector<std::string> lineWords(words.begin() + lastBreakAt, words.begin() + lineBreak);
|
||||||
std::make_move_iterator(words.begin() + lineBreak));
|
|
||||||
std::vector<EpdFontFamily::Style> lineWordStyles(wordStyles.begin() + lastBreakAt, wordStyles.begin() + lineBreak);
|
std::vector<EpdFontFamily::Style> lineWordStyles(wordStyles.begin() + lastBreakAt, wordStyles.begin() + lineBreak);
|
||||||
|
|
||||||
for (auto& word : lineWords) {
|
for (auto& word : lineWords) {
|
||||||
|
|||||||
@@ -35,6 +35,12 @@ class ParsedText {
|
|||||||
std::vector<bool>& lineEndsWithHyphenatedWord,
|
std::vector<bool>& lineEndsWithHyphenatedWord,
|
||||||
std::vector<int>& splitPrefixWordIndexes,
|
std::vector<int>& splitPrefixWordIndexes,
|
||||||
std::vector<bool>& splitInsertedHyphen);
|
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,
|
bool hyphenateWordAtIndex(size_t wordIndex, int availableWidth, const GfxRenderer& renderer, int fontId,
|
||||||
std::vector<uint16_t>& wordWidths, bool allowFallbackBreaks,
|
std::vector<uint16_t>& wordWidths, bool allowFallbackBreaks,
|
||||||
bool* outInsertedHyphen = nullptr);
|
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;
|
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>
|
// Calibre sometimes injects empty <p style="margin:0; border:0; height:0">...</p>
|
||||||
// spacers inside running prose. Keep them as paragraph boundaries, but ignore
|
// spacers inside running prose. Keep them as paragraph boundaries, but ignore
|
||||||
// their inner text payload (usually NBSP) to avoid no-break-space glue artifacts.
|
// 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 =
|
const bool noRoomForAnotherLine =
|
||||||
currentPageNextY + lineHeight <= viewportHeight && currentPageNextY + (lineHeight * 2) > viewportHeight;
|
currentPageNextY + lineHeight <= viewportHeight && currentPageNextY + (lineHeight * 2) > viewportHeight;
|
||||||
if (lineEndsWithHyphenatedWord && !suppressHyphenationRetry && noRoomForAnotherLine) {
|
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;
|
return ParsedText::LineProcessResult::RetryWithoutHyphenation;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user