#include "ParsedText.h" #include #include #include #include #include #include constexpr int MAX_COST = std::numeric_limits::max(); void ParsedText::addWord(std::string word, const EpdFontFamily::Style fontStyle) { if (word.empty()) return; words.push_back(std::move(word)); wordStyles.push_back(fontStyle); } // Consumes data to minimize memory usage void ParsedText::layoutAndExtractLines(const GfxRenderer& renderer, const int fontId, const uint16_t viewportWidth, const std::function)>& processLine, const bool includeLastLine) { if (words.empty()) { return; } const int pageWidth = viewportWidth; const int spaceWidth = renderer.getSpaceWidth(fontId); const auto wordWidths = calculateWordWidths(renderer, fontId); const auto lineBreakIndices = computeLineBreaks(pageWidth, spaceWidth, wordWidths); const size_t lineCount = includeLastLine ? lineBreakIndices.size() : lineBreakIndices.size() - 1; for (size_t i = 0; i < lineCount; ++i) { extractLine(i, pageWidth, spaceWidth, wordWidths, lineBreakIndices, processLine); } } std::vector ParsedText::calculateWordWidths(const GfxRenderer& renderer, const int fontId) { const size_t totalWordCount = words.size(); std::vector wordWidths; wordWidths.reserve(totalWordCount); // add em-space at the beginning of first word in paragraph to indent if ((style == TextBlock::JUSTIFIED || style == TextBlock::LEFT_ALIGN) && !extraParagraphSpacing) { std::string& first_word = words.front(); first_word.insert(0, "\xe2\x80\x83"); } auto wordsIt = words.begin(); auto wordStylesIt = wordStyles.begin(); while (wordsIt != words.end()) { wordWidths.push_back(renderer.getTextWidth(fontId, wordsIt->c_str(), *wordStylesIt)); std::advance(wordsIt, 1); std::advance(wordStylesIt, 1); } return wordWidths; } std::vector ParsedText::computeLineBreaks(const int pageWidth, const int spaceWidth, const std::vector& wordWidths) const { const size_t totalWordCount = words.size(); // DP table to store the minimum badness (cost) of lines starting at index i std::vector dp(totalWordCount); // 'ans[i]' stores the index 'j' of the *last word* in the optimal line starting at 'i' std::vector ans(totalWordCount); // Base Case dp[totalWordCount - 1] = 0; ans[totalWordCount - 1] = totalWordCount - 1; for (int i = totalWordCount - 2; i >= 0; --i) { int currlen = -spaceWidth; dp[i] = MAX_COST; for (size_t j = i; j < totalWordCount; ++j) { // Current line length: previous width + space + current word width currlen += wordWidths[j] + spaceWidth; if (currlen > pageWidth) { break; } int cost; if (j == totalWordCount - 1) { cost = 0; // Last line } else { const int remainingSpace = pageWidth - currlen; // Use long long for the square to prevent overflow const long long cost_ll = static_cast(remainingSpace) * remainingSpace + dp[j + 1]; if (cost_ll > MAX_COST) { cost = MAX_COST; } else { cost = static_cast(cost_ll); } } if (cost < dp[i]) { dp[i] = cost; ans[i] = j; // j is the index of the last word in this optimal line } } // Handle oversized word: if no valid configuration found, force single-word line // This prevents cascade failure where one oversized word breaks all preceding words if (dp[i] == MAX_COST) { ans[i] = i; // Just this word on its own line // Inherit cost from next word to allow subsequent words to find valid configurations if (i + 1 < static_cast(totalWordCount)) { dp[i] = dp[i + 1]; } else { dp[i] = 0; } } } // Stores the index of the word that starts the next line (last_word_index + 1) std::vector lineBreakIndices; size_t currentWordIndex = 0; while (currentWordIndex < totalWordCount) { size_t nextBreakIndex = ans[currentWordIndex] + 1; // Safety check: prevent infinite loop if nextBreakIndex doesn't advance if (nextBreakIndex <= currentWordIndex) { // Force advance by at least one word to avoid infinite loop nextBreakIndex = currentWordIndex + 1; } lineBreakIndices.push_back(nextBreakIndex); currentWordIndex = nextBreakIndex; } return lineBreakIndices; } void ParsedText::extractLine(const size_t breakIndex, const int pageWidth, const int spaceWidth, const std::vector& wordWidths, const std::vector& lineBreakIndices, const std::function)>& processLine) { const size_t lineBreak = lineBreakIndices[breakIndex]; const size_t lastBreakAt = breakIndex > 0 ? lineBreakIndices[breakIndex - 1] : 0; const size_t lineWordCount = lineBreak - lastBreakAt; // Calculate total word width for this line int lineWordWidthSum = 0; for (size_t i = lastBreakAt; i < lineBreak; i++) { lineWordWidthSum += wordWidths[i]; } // Calculate spacing const int spareSpace = pageWidth - lineWordWidthSum; int spacing = spaceWidth; const bool isLastLine = breakIndex == lineBreakIndices.size() - 1; if (style == TextBlock::JUSTIFIED && !isLastLine && lineWordCount >= 2) { spacing = spareSpace / (lineWordCount - 1); } // Calculate initial x position uint16_t xpos = 0; if (style == TextBlock::RIGHT_ALIGN) { xpos = spareSpace - (lineWordCount - 1) * spaceWidth; } else if (style == TextBlock::CENTER_ALIGN) { xpos = (spareSpace - (lineWordCount - 1) * spaceWidth) / 2; } // Pre-calculate X positions for words std::list lineXPos; for (size_t i = lastBreakAt; i < lineBreak; i++) { const uint16_t currentWordWidth = wordWidths[i]; lineXPos.push_back(xpos); xpos += currentWordWidth + spacing; } // Iterators always start at the beginning as we are moving content with splice below auto wordEndIt = words.begin(); auto wordStyleEndIt = wordStyles.begin(); std::advance(wordEndIt, lineWordCount); std::advance(wordStyleEndIt, lineWordCount); // *** CRITICAL STEP: CONSUME DATA USING SPLICE *** std::list lineWords; lineWords.splice(lineWords.begin(), words, words.begin(), wordEndIt); std::list lineWordStyles; lineWordStyles.splice(lineWordStyles.begin(), wordStyles, wordStyles.begin(), wordStyleEndIt); processLine(std::make_shared(std::move(lineWords), std::move(lineXPos), std::move(lineWordStyles), style)); }