Add css cache
This commit is contained in:
@@ -165,6 +165,20 @@ std::vector<size_t> ParsedText::computeLineBreaks(const GfxRenderer& renderer, c
|
||||
|
||||
const size_t totalWordCount = words.size();
|
||||
|
||||
// Pre-compute inter-word gaps once so the O(n²) DP inner loop avoids repeated
|
||||
// codepoint scanning and renderer calls for every (i,j) pair.
|
||||
// interWordGaps[j] = the spacing between words[j-1] and words[j] (0 for j==0).
|
||||
std::vector<int> interWordGaps(totalWordCount, 0);
|
||||
for (size_t j = 1; j < totalWordCount; ++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]);
|
||||
}
|
||||
}
|
||||
|
||||
// DP table to store the minimum badness (cost) of lines starting at index i
|
||||
std::vector<int> dp(totalWordCount);
|
||||
// 'ans[i]' stores the index 'j' of the *last word* in the optimal line starting at 'i'
|
||||
@@ -182,15 +196,7 @@ std::vector<size_t> ParsedText::computeLineBreaks(const GfxRenderer& renderer, c
|
||||
const int effectivePageWidth = i == 0 ? pageWidth - firstLineIndent : pageWidth;
|
||||
|
||||
for (size_t j = i; j < totalWordCount; ++j) {
|
||||
// Add space before word j, unless it's the first word on the line or a continuation
|
||||
int gap = 0;
|
||||
if (j > static_cast<size_t>(i) && !continuesVec[j]) {
|
||||
gap =
|
||||
renderer.getSpaceAdvance(fontId, lastCodepoint(words[j - 1]), firstCodepoint(words[j]), wordStyles[j - 1]);
|
||||
} else if (j > static_cast<size_t>(i) && continuesVec[j]) {
|
||||
// Cross-boundary kerning for continuation words (e.g. nonbreaking spaces, attached punctuation)
|
||||
gap = renderer.getKerning(fontId, lastCodepoint(words[j - 1]), firstCodepoint(words[j]), wordStyles[j - 1]);
|
||||
}
|
||||
const int gap = (j > static_cast<size_t>(i)) ? interWordGaps[j] : 0;
|
||||
currlen += wordWidths[j] + gap;
|
||||
|
||||
if (currlen > effectivePageWidth) {
|
||||
@@ -284,6 +290,21 @@ std::vector<size_t> ParsedText::computeHyphenatedLineBreaks(const GfxRenderer& r
|
||||
? blockStyle.textIndent
|
||||
: 0;
|
||||
|
||||
// Pre-compute inter-word gaps to avoid repeated codepoint scanning and renderer
|
||||
// calls in the inner loop. When hyphenateWordAtIndex inserts a new word, we insert
|
||||
// a placeholder gap (0) at that position to keep the vector in sync; the remainder
|
||||
// is always the first word on the next line so its spacing is never used.
|
||||
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;
|
||||
size_t currentIndex = 0;
|
||||
bool isFirstLine = true;
|
||||
@@ -298,15 +319,7 @@ std::vector<size_t> ParsedText::computeHyphenatedLineBreaks(const GfxRenderer& r
|
||||
// Consume as many words as possible for current line, splitting when prefixes fit
|
||||
while (currentIndex < wordWidths.size()) {
|
||||
const bool isFirstWord = currentIndex == lineStart;
|
||||
int spacing = 0;
|
||||
if (!isFirstWord && !continuesVec[currentIndex]) {
|
||||
spacing = renderer.getSpaceAdvance(fontId, lastCodepoint(words[currentIndex - 1]),
|
||||
firstCodepoint(words[currentIndex]), wordStyles[currentIndex - 1]);
|
||||
} else if (!isFirstWord && continuesVec[currentIndex]) {
|
||||
// Cross-boundary kerning for continuation words (e.g. nonbreaking spaces, attached punctuation)
|
||||
spacing = renderer.getKerning(fontId, lastCodepoint(words[currentIndex - 1]),
|
||||
firstCodepoint(words[currentIndex]), wordStyles[currentIndex - 1]);
|
||||
}
|
||||
const int spacing = isFirstWord ? 0 : interWordGaps[currentIndex];
|
||||
const int candidateWidth = spacing + wordWidths[currentIndex];
|
||||
|
||||
// Word fits on current line
|
||||
@@ -322,6 +335,9 @@ std::vector<size_t> ParsedText::computeHyphenatedLineBreaks(const GfxRenderer& r
|
||||
|
||||
if (availableWidth > 0 &&
|
||||
hyphenateWordAtIndex(currentIndex, availableWidth, renderer, fontId, wordWidths, allowFallbackBreaks)) {
|
||||
// Keep interWordGaps in sync: insert placeholder for the new remainder word.
|
||||
// The remainder is always the first word on the next line so this slot is never read.
|
||||
interWordGaps.insert(interWordGaps.begin() + currentIndex + 1, 0);
|
||||
// Prefix now fits; append it to this line and move to next line
|
||||
lineWidth += spacing + wordWidths[currentIndex];
|
||||
++currentIndex;
|
||||
|
||||
@@ -186,10 +186,20 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char*
|
||||
// before tag-specific branches emit any content or metadata.
|
||||
CssStyle cssStyle;
|
||||
if (self->cssParser) {
|
||||
cssStyle = self->cssParser->resolveStyle(name, classAttr);
|
||||
{
|
||||
std::string cacheKey(name);
|
||||
cacheKey += '|';
|
||||
cacheKey += classAttr;
|
||||
auto it = self->cssStyleCache_.find(cacheKey);
|
||||
if (it == self->cssStyleCache_.end())
|
||||
it = self->cssStyleCache_.emplace(cacheKey, self->cssParser->resolveStyle(name, classAttr)).first;
|
||||
cssStyle = it->second;
|
||||
}
|
||||
if (!styleAttr.empty()) {
|
||||
CssStyle inlineStyle = CssParser::parseInlineStyle(styleAttr);
|
||||
cssStyle.applyOver(inlineStyle);
|
||||
auto it = self->inlineStyleCache_.find(styleAttr);
|
||||
if (it == self->inlineStyleCache_.end())
|
||||
it = self->inlineStyleCache_.emplace(styleAttr, CssParser::parseInlineStyle(styleAttr)).first;
|
||||
cssStyle.applyOver(it->second);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -284,9 +294,17 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char*
|
||||
|
||||
// Skip image if CSS display:none
|
||||
if (self->cssParser) {
|
||||
CssStyle imgDisplayStyle = self->cssParser->resolveStyle("img", classAttr);
|
||||
std::string imgCacheKey("img|");
|
||||
imgCacheKey += classAttr;
|
||||
auto imgIt = self->cssStyleCache_.find(imgCacheKey);
|
||||
if (imgIt == self->cssStyleCache_.end())
|
||||
imgIt = self->cssStyleCache_.emplace(imgCacheKey, self->cssParser->resolveStyle("img", classAttr)).first;
|
||||
CssStyle imgDisplayStyle = imgIt->second;
|
||||
if (!styleAttr.empty()) {
|
||||
imgDisplayStyle.applyOver(CssParser::parseInlineStyle(styleAttr));
|
||||
auto it = self->inlineStyleCache_.find(styleAttr);
|
||||
if (it == self->inlineStyleCache_.end())
|
||||
it = self->inlineStyleCache_.emplace(styleAttr, CssParser::parseInlineStyle(styleAttr)).first;
|
||||
imgDisplayStyle.applyOver(it->second);
|
||||
}
|
||||
if (imgDisplayStyle.hasDisplay() && imgDisplayStyle.display == CssDisplay::None) {
|
||||
self->skipUntilDepth = self->depth;
|
||||
@@ -331,10 +349,19 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char*
|
||||
int displayWidth = 0;
|
||||
int displayHeight = 0;
|
||||
const float emSize = static_cast<float>(self->renderer.getFontAscenderSize(self->fontId));
|
||||
CssStyle imgStyle = self->cssParser ? self->cssParser->resolveStyle("img", classAttr) : CssStyle{};
|
||||
std::string imgCacheKey("img|");
|
||||
imgCacheKey += classAttr;
|
||||
auto imgStyleIt = self->cssParser ? self->cssStyleCache_.find(imgCacheKey) : self->cssStyleCache_.end();
|
||||
if (self->cssParser && imgStyleIt == self->cssStyleCache_.end())
|
||||
imgStyleIt =
|
||||
self->cssStyleCache_.emplace(imgCacheKey, self->cssParser->resolveStyle("img", classAttr)).first;
|
||||
CssStyle imgStyle = self->cssParser ? imgStyleIt->second : CssStyle{};
|
||||
// Merge inline style (e.g. style="height: 2em") so it overrides stylesheet rules
|
||||
if (!styleAttr.empty()) {
|
||||
imgStyle.applyOver(CssParser::parseInlineStyle(styleAttr));
|
||||
auto it = self->inlineStyleCache_.find(styleAttr);
|
||||
if (it == self->inlineStyleCache_.end())
|
||||
it = self->inlineStyleCache_.emplace(styleAttr, CssParser::parseInlineStyle(styleAttr)).first;
|
||||
imgStyle.applyOver(it->second);
|
||||
}
|
||||
const bool hasCssHeight = imgStyle.hasImageHeight();
|
||||
const bool hasCssWidth = imgStyle.hasImageWidth();
|
||||
@@ -708,6 +735,22 @@ void XMLCALL ChapterHtmlSlimParser::characterData(void* userData, const XML_Char
|
||||
}
|
||||
|
||||
for (int i = 0; i < len; i++) {
|
||||
const unsigned char c = static_cast<unsigned char>(s[i]);
|
||||
|
||||
// Fast path for plain ASCII word characters (> 0x20 and < 0x80).
|
||||
// This covers the vast majority of characters in Latin-script text.
|
||||
// All multi-byte UTF-8 sequences start with a byte >= 0x80, so this
|
||||
// path is safe to take without any further multi-byte checks.
|
||||
if (c > 0x20 && c < 0x80) {
|
||||
if (self->partWordBufferIndex >= MAX_WORD_SIZE) {
|
||||
// Buffer is full — flush before appending. Pure ASCII means no
|
||||
// partial multi-byte sequence can be at the boundary.
|
||||
self->flushPartWordBuffer();
|
||||
}
|
||||
self->partWordBuffer[self->partWordBufferIndex++] = s[i];
|
||||
continue;
|
||||
}
|
||||
|
||||
if (isWhitespace(s[i])) {
|
||||
// Currently looking at whitespace, if there's anything in the partWordBuffer, flush it
|
||||
if (self->partWordBufferIndex > 0) {
|
||||
@@ -1047,7 +1090,8 @@ bool ChapterHtmlSlimParser::parseAndBuildPages() {
|
||||
return false;
|
||||
}
|
||||
} while (!done);
|
||||
LOG_DBG("EHP", "Time to parse and build pages: %lu ms", millis() - chapterStartTime);
|
||||
const uint32_t totalTimeMs = millis() - chapterStartTime;
|
||||
LOG_DBG("EHP", "Time to parse and build pages: %lu ms", totalTimeMs);
|
||||
|
||||
XML_StopParser(parser, XML_FALSE); // Stop any pending processing
|
||||
XML_SetElementHandler(parser, nullptr, nullptr); // Clear callbacks
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
#include "../FootnoteEntry.h"
|
||||
@@ -84,6 +85,11 @@ class ChapterHtmlSlimParser {
|
||||
std::vector<std::pair<int, FootnoteEntry>> pendingFootnotes; // <wordIndex, entry>
|
||||
int wordsExtractedInBlock = 0;
|
||||
|
||||
// Per-chapter caches: resolveStyle and parseInlineStyle are called for every HTML element;
|
||||
// caching by (tag|classAttr) and styleAttr avoids repeated string operations and hash lookups.
|
||||
std::unordered_map<std::string, CssStyle> cssStyleCache_;
|
||||
std::unordered_map<std::string, CssStyle> inlineStyleCache_;
|
||||
|
||||
void updateEffectiveInlineStyle();
|
||||
void startNewTextBlock(const BlockStyle& blockStyle);
|
||||
void flushPartWordBuffer();
|
||||
|
||||
Reference in New Issue
Block a user