Merge pull request #162 from spfenwick/fix-oom-crash
fix: Avoid OOM crash when using SD font with many kern pairs
This commit is contained in:
+89
-38
@@ -52,6 +52,21 @@ void SdCardFont::freeStyleMiniData(PerStyle& s) {
|
|||||||
s.miniIntervalCount = 0;
|
s.miniIntervalCount = 0;
|
||||||
s.miniGlyphCount = 0;
|
s.miniGlyphCount = 0;
|
||||||
s.miniMode = PerStyle::MiniMode::NONE;
|
s.miniMode = PerStyle::MiniMode::NONE;
|
||||||
|
// Clear dangling pointers in miniData and stubData (kern data points to freed mini arrays)
|
||||||
|
s.miniData.kernLeftClasses = nullptr;
|
||||||
|
s.miniData.kernRightClasses = nullptr;
|
||||||
|
s.miniData.kernMatrix = nullptr;
|
||||||
|
s.miniData.kernLeftEntryCount = 0;
|
||||||
|
s.miniData.kernRightEntryCount = 0;
|
||||||
|
s.miniData.kernLeftClassCount = 0;
|
||||||
|
s.miniData.kernRightClassCount = 0;
|
||||||
|
s.stubData.kernLeftClasses = nullptr;
|
||||||
|
s.stubData.kernRightClasses = nullptr;
|
||||||
|
s.stubData.kernMatrix = nullptr;
|
||||||
|
s.stubData.kernLeftEntryCount = 0;
|
||||||
|
s.stubData.kernRightEntryCount = 0;
|
||||||
|
s.stubData.kernLeftClassCount = 0;
|
||||||
|
s.stubData.kernRightClassCount = 0;
|
||||||
// NOTE: reportedMissCount is intentionally NOT reset here. The merge path
|
// NOTE: reportedMissCount is intentionally NOT reset here. The merge path
|
||||||
// calls freeStyleMiniData() to swap mini buffers, and resetting the miss
|
// calls freeStyleMiniData() to swap mini buffers, and resetting the miss
|
||||||
// tracker every paragraph would re-spam the log for the same 4 missing cps.
|
// tracker every paragraph would re-spam the log for the same 4 missing cps.
|
||||||
@@ -66,11 +81,17 @@ void SdCardFont::freeStyleMiniData(PerStyle& s) {
|
|||||||
void SdCardFont::freeStyleKernLigatureData(PerStyle& s) {
|
void SdCardFont::freeStyleKernLigatureData(PerStyle& s) {
|
||||||
delete[] s.kernLeftClasses;
|
delete[] s.kernLeftClasses;
|
||||||
s.kernLeftClasses = nullptr;
|
s.kernLeftClasses = nullptr;
|
||||||
|
s.kernClassesLoaded = false;
|
||||||
delete[] s.kernRightClasses;
|
delete[] s.kernRightClasses;
|
||||||
s.kernRightClasses = nullptr;
|
s.kernRightClasses = nullptr;
|
||||||
delete[] s.ligaturePairs;
|
delete[] s.ligaturePairs;
|
||||||
s.ligaturePairs = nullptr;
|
s.ligaturePairs = nullptr;
|
||||||
s.kernLigLoaded = false;
|
s.ligLoaded = false;
|
||||||
|
// Clear dangling pointers in EpdFontData structs
|
||||||
|
s.stubData.ligaturePairs = nullptr;
|
||||||
|
s.stubData.ligaturePairCount = 0;
|
||||||
|
s.miniData.ligaturePairs = nullptr;
|
||||||
|
s.miniData.ligaturePairCount = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
void SdCardFont::freeStyleMiniKern(PerStyle& s) {
|
void SdCardFont::freeStyleMiniKern(PerStyle& s) {
|
||||||
@@ -134,14 +155,17 @@ void SdCardFont::applyKernLigaturePointers(const PerStyle& s, EpdFontData& data)
|
|||||||
data.ligaturePairCount = s.header.ligaturePairCount;
|
data.ligaturePairCount = s.header.ligaturePairCount;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool SdCardFont::loadStyleKernLigatureData(PerStyle& s) {
|
bool SdCardFont::loadStyleKernLigatureData(PerStyle& s, bool ligatureOnly) {
|
||||||
if (s.kernLigLoaded) return true;
|
// During metadata-only (layout) prewarms, skip the kern class tables: the kern
|
||||||
bool hasKern = s.header.kernLeftEntryCount > 0;
|
// matrix is never built at layout time so getKerning() returns 0 regardless.
|
||||||
bool hasLig = s.header.ligaturePairCount > 0;
|
// Skipping them saves ~4KB per style (~17KB total for 4 styles), preventing OOM
|
||||||
if (!hasKern && !hasLig) {
|
// on low-heap devices when long paragraphs try to grow their word vector.
|
||||||
s.kernLigLoaded = true;
|
const bool wantKern = !ligatureOnly && s.header.kernLeftEntryCount > 0;
|
||||||
return true;
|
const bool wantLig = s.header.ligaturePairCount > 0;
|
||||||
}
|
|
||||||
|
const bool kernDone = !wantKern || s.kernClassesLoaded;
|
||||||
|
const bool ligDone = !wantLig || s.ligLoaded;
|
||||||
|
if (kernDone && ligDone) return true;
|
||||||
|
|
||||||
FsFile file;
|
FsFile file;
|
||||||
if (!Storage.openFileForRead("SDCF", filePath_, file)) {
|
if (!Storage.openFileForRead("SDCF", filePath_, file)) {
|
||||||
@@ -149,72 +173,79 @@ bool SdCardFont::loadStyleKernLigatureData(PerStyle& s) {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (hasKern) {
|
if (wantKern && !s.kernClassesLoaded) {
|
||||||
// Load only the small class-lookup tables (~3KB each). The full matrix
|
// Load only the small class-lookup tables (~3KB each). The full matrix
|
||||||
// (~36KB contiguous for Literata) is built per-page from SD in
|
// (~36KB contiguous for Literata) is built per-page from SD in
|
||||||
// buildMiniKernMatrix().
|
// buildMiniKernMatrix().
|
||||||
s.kernLeftClasses = new (std::nothrow) EpdKernClassEntry[s.header.kernLeftEntryCount];
|
EpdKernClassEntry* newLeft = new (std::nothrow) EpdKernClassEntry[s.header.kernLeftEntryCount];
|
||||||
s.kernRightClasses = new (std::nothrow) EpdKernClassEntry[s.header.kernRightEntryCount];
|
EpdKernClassEntry* newRight = new (std::nothrow) EpdKernClassEntry[s.header.kernRightEntryCount];
|
||||||
|
|
||||||
if (!s.kernLeftClasses || !s.kernRightClasses) {
|
if (!newLeft || !newRight) {
|
||||||
|
delete[] newLeft;
|
||||||
|
delete[] newRight;
|
||||||
LOG_ERR("SDCF", "Failed to allocate kern classes (%u+%u bytes)", s.header.kernLeftEntryCount * 3u,
|
LOG_ERR("SDCF", "Failed to allocate kern classes (%u+%u bytes)", s.header.kernLeftEntryCount * 3u,
|
||||||
s.header.kernRightEntryCount * 3u);
|
s.header.kernRightEntryCount * 3u);
|
||||||
freeStyleKernLigatureData(s);
|
|
||||||
file.close();
|
file.close();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!file.seekSet(s.kernLeftFileOffset)) {
|
if (!file.seekSet(s.kernLeftFileOffset)) {
|
||||||
|
delete[] newLeft;
|
||||||
|
delete[] newRight;
|
||||||
LOG_ERR("SDCF", "Failed to seek to kern data");
|
LOG_ERR("SDCF", "Failed to seek to kern data");
|
||||||
freeStyleKernLigatureData(s);
|
|
||||||
file.close();
|
file.close();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
size_t leftSz = s.header.kernLeftEntryCount * sizeof(EpdKernClassEntry);
|
size_t leftSz = s.header.kernLeftEntryCount * sizeof(EpdKernClassEntry);
|
||||||
size_t rightSz = s.header.kernRightEntryCount * sizeof(EpdKernClassEntry);
|
size_t rightSz = s.header.kernRightEntryCount * sizeof(EpdKernClassEntry);
|
||||||
if (file.read(reinterpret_cast<uint8_t*>(s.kernLeftClasses), leftSz) != static_cast<int>(leftSz) ||
|
if (file.read(reinterpret_cast<uint8_t*>(newLeft), leftSz) != static_cast<int>(leftSz) ||
|
||||||
file.read(reinterpret_cast<uint8_t*>(s.kernRightClasses), rightSz) != static_cast<int>(rightSz)) {
|
file.read(reinterpret_cast<uint8_t*>(newRight), rightSz) != static_cast<int>(rightSz)) {
|
||||||
|
delete[] newLeft;
|
||||||
|
delete[] newRight;
|
||||||
LOG_ERR("SDCF", "Failed to read kern classes");
|
LOG_ERR("SDCF", "Failed to read kern classes");
|
||||||
freeStyleKernLigatureData(s);
|
|
||||||
file.close();
|
file.close();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
s.kernLeftClasses = newLeft;
|
||||||
|
s.kernRightClasses = newRight;
|
||||||
|
s.kernClassesLoaded = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (hasLig) {
|
if (wantLig && !s.ligLoaded) {
|
||||||
s.ligaturePairs = new (std::nothrow) EpdLigaturePair[s.header.ligaturePairCount];
|
EpdLigaturePair* newLig = new (std::nothrow) EpdLigaturePair[s.header.ligaturePairCount];
|
||||||
if (!s.ligaturePairs) {
|
if (!newLig) {
|
||||||
LOG_ERR("SDCF", "Failed to allocate ligature pairs");
|
LOG_ERR("SDCF", "Failed to allocate ligature pairs");
|
||||||
freeStyleKernLigatureData(s);
|
|
||||||
file.close();
|
file.close();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (!file.seekSet(s.ligatureFileOffset)) {
|
if (!file.seekSet(s.ligatureFileOffset)) {
|
||||||
|
delete[] newLig;
|
||||||
LOG_ERR("SDCF", "Failed to seek to ligature data");
|
LOG_ERR("SDCF", "Failed to seek to ligature data");
|
||||||
freeStyleKernLigatureData(s);
|
|
||||||
file.close();
|
file.close();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
size_t sz = s.header.ligaturePairCount * sizeof(EpdLigaturePair);
|
size_t sz = s.header.ligaturePairCount * sizeof(EpdLigaturePair);
|
||||||
if (file.read(reinterpret_cast<uint8_t*>(s.ligaturePairs), sz) != static_cast<int>(sz)) {
|
if (file.read(reinterpret_cast<uint8_t*>(newLig), sz) != static_cast<int>(sz)) {
|
||||||
|
delete[] newLig;
|
||||||
LOG_ERR("SDCF", "Failed to read ligature pairs");
|
LOG_ERR("SDCF", "Failed to read ligature pairs");
|
||||||
freeStyleKernLigatureData(s);
|
|
||||||
file.close();
|
file.close();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
s.ligaturePairs = newLig;
|
||||||
|
s.ligLoaded = true;
|
||||||
|
|
||||||
|
// Make ligatures visible to the stub (used when no mini data built yet).
|
||||||
|
// Kern stays nullptr on the stub — it is only wired in miniData via
|
||||||
|
// applyKernLigaturePointers() after buildMiniKernMatrix() runs.
|
||||||
|
s.stubData.ligaturePairs = s.ligaturePairs;
|
||||||
|
s.stubData.ligaturePairCount = s.header.ligaturePairCount;
|
||||||
}
|
}
|
||||||
|
|
||||||
file.close();
|
file.close();
|
||||||
s.kernLigLoaded = true;
|
LOG_DBG("SDCF", "Kern/lig loaded: kernL=%u kernR=%u ligs=%u ligOnly=%d",
|
||||||
|
s.kernClassesLoaded ? s.header.kernLeftEntryCount : 0u,
|
||||||
// Make ligatures visible to the stub (used when no mini data built yet).
|
s.kernClassesLoaded ? s.header.kernRightEntryCount : 0u, s.ligLoaded ? s.header.ligaturePairCount : 0u,
|
||||||
// Kern stays nullptr on the stub — it is only wired in miniData via
|
ligatureOnly);
|
||||||
// applyKernLigaturePointers() after buildMiniKernMatrix() runs.
|
|
||||||
s.stubData.ligaturePairs = s.ligaturePairs;
|
|
||||||
s.stubData.ligaturePairCount = s.header.ligaturePairCount;
|
|
||||||
|
|
||||||
LOG_DBG("SDCF", "Kern classes + lig loaded: kernL=%u, kernR=%u, ligs=%u", s.header.kernLeftEntryCount,
|
|
||||||
s.header.kernRightEntryCount, s.header.ligaturePairCount);
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -653,7 +684,7 @@ int SdCardFont::prewarm(const char* utf8Text, uint8_t styleMask, bool metadataOn
|
|||||||
if (!(styleMask & (1 << si)) || !styles_[si].present) continue;
|
if (!(styleMask & (1 << si)) || !styles_[si].present) continue;
|
||||||
auto& s = styles_[si];
|
auto& s = styles_[si];
|
||||||
|
|
||||||
loadStyleKernLigatureData(s);
|
loadStyleKernLigatureData(s, /*ligatureOnly=*/true);
|
||||||
if (s.ligaturePairs && s.header.ligaturePairCount > 0) {
|
if (s.ligaturePairs && s.header.ligaturePairCount > 0) {
|
||||||
for (uint8_t li = 0; li < s.header.ligaturePairCount && cpCount < MAX_PAGE_GLYPHS; li++) {
|
for (uint8_t li = 0; li < s.header.ligaturePairCount && cpCount < MAX_PAGE_GLYPHS; li++) {
|
||||||
uint32_t leftCp = s.ligaturePairs[li].pair >> 16;
|
uint32_t leftCp = s.ligaturePairs[li].pair >> 16;
|
||||||
@@ -736,6 +767,18 @@ int SdCardFont::prewarmStyle(uint8_t styleIdx, const uint32_t* codepoints, uint3
|
|||||||
// For metadata-only calls, METADATA or FULL cache both satisfy layout queries.
|
// For metadata-only calls, METADATA or FULL cache both satisfy layout queries.
|
||||||
// For full (bitmap) calls, only FULL satisfies — METADATA lacks bitmap data.
|
// For full (bitmap) calls, only FULL satisfies — METADATA lacks bitmap data.
|
||||||
if (metadataOnly || s.miniMode == PerStyle::MiniMode::FULL) {
|
if (metadataOnly || s.miniMode == PerStyle::MiniMode::FULL) {
|
||||||
|
// Ensure ligature metadata is wired if requested but not yet wired.
|
||||||
|
if (loadKernLigatureData && !s.ligLoaded) {
|
||||||
|
loadStyleKernLigatureData(s, /*ligatureOnly=*/true);
|
||||||
|
}
|
||||||
|
if (loadKernLigatureData && s.ligLoaded && s.miniData.ligaturePairs == nullptr) {
|
||||||
|
if (s.miniMode == PerStyle::MiniMode::FULL) {
|
||||||
|
applyKernLigaturePointers(s, s.miniData);
|
||||||
|
} else {
|
||||||
|
s.miniData.ligaturePairs = s.ligaturePairs;
|
||||||
|
s.miniData.ligaturePairCount = s.header.ligaturePairCount;
|
||||||
|
}
|
||||||
|
}
|
||||||
// Already wired into miniData; nothing else to do.
|
// Already wired into miniData; nothing else to do.
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
@@ -1143,7 +1186,7 @@ int SdCardFont::prewarmStyle(uint8_t styleIdx, const uint32_t* codepoints, uint3
|
|||||||
kernLigOk = buildMiniKernMatrix(s, codepoints, cpCount);
|
kernLigOk = buildMiniKernMatrix(s, codepoints, cpCount);
|
||||||
}
|
}
|
||||||
} else if (loadKernLigatureData) {
|
} else if (loadKernLigatureData) {
|
||||||
loadStyleKernLigatureData(s);
|
loadStyleKernLigatureData(s, /*ligatureOnly=*/true);
|
||||||
// Don't set kernLigOk → mini kern matrix stays null on miniData, but
|
// Don't set kernLigOk → mini kern matrix stays null on miniData, but
|
||||||
// ligatures are still resident on stubData (set in loadStyleKernLigatureData).
|
// ligatures are still resident on stubData (set in loadStyleKernLigatureData).
|
||||||
}
|
}
|
||||||
@@ -1161,7 +1204,7 @@ int SdCardFont::prewarmStyle(uint8_t styleIdx, const uint32_t* codepoints, uint3
|
|||||||
if (kernLigOk) {
|
if (kernLigOk) {
|
||||||
// Full prewarm: wire mini kern matrix + class tables + ligatures.
|
// Full prewarm: wire mini kern matrix + class tables + ligatures.
|
||||||
applyKernLigaturePointers(s, s.miniData);
|
applyKernLigaturePointers(s, s.miniData);
|
||||||
} else if (loadKernLigatureData && s.kernLigLoaded) {
|
} else if (loadKernLigatureData && s.ligLoaded) {
|
||||||
// Layout-only prewarm: wire ligatures so applyLigatures() works (e.g. "fi"
|
// Layout-only prewarm: wire ligatures so applyLigatures() works (e.g. "fi"
|
||||||
// measures correctly). Skip the kern matrix — getKerning() returns 0
|
// measures correctly). Skip the kern matrix — getKerning() returns 0
|
||||||
// cleanly when kernMatrix is null. Per-pair kern is applied at render time.
|
// cleanly when kernMatrix is null. Per-pair kern is applied at render time.
|
||||||
@@ -1173,6 +1216,8 @@ int SdCardFont::prewarmStyle(uint8_t styleIdx, const uint32_t* codepoints, uint3
|
|||||||
|
|
||||||
s.epdFont.data = &s.miniData;
|
s.epdFont.data = &s.miniData;
|
||||||
s.miniMode = metadataOnly ? PerStyle::MiniMode::METADATA : PerStyle::MiniMode::FULL;
|
s.miniMode = metadataOnly ? PerStyle::MiniMode::METADATA : PerStyle::MiniMode::FULL;
|
||||||
|
LOG_DBG("SDCF", "prewarmStyle %u: mode→%s glyphs=%u bitmap=%p", styleIdx, metadataOnly ? "METADATA" : "FULL",
|
||||||
|
validCount, s.miniBitmap);
|
||||||
|
|
||||||
// Accumulate stats
|
// Accumulate stats
|
||||||
stats_.sdReadTimeMs += sdTime;
|
stats_.sdReadTimeMs += sdTime;
|
||||||
@@ -1236,6 +1281,10 @@ const EpdGlyph* SdCardFont::onGlyphMiss(void* ctx, uint32_t codepoint) {
|
|||||||
const auto& s = self->styles_[styleIdx];
|
const auto& s = self->styles_[styleIdx];
|
||||||
if (!s.fullIntervals) return nullptr;
|
if (!s.fullIntervals) return nullptr;
|
||||||
|
|
||||||
|
// Diagnostic: log first miss per codepoint+style to show why it bypassed prewarm
|
||||||
|
LOG_DBG("SDCF", "onGlyphMiss: U+%04X style %u miniMode=%u miniIntervals=%u bitmap=%p", codepoint, styleIdx,
|
||||||
|
(uint8_t)s.miniMode, s.miniIntervalCount, s.miniBitmap);
|
||||||
|
|
||||||
// Check overflow cache first (matching both codepoint and style)
|
// Check overflow cache first (matching both codepoint and style)
|
||||||
for (uint32_t i = 0; i < self->overflowCount_; i++) {
|
for (uint32_t i = 0; i < self->overflowCount_; i++) {
|
||||||
if (self->overflow_[i].codepoint == codepoint && self->overflow_[i].styleIdx == styleIdx) {
|
if (self->overflow_[i].codepoint == codepoint && self->overflow_[i].styleIdx == styleIdx) {
|
||||||
@@ -1309,6 +1358,8 @@ const EpdGlyph* SdCardFont::onGlyphMiss(void* ctx, uint32_t codepoint) {
|
|||||||
|
|
||||||
// All reads succeeded — commit to slot (evict old entry if at capacity)
|
// All reads succeeded — commit to slot (evict old entry if at capacity)
|
||||||
if (wasAtCapacity) {
|
if (wasAtCapacity) {
|
||||||
|
LOG_DBG("SDCF", "Overflow: evicting U+%04X style %u from slot %u", self->overflow_[slot].codepoint,
|
||||||
|
self->overflow_[slot].styleIdx, slot);
|
||||||
delete[] self->overflow_[slot].bitmap;
|
delete[] self->overflow_[slot].bitmap;
|
||||||
}
|
}
|
||||||
self->overflow_[slot].glyph = tempGlyph;
|
self->overflow_[slot].glyph = tempGlyph;
|
||||||
|
|||||||
@@ -118,7 +118,8 @@ class SdCardFont {
|
|||||||
EpdKernClassEntry* kernLeftClasses = nullptr;
|
EpdKernClassEntry* kernLeftClasses = nullptr;
|
||||||
EpdKernClassEntry* kernRightClasses = nullptr;
|
EpdKernClassEntry* kernRightClasses = nullptr;
|
||||||
EpdLigaturePair* ligaturePairs = nullptr;
|
EpdLigaturePair* ligaturePairs = nullptr;
|
||||||
bool kernLigLoaded = false;
|
bool ligLoaded = false; ///< ligaturePairs resident
|
||||||
|
bool kernClassesLoaded = false; ///< kernLeft/RightClasses resident (skipped during metadata-only prewarm)
|
||||||
|
|
||||||
// Stub EpdFontData returned when not prewarmed
|
// Stub EpdFontData returned when not prewarmed
|
||||||
EpdFontData stubData{};
|
EpdFontData stubData{};
|
||||||
@@ -204,7 +205,7 @@ class SdCardFont {
|
|||||||
void freeStyleAll(PerStyle& s);
|
void freeStyleAll(PerStyle& s);
|
||||||
void freeStyleKernLigatureData(PerStyle& s);
|
void freeStyleKernLigatureData(PerStyle& s);
|
||||||
void freeStyleMiniKern(PerStyle& s);
|
void freeStyleMiniKern(PerStyle& s);
|
||||||
bool loadStyleKernLigatureData(PerStyle& s);
|
bool loadStyleKernLigatureData(PerStyle& s, bool ligatureOnly = false);
|
||||||
bool buildMiniKernMatrix(PerStyle& s, const uint32_t* codepoints, uint32_t cpCount);
|
bool buildMiniKernMatrix(PerStyle& s, const uint32_t* codepoints, uint32_t cpCount);
|
||||||
void applyKernLigaturePointers(const PerStyle& s, EpdFontData& data) const;
|
void applyKernLigaturePointers(const PerStyle& s, EpdFontData& data) const;
|
||||||
void applyGlyphMissCallback(uint8_t styleIdx);
|
void applyGlyphMissCallback(uint8_t styleIdx);
|
||||||
|
|||||||
@@ -215,7 +215,14 @@ void ParsedText::layoutAndExtractLines(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Apply fixed transforms before any per-line layout work.
|
// Apply fixed transforms before any per-line layout work.
|
||||||
applyParagraphIndent();
|
// Paragraph indent only applies to the first layout pass; skip on continuations.
|
||||||
|
if (!isContinuation_) {
|
||||||
|
applyParagraphIndent();
|
||||||
|
}
|
||||||
|
// Bionic transform is incremental: applyBionicReadingTransform() is a no-op
|
||||||
|
// for already-transformed words (bionicTransformedUpTo_ == words.size()) and
|
||||||
|
// only processes raw words appended since the last flush, so it is always safe
|
||||||
|
// to call regardless of isContinuation_.
|
||||||
if (bionicReadingEnabled) {
|
if (bionicReadingEnabled) {
|
||||||
applyBionicReadingTransform();
|
applyBionicReadingTransform();
|
||||||
}
|
}
|
||||||
@@ -242,6 +249,15 @@ void ParsedText::layoutAndExtractLines(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const int pageWidth = viewportWidth;
|
const int pageWidth = viewportWidth;
|
||||||
|
|
||||||
|
// Compute firstLineIndent once here so all layout helpers use the same value.
|
||||||
|
// On a continuation flush the remaining words are mid-paragraph, so no indent.
|
||||||
|
const int firstLineIndent =
|
||||||
|
!isContinuation_ && blockStyle.textIndentDefined &&
|
||||||
|
(blockStyle.alignment == CssTextAlign::Justify || blockStyle.alignment == CssTextAlign::Left)
|
||||||
|
? std::min(std::max<int>(static_cast<int>(blockStyle.textIndent), -(pageWidth - 1)), pageWidth - 1)
|
||||||
|
: 0;
|
||||||
|
|
||||||
auto wordWidths = calculateWordWidths(renderer, fontId);
|
auto wordWidths = calculateWordWidths(renderer, fontId);
|
||||||
|
|
||||||
std::vector<size_t> lineBreakIndices;
|
std::vector<size_t> lineBreakIndices;
|
||||||
@@ -252,9 +268,9 @@ void ParsedText::layoutAndExtractLines(
|
|||||||
// Use greedy layout that can split words mid-loop when a hyphenated prefix fits.
|
// Use greedy layout that can split words mid-loop when a hyphenated prefix fits.
|
||||||
lineBreakIndices =
|
lineBreakIndices =
|
||||||
computeHyphenatedLineBreaks(renderer, fontId, pageWidth, wordWidths, wordContinues, lineEndsWithHyphenatedWord,
|
computeHyphenatedLineBreaks(renderer, fontId, pageWidth, wordWidths, wordContinues, lineEndsWithHyphenatedWord,
|
||||||
splitPrefixWordIndexes, splitInsertedHyphen);
|
splitPrefixWordIndexes, splitInsertedHyphen, firstLineIndent);
|
||||||
} else {
|
} else {
|
||||||
lineBreakIndices = computeLineBreaks(renderer, fontId, pageWidth, wordWidths, wordContinues);
|
lineBreakIndices = computeLineBreaks(renderer, fontId, pageWidth, wordWidths, wordContinues, firstLineIndent);
|
||||||
lineEndsWithHyphenatedWord.assign(lineBreakIndices.size(), false);
|
lineEndsWithHyphenatedWord.assign(lineBreakIndices.size(), false);
|
||||||
splitPrefixWordIndexes.assign(lineBreakIndices.size(), -1);
|
splitPrefixWordIndexes.assign(lineBreakIndices.size(), -1);
|
||||||
splitInsertedHyphen.assign(lineBreakIndices.size(), false);
|
splitInsertedHyphen.assign(lineBreakIndices.size(), false);
|
||||||
@@ -264,7 +280,7 @@ void ParsedText::layoutAndExtractLines(
|
|||||||
for (size_t i = 0; i < lineCount; ++i) {
|
for (size_t i = 0; i < lineCount; ++i) {
|
||||||
const bool lineEndedWithHyphenation = i < lineEndsWithHyphenatedWord.size() ? lineEndsWithHyphenatedWord[i] : false;
|
const bool lineEndedWithHyphenation = i < lineEndsWithHyphenatedWord.size() ? lineEndsWithHyphenatedWord[i] : false;
|
||||||
const auto result = extractLine(i, pageWidth, wordWidths, wordContinues, lineBreakIndices, processLine, renderer,
|
const auto result = extractLine(i, pageWidth, wordWidths, wordContinues, lineBreakIndices, processLine, renderer,
|
||||||
fontId, lineEndedWithHyphenation, false);
|
fontId, lineEndedWithHyphenation, false, firstLineIndent);
|
||||||
|
|
||||||
if (result == LineProcessResult::RetryWithoutHyphenation && lineEndedWithHyphenation) {
|
if (result == LineProcessResult::RetryWithoutHyphenation && lineEndedWithHyphenation) {
|
||||||
const size_t lineStart = i > 0 ? lineBreakIndices[i - 1] : 0;
|
const size_t lineStart = i > 0 ? lineBreakIndices[i - 1] : 0;
|
||||||
@@ -309,8 +325,8 @@ void ParsedText::layoutAndExtractLines(
|
|||||||
|
|
||||||
// Keep previous lines fixed; recompute only this specific line without hyphenation.
|
// Keep previous lines fixed; recompute only this specific line without hyphenation.
|
||||||
// Suppression is intentionally line-local.
|
// Suppression is intentionally line-local.
|
||||||
const size_t retryBreak =
|
const size_t retryBreak = computeSingleLineBreakNoHyphen(renderer, fontId, pageWidth, wordWidths, wordContinues,
|
||||||
computeSingleLineBreakNoHyphen(renderer, fontId, pageWidth, wordWidths, wordContinues, lineStart);
|
lineStart, firstLineIndent);
|
||||||
|
|
||||||
lineBreakIndices.resize(i + 1);
|
lineBreakIndices.resize(i + 1);
|
||||||
lineEndsWithHyphenatedWord.resize(i + 1);
|
lineEndsWithHyphenatedWord.resize(i + 1);
|
||||||
@@ -330,7 +346,7 @@ void ParsedText::layoutAndExtractLines(
|
|||||||
LOG_DBG("PTX", "Rerendering line %u with hyphenation suppressed, retry attempt: %s", static_cast<unsigned>(i),
|
LOG_DBG("PTX", "Rerendering line %u with hyphenation suppressed, retry attempt: %s", static_cast<unsigned>(i),
|
||||||
retryPreview.c_str());
|
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, firstLineIndent);
|
||||||
|
|
||||||
// Resume regular hyphenation from the first word after the retried line.
|
// Resume regular hyphenation from the first word after the retried line.
|
||||||
const size_t resumeIndex = lineBreakIndices[i];
|
const size_t resumeIndex = lineBreakIndices[i];
|
||||||
@@ -355,13 +371,23 @@ void ParsedText::layoutAndExtractLines(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove consumed words so size() reflects only remaining words
|
// Remove consumed words so size() reflects only remaining words, then
|
||||||
|
// release excess capacity. Without shrink_to_fit the vector retains a
|
||||||
|
// large allocation from before the flush; the next paragraph fills it
|
||||||
|
// back up and eventually needs an even larger contiguous realloc.
|
||||||
if (lineCount > 0) {
|
if (lineCount > 0) {
|
||||||
const size_t consumed = lineBreakIndices[lineCount - 1];
|
const size_t consumed = lineBreakIndices[lineCount - 1];
|
||||||
words.erase(words.begin(), words.begin() + consumed);
|
words.erase(words.begin(), words.begin() + consumed);
|
||||||
wordStyles.erase(wordStyles.begin(), wordStyles.begin() + consumed);
|
wordStyles.erase(wordStyles.begin(), wordStyles.begin() + consumed);
|
||||||
wordContinues.erase(wordContinues.begin(), wordContinues.begin() + consumed);
|
wordContinues.erase(wordContinues.begin(), wordContinues.begin() + consumed);
|
||||||
|
words.shrink_to_fit();
|
||||||
|
wordStyles.shrink_to_fit();
|
||||||
|
wordContinues.shrink_to_fit();
|
||||||
|
// All remaining words were already transformed before the flush; reset the
|
||||||
|
// watermark so that words appended by addWord() are processed next time.
|
||||||
|
bionicTransformedUpTo_ = words.size();
|
||||||
}
|
}
|
||||||
|
isContinuation_ = !includeLastLine;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::vector<uint16_t> ParsedText::calculateWordWidths(const GfxRenderer& renderer, const int fontId) {
|
std::vector<uint16_t> ParsedText::calculateWordWidths(const GfxRenderer& renderer, const int fontId) {
|
||||||
@@ -376,20 +402,12 @@ std::vector<uint16_t> ParsedText::calculateWordWidths(const GfxRenderer& rendere
|
|||||||
}
|
}
|
||||||
|
|
||||||
std::vector<size_t> ParsedText::computeLineBreaks(const GfxRenderer& renderer, const int fontId, const int pageWidth,
|
std::vector<size_t> ParsedText::computeLineBreaks(const GfxRenderer& renderer, const int fontId, const int pageWidth,
|
||||||
std::vector<uint16_t>& wordWidths, std::vector<bool>& continuesVec) {
|
std::vector<uint16_t>& wordWidths, std::vector<bool>& continuesVec,
|
||||||
|
const int firstLineIndent) {
|
||||||
if (words.empty()) {
|
if (words.empty()) {
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Calculate first line indent (only for left/justified text).
|
|
||||||
// Explicit CSS text-indent always applies — author intent overrides the extraParagraphSpacing
|
|
||||||
// toggle. Only the implicit EmSpace fallback in applyParagraphIndent() is gated on it.
|
|
||||||
const int firstLineIndent =
|
|
||||||
blockStyle.textIndentDefined &&
|
|
||||||
(blockStyle.alignment == CssTextAlign::Justify || blockStyle.alignment == CssTextAlign::Left)
|
|
||||||
? std::min(std::max<int>(static_cast<int>(blockStyle.textIndent), -(pageWidth - 1)), pageWidth - 1)
|
|
||||||
: 0;
|
|
||||||
|
|
||||||
// Ensure any word that would overflow even as the first entry on a line is split using fallback hyphenation.
|
// Ensure any word that would overflow even as the first entry on a line is split using fallback hyphenation.
|
||||||
for (size_t i = 0; i < wordWidths.size(); ++i) {
|
for (size_t i = 0; i < wordWidths.size(); ++i) {
|
||||||
// First word needs to fit in reduced width if there's an indent
|
// First word needs to fit in reduced width if there's an indent
|
||||||
@@ -510,19 +528,14 @@ std::vector<size_t> ParsedText::computeLineBreaks(const GfxRenderer& renderer, c
|
|||||||
|
|
||||||
size_t ParsedText::computeSingleLineBreakNoHyphen(const GfxRenderer& renderer, const int fontId, const int pageWidth,
|
size_t ParsedText::computeSingleLineBreakNoHyphen(const GfxRenderer& renderer, const int fontId, const int pageWidth,
|
||||||
const std::vector<uint16_t>& wordWidths,
|
const std::vector<uint16_t>& wordWidths,
|
||||||
const std::vector<bool>& continuesVec,
|
const std::vector<bool>& continuesVec, const size_t lineStartIndex,
|
||||||
const size_t lineStartIndex) const {
|
const int firstLineIndent) const {
|
||||||
// One-line non-hyphenating breaker used by the page-boundary retry path.
|
// One-line non-hyphenating breaker used by the page-boundary retry path.
|
||||||
if (lineStartIndex >= wordWidths.size()) {
|
if (lineStartIndex >= wordWidths.size()) {
|
||||||
return lineStartIndex;
|
return lineStartIndex;
|
||||||
}
|
}
|
||||||
|
|
||||||
const int firstLineIndent =
|
const int effectivePageWidth = pageWidth - (lineStartIndex == 0 ? firstLineIndent : 0);
|
||||||
lineStartIndex == 0 && blockStyle.textIndentDefined &&
|
|
||||||
(blockStyle.alignment == CssTextAlign::Justify || blockStyle.alignment == CssTextAlign::Left)
|
|
||||||
? std::min(std::max<int>(static_cast<int>(blockStyle.textIndent), -(pageWidth - 1)), pageWidth - 1)
|
|
||||||
: 0;
|
|
||||||
const int effectivePageWidth = pageWidth - firstLineIndent;
|
|
||||||
|
|
||||||
size_t currentIndex = lineStartIndex;
|
size_t currentIndex = lineStartIndex;
|
||||||
int lineWidth = 0;
|
int lineWidth = 0;
|
||||||
@@ -577,22 +590,26 @@ void ParsedText::applyParagraphIndent() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void ParsedText::applyBionicReadingTransform() {
|
void ParsedText::applyBionicReadingTransform() {
|
||||||
if (words.empty()) {
|
// Only transform words that haven't been processed yet. On a fresh block
|
||||||
|
// bionicTransformedUpTo_ == 0 so all words are processed. After an
|
||||||
|
// intermediate flush, only the new raw words appended since the last flush
|
||||||
|
// (indices bionicTransformedUpTo_..words.size()-1) need transformation.
|
||||||
|
if (words.empty() || bionicTransformedUpTo_ >= words.size()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::vector<std::string> transformedWords;
|
const size_t suffixStart = bionicTransformedUpTo_;
|
||||||
std::vector<EpdFontFamily::Style> transformedStyles;
|
std::vector<std::string> transformedSuffix;
|
||||||
std::vector<bool> transformedContinues;
|
std::vector<EpdFontFamily::Style> transformedSuffixStyles;
|
||||||
transformedWords.reserve(words.size() * 2);
|
std::vector<bool> transformedSuffixContinues;
|
||||||
transformedStyles.reserve(wordStyles.size() * 2);
|
transformedSuffix.reserve((words.size() - suffixStart) * 2);
|
||||||
transformedContinues.reserve(wordContinues.size() * 2);
|
transformedSuffixStyles.reserve(transformedSuffix.capacity());
|
||||||
|
transformedSuffixContinues.reserve(transformedSuffix.capacity());
|
||||||
|
|
||||||
for (size_t i = 0; i < words.size(); ++i) {
|
for (size_t i = suffixStart; i < words.size(); ++i) {
|
||||||
std::string source = std::move(words[i]);
|
std::string source = std::move(words[i]);
|
||||||
const auto originalStyle = wordStyles[i];
|
const auto originalStyle = wordStyles[i];
|
||||||
const bool originalAttachToPrevious = wordContinues[i];
|
const bool originalAttachToPrevious = wordContinues[i];
|
||||||
const char* raw = source.c_str();
|
|
||||||
|
|
||||||
const auto spans = tokenizeBionicWord(source);
|
const auto spans = tokenizeBionicWord(source);
|
||||||
if (spans.empty()) {
|
if (spans.empty()) {
|
||||||
@@ -603,12 +620,7 @@ void ParsedText::applyBionicReadingTransform() {
|
|||||||
for (size_t spanIndex = 0; spanIndex < spans.size(); ++spanIndex) {
|
for (size_t spanIndex = 0; spanIndex < spans.size(); ++spanIndex) {
|
||||||
const TokenSpan span = spans[spanIndex];
|
const TokenSpan span = spans[spanIndex];
|
||||||
const size_t spanLength = span.end - span.start;
|
const size_t spanLength = span.end - span.start;
|
||||||
std::string token;
|
std::string token = source.substr(span.start, spanLength);
|
||||||
if (spans.size() == 1 && spanIndex == 0) {
|
|
||||||
token = std::move(source);
|
|
||||||
} else {
|
|
||||||
token.assign(raw + span.start, spanLength);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (span.isWord) {
|
if (span.isWord) {
|
||||||
const unsigned char* ptr = reinterpret_cast<const unsigned char*>(token.c_str());
|
const unsigned char* ptr = reinterpret_cast<const unsigned char*>(token.c_str());
|
||||||
@@ -630,47 +642,42 @@ void ParsedText::applyBionicReadingTransform() {
|
|||||||
std::string suffix(reinterpret_cast<const char*>(prefixEnd), token.size() - prefixByteCount);
|
std::string suffix(reinterpret_cast<const char*>(prefixEnd), token.size() - prefixByteCount);
|
||||||
token.resize(prefixByteCount);
|
token.resize(prefixByteCount);
|
||||||
const auto boldStyle = static_cast<EpdFontFamily::Style>(originalStyle | EpdFontFamily::BOLD);
|
const auto boldStyle = static_cast<EpdFontFamily::Style>(originalStyle | EpdFontFamily::BOLD);
|
||||||
transformedWords.push_back(std::move(token));
|
transformedSuffix.push_back(std::move(token));
|
||||||
transformedStyles.push_back(boldStyle);
|
transformedSuffixStyles.push_back(boldStyle);
|
||||||
transformedContinues.push_back(attachToPrevious);
|
transformedSuffixContinues.push_back(attachToPrevious);
|
||||||
|
|
||||||
transformedWords.push_back(std::move(suffix));
|
transformedSuffix.push_back(std::move(suffix));
|
||||||
transformedStyles.push_back(originalStyle);
|
transformedSuffixStyles.push_back(originalStyle);
|
||||||
transformedContinues.push_back(true);
|
transformedSuffixContinues.push_back(true);
|
||||||
attachToPrevious = true;
|
attachToPrevious = true;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
transformedWords.push_back(std::move(token));
|
transformedSuffix.push_back(std::move(token));
|
||||||
transformedStyles.push_back(originalStyle);
|
transformedSuffixStyles.push_back(originalStyle);
|
||||||
transformedContinues.push_back(attachToPrevious);
|
transformedSuffixContinues.push_back(attachToPrevious);
|
||||||
attachToPrevious = true;
|
attachToPrevious = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
words = std::move(transformedWords);
|
// Replace the (now move-emptied) suffix with the transformed version.
|
||||||
wordStyles = std::move(transformedStyles);
|
words.resize(suffixStart);
|
||||||
wordContinues = std::move(transformedContinues);
|
wordStyles.resize(suffixStart);
|
||||||
|
wordContinues.resize(suffixStart);
|
||||||
|
words.insert(words.end(), std::make_move_iterator(transformedSuffix.begin()),
|
||||||
|
std::make_move_iterator(transformedSuffix.end()));
|
||||||
|
wordStyles.insert(wordStyles.end(), transformedSuffixStyles.begin(), transformedSuffixStyles.end());
|
||||||
|
wordContinues.insert(wordContinues.end(), transformedSuffixContinues.begin(), transformedSuffixContinues.end());
|
||||||
|
bionicTransformedUpTo_ = words.size();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Builds break indices while opportunistically splitting the word that would overflow the current line.
|
// Builds break indices while opportunistically splitting the word that would overflow the current line.
|
||||||
std::vector<size_t> ParsedText::computeHyphenatedLineBreaks(const GfxRenderer& renderer, const int fontId,
|
std::vector<size_t> ParsedText::computeHyphenatedLineBreaks(
|
||||||
const int pageWidth, std::vector<uint16_t>& wordWidths,
|
const GfxRenderer& renderer, const int fontId, const int pageWidth, std::vector<uint16_t>& wordWidths,
|
||||||
std::vector<bool>& continuesVec,
|
std::vector<bool>& continuesVec, std::vector<bool>& lineEndsWithHyphenatedWord,
|
||||||
std::vector<bool>& lineEndsWithHyphenatedWord,
|
std::vector<int>& splitPrefixWordIndexes, std::vector<bool>& splitInsertedHyphen, const int firstLineIndent) {
|
||||||
std::vector<int>& splitPrefixWordIndexes,
|
|
||||||
std::vector<bool>& splitInsertedHyphen) {
|
|
||||||
// Calculate first line indent (only for left/justified text).
|
|
||||||
// Explicit CSS text-indent always applies — author intent overrides the extraParagraphSpacing
|
|
||||||
// toggle. Only the implicit EmSpace fallback in applyParagraphIndent() is gated on it.
|
|
||||||
const int firstLineIndent =
|
|
||||||
blockStyle.textIndentDefined &&
|
|
||||||
(blockStyle.alignment == CssTextAlign::Justify || blockStyle.alignment == CssTextAlign::Left)
|
|
||||||
? std::min(std::max<int>(static_cast<int>(blockStyle.textIndent), -(pageWidth - 1)), pageWidth - 1)
|
|
||||||
: 0;
|
|
||||||
|
|
||||||
// Pre-compute inter-word gaps to avoid repeated codepoint scanning and renderer
|
// 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
|
// 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
|
// a placeholder gap (0) at that position to keep the vector in sync; the remainder
|
||||||
@@ -949,20 +956,14 @@ ParsedText::LineProcessResult ParsedText::extractLine(
|
|||||||
const std::vector<bool>& continuesVec, const std::vector<size_t>& lineBreakIndices,
|
const std::vector<bool>& continuesVec, const std::vector<size_t>& lineBreakIndices,
|
||||||
const std::function<LineProcessResult(std::shared_ptr<TextBlock>, bool, bool)>& processLine,
|
const std::function<LineProcessResult(std::shared_ptr<TextBlock>, bool, bool)>& processLine,
|
||||||
const GfxRenderer& renderer, const int fontId, const bool lineEndsWithHyphenatedWord,
|
const GfxRenderer& renderer, const int fontId, const bool lineEndsWithHyphenatedWord,
|
||||||
const bool suppressHyphenationRetry) {
|
const bool suppressHyphenationRetry, const int firstLineIndent) {
|
||||||
const size_t lineBreak = lineBreakIndices[breakIndex];
|
const size_t lineBreak = lineBreakIndices[breakIndex];
|
||||||
const size_t lastBreakAt = breakIndex > 0 ? lineBreakIndices[breakIndex - 1] : 0;
|
const size_t lastBreakAt = breakIndex > 0 ? lineBreakIndices[breakIndex - 1] : 0;
|
||||||
const size_t lineWordCount = lineBreak - lastBreakAt;
|
const size_t lineWordCount = lineBreak - lastBreakAt;
|
||||||
|
|
||||||
// Calculate first line indent (only for left/justified text).
|
// Apply indent only to line 0 of the layout pass; firstLineIndent is already
|
||||||
// Explicit CSS text-indent always applies — author intent overrides the extraParagraphSpacing
|
// 0 for continuation flushes (computed once in layoutAndExtractLines).
|
||||||
// toggle. Only the implicit EmSpace fallback in applyParagraphIndent() is gated on it.
|
const int lineIndent = (breakIndex == 0) ? firstLineIndent : 0;
|
||||||
const bool isFirstLine = breakIndex == 0;
|
|
||||||
const int firstLineIndent =
|
|
||||||
isFirstLine && blockStyle.textIndentDefined &&
|
|
||||||
(blockStyle.alignment == CssTextAlign::Justify || blockStyle.alignment == CssTextAlign::Left)
|
|
||||||
? std::min(std::max<int>(static_cast<int>(blockStyle.textIndent), -(pageWidth - 1)), pageWidth - 1)
|
|
||||||
: 0;
|
|
||||||
|
|
||||||
// Calculate total word width for this line, count actual word gaps,
|
// Calculate total word width for this line, count actual word gaps,
|
||||||
// and accumulate total natural gap widths (including space kerning adjustments).
|
// and accumulate total natural gap widths (including space kerning adjustments).
|
||||||
@@ -989,7 +990,7 @@ ParsedText::LineProcessResult ParsedText::extractLine(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Calculate spacing (account for indent reducing effective page width on first line)
|
// Calculate spacing (account for indent reducing effective page width on first line)
|
||||||
const int effectivePageWidth = pageWidth - firstLineIndent;
|
const int effectivePageWidth = pageWidth - lineIndent;
|
||||||
// A line is only truly last when it consumes all paragraph words.
|
// A line is only truly last when it consumes all paragraph words.
|
||||||
// During single-line retry we may temporarily pass a truncated break vector,
|
// During single-line retry we may temporarily pass a truncated break vector,
|
||||||
// so relying only on breakIndex would incorrectly disable justification.
|
// so relying only on breakIndex would incorrectly disable justification.
|
||||||
@@ -1003,7 +1004,7 @@ ParsedText::LineProcessResult ParsedText::extractLine(
|
|||||||
|
|
||||||
// Calculate initial x position (first line starts at indent for left/justified text;
|
// Calculate initial x position (first line starts at indent for left/justified text;
|
||||||
// may be negative for hanging indents, e.g. margin-left:3em; text-indent:-1em).
|
// may be negative for hanging indents, e.g. margin-left:3em; text-indent:-1em).
|
||||||
auto xpos = static_cast<int16_t>(firstLineIndent);
|
auto xpos = static_cast<int16_t>(lineIndent);
|
||||||
if (blockStyle.alignment == CssTextAlign::Right) {
|
if (blockStyle.alignment == CssTextAlign::Right) {
|
||||||
xpos = effectivePageWidth - lineWordWidthSum - totalNaturalGaps;
|
xpos = effectivePageWidth - lineWordWidthSum - totalNaturalGaps;
|
||||||
} else if (blockStyle.alignment == CssTextAlign::Center) {
|
} else if (blockStyle.alignment == CssTextAlign::Center) {
|
||||||
|
|||||||
@@ -27,16 +27,19 @@ class ParsedText {
|
|||||||
bool extraParagraphSpacing;
|
bool extraParagraphSpacing;
|
||||||
bool hyphenationEnabled;
|
bool hyphenationEnabled;
|
||||||
bool bionicReadingEnabled;
|
bool bionicReadingEnabled;
|
||||||
|
bool isContinuation_ = false; ///< true after an intermediate flush; suppresses re-applying paragraph indent
|
||||||
|
size_t bionicTransformedUpTo_ = 0; ///< words[0..bionicTransformedUpTo_) have already been bionic-transformed
|
||||||
|
|
||||||
void applyParagraphIndent();
|
void applyParagraphIndent();
|
||||||
void applyBionicReadingTransform();
|
void applyBionicReadingTransform();
|
||||||
std::vector<size_t> computeLineBreaks(const GfxRenderer& renderer, int fontId, int pageWidth,
|
std::vector<size_t> computeLineBreaks(const GfxRenderer& renderer, int fontId, int pageWidth,
|
||||||
std::vector<uint16_t>& wordWidths, std::vector<bool>& continuesVec);
|
std::vector<uint16_t>& wordWidths, std::vector<bool>& continuesVec,
|
||||||
|
int firstLineIndent);
|
||||||
std::vector<size_t> computeHyphenatedLineBreaks(const GfxRenderer& renderer, int fontId, int pageWidth,
|
std::vector<size_t> computeHyphenatedLineBreaks(const GfxRenderer& renderer, int fontId, int pageWidth,
|
||||||
std::vector<uint16_t>& wordWidths, std::vector<bool>& continuesVec,
|
std::vector<uint16_t>& wordWidths, std::vector<bool>& continuesVec,
|
||||||
std::vector<bool>& lineEndsWithHyphenatedWord,
|
std::vector<bool>& lineEndsWithHyphenatedWord,
|
||||||
std::vector<int>& splitPrefixWordIndexes,
|
std::vector<int>& splitPrefixWordIndexes,
|
||||||
std::vector<bool>& splitInsertedHyphen);
|
std::vector<bool>& splitInsertedHyphen, int firstLineIndent);
|
||||||
// Recompute hyphenated breaks for a suffix that starts at startIndex.
|
// Recompute hyphenated breaks for a suffix that starts at startIndex.
|
||||||
// Used after a single-line retry so later lines keep normal hyphenation.
|
// Used after a single-line retry so later lines keep normal hyphenation.
|
||||||
std::vector<size_t> computeHyphenatedLineBreaksFromIndex(const GfxRenderer& renderer, int fontId, int pageWidth,
|
std::vector<size_t> computeHyphenatedLineBreaksFromIndex(const GfxRenderer& renderer, int fontId, int pageWidth,
|
||||||
@@ -49,7 +52,7 @@ class ParsedText {
|
|||||||
// Used only for the page-boundary retry line.
|
// Used only for the page-boundary retry line.
|
||||||
size_t computeSingleLineBreakNoHyphen(const GfxRenderer& renderer, int fontId, int pageWidth,
|
size_t computeSingleLineBreakNoHyphen(const GfxRenderer& renderer, int fontId, int pageWidth,
|
||||||
const std::vector<uint16_t>& wordWidths, const std::vector<bool>& continuesVec,
|
const std::vector<uint16_t>& wordWidths, const std::vector<bool>& continuesVec,
|
||||||
size_t lineStartIndex) const;
|
size_t lineStartIndex, int firstLineIndent) const;
|
||||||
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);
|
||||||
@@ -57,7 +60,8 @@ class ParsedText {
|
|||||||
size_t breakIndex, int pageWidth, const std::vector<uint16_t>& wordWidths, const std::vector<bool>& continuesVec,
|
size_t breakIndex, int pageWidth, const std::vector<uint16_t>& wordWidths, const std::vector<bool>& continuesVec,
|
||||||
const std::vector<size_t>& lineBreakIndices,
|
const std::vector<size_t>& lineBreakIndices,
|
||||||
const std::function<LineProcessResult(std::shared_ptr<TextBlock>, bool, bool)>& processLine,
|
const std::function<LineProcessResult(std::shared_ptr<TextBlock>, bool, bool)>& processLine,
|
||||||
const GfxRenderer& renderer, int fontId, bool lineEndsWithHyphenatedWord, bool suppressHyphenationRetry);
|
const GfxRenderer& renderer, int fontId, bool lineEndsWithHyphenatedWord, bool suppressHyphenationRetry,
|
||||||
|
int firstLineIndent);
|
||||||
std::vector<uint16_t> calculateWordWidths(const GfxRenderer& renderer, int fontId);
|
std::vector<uint16_t> calculateWordWidths(const GfxRenderer& renderer, int fontId);
|
||||||
|
|
||||||
public:
|
public:
|
||||||
@@ -74,6 +78,7 @@ class ParsedText {
|
|||||||
BlockStyle& getBlockStyle() { return blockStyle; }
|
BlockStyle& getBlockStyle() { return blockStyle; }
|
||||||
size_t size() const { return words.size(); }
|
size_t size() const { return words.size(); }
|
||||||
bool isEmpty() const { return words.empty(); }
|
bool isEmpty() const { return words.empty(); }
|
||||||
|
bool isContinuation() const { return isContinuation_; }
|
||||||
void layoutAndExtractLines(
|
void layoutAndExtractLines(
|
||||||
const GfxRenderer& renderer, int fontId, uint16_t viewportWidth,
|
const GfxRenderer& renderer, int fontId, uint16_t viewportWidth,
|
||||||
const std::function<LineProcessResult(std::shared_ptr<TextBlock>, bool, bool)>& processLine,
|
const std::function<LineProcessResult(std::shared_ptr<TextBlock>, bool, bool)>& processLine,
|
||||||
|
|||||||
@@ -189,6 +189,20 @@ void ChapterHtmlSlimParser::flushPartWordBuffer() {
|
|||||||
currentTextBlock->addWord(partWordBuffer, fontStyle, false, nextWordContinues);
|
currentTextBlock->addWord(partWordBuffer, fontStyle, false, nextWordContinues);
|
||||||
partWordBufferIndex = 0;
|
partWordBufferIndex = 0;
|
||||||
nextWordContinues = false;
|
nextWordContinues = false;
|
||||||
|
|
||||||
|
if (currentTextBlock->size() > 96) {
|
||||||
|
LOG_DBG("EHP", "Text block too long, splitting into multiple pages");
|
||||||
|
const int horizontalInset = currentTextBlock->getBlockStyle().totalHorizontalInset();
|
||||||
|
const uint16_t effectiveWidth =
|
||||||
|
(horizontalInset < viewportWidth) ? static_cast<uint16_t>(viewportWidth - horizontalInset) : viewportWidth;
|
||||||
|
currentTextBlock->layoutAndExtractLines(
|
||||||
|
renderer, fontId, effectiveWidth,
|
||||||
|
[this](const std::shared_ptr<TextBlock>& textBlock, const bool lineEndsWithHyphenatedWord,
|
||||||
|
const bool suppressHyphenationRetry) {
|
||||||
|
return addLineToPage(textBlock, lineEndsWithHyphenatedWord, suppressHyphenationRetry);
|
||||||
|
},
|
||||||
|
false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Emit the current page, keeping paragraphLutPerPage and completedPageCount in lockstep.
|
// Emit the current page, keeping paragraphLutPerPage and completedPageCount in lockstep.
|
||||||
@@ -1206,25 +1220,6 @@ void XMLCALL ChapterHtmlSlimParser::characterData(void* userData, const XML_Char
|
|||||||
|
|
||||||
self->partWordBuffer[self->partWordBufferIndex++] = s[i];
|
self->partWordBuffer[self->partWordBufferIndex++] = s[i];
|
||||||
}
|
}
|
||||||
|
|
||||||
// If we have > 750 words buffered up, perform the layout and consume out all but the last line
|
|
||||||
// There should be enough here to build out 1-2 full pages and doing this will free up a lot of
|
|
||||||
// memory.
|
|
||||||
// Spotted when reading Intermezzo, there are some really long text blocks in there.
|
|
||||||
if (self->currentTextBlock->size() > 750) {
|
|
||||||
LOG_DBG("EHP", "Text block too long, splitting into multiple pages");
|
|
||||||
const int horizontalInset = self->currentTextBlock->getBlockStyle().totalHorizontalInset();
|
|
||||||
const uint16_t effectiveWidth = (horizontalInset < self->viewportWidth)
|
|
||||||
? static_cast<uint16_t>(self->viewportWidth - horizontalInset)
|
|
||||||
: self->viewportWidth;
|
|
||||||
self->currentTextBlock->layoutAndExtractLines(
|
|
||||||
self->renderer, self->fontId, effectiveWidth,
|
|
||||||
[self](const std::shared_ptr<TextBlock>& textBlock, const bool lineEndsWithHyphenatedWord,
|
|
||||||
const bool suppressHyphenationRetry) {
|
|
||||||
return self->addLineToPage(textBlock, lineEndsWithHyphenatedWord, suppressHyphenationRetry);
|
|
||||||
},
|
|
||||||
false);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void XMLCALL ChapterHtmlSlimParser::defaultHandlerExpand(void* userData, const XML_Char* s, const int len) {
|
void XMLCALL ChapterHtmlSlimParser::defaultHandlerExpand(void* userData, const XML_Char* s, const int len) {
|
||||||
@@ -1591,13 +1586,17 @@ void ChapterHtmlSlimParser::makePages() {
|
|||||||
|
|
||||||
const int lineHeight = renderer.getLineHeight(fontId) * lineCompression;
|
const int lineHeight = renderer.getLineHeight(fontId) * lineCompression;
|
||||||
|
|
||||||
// Apply top spacing before the paragraph (stored in pixels)
|
// Apply top spacing before the paragraph — skip for continuation fragments
|
||||||
|
// (words left over after an intermediate flush): the top margin was already
|
||||||
|
// applied before the first set of lines from this logical paragraph.
|
||||||
const BlockStyle& blockStyle = currentTextBlock->getBlockStyle();
|
const BlockStyle& blockStyle = currentTextBlock->getBlockStyle();
|
||||||
if (blockStyle.marginTop > 0) {
|
if (!currentTextBlock->isContinuation()) {
|
||||||
currentPageNextY += blockStyle.marginTop;
|
if (blockStyle.marginTop > 0) {
|
||||||
}
|
currentPageNextY += blockStyle.marginTop;
|
||||||
if (blockStyle.paddingTop > 0) {
|
}
|
||||||
currentPageNextY += blockStyle.paddingTop;
|
if (blockStyle.paddingTop > 0) {
|
||||||
|
currentPageNextY += blockStyle.paddingTop;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Calculate effective width accounting for horizontal margins/padding
|
// Calculate effective width accounting for horizontal margins/padding
|
||||||
|
|||||||
Reference in New Issue
Block a user