diff --git a/lib/EpdFont/FontDecompressor.cpp b/lib/EpdFont/FontDecompressor.cpp index 1f34186b..a0153155 100644 --- a/lib/EpdFont/FontDecompressor.cpp +++ b/lib/EpdFont/FontDecompressor.cpp @@ -112,7 +112,7 @@ void FontDecompressor::compactSingleGlyph(const uint8_t* alignedSrc, uint8_t* pa if (outBits > 0) packedDst[writeIdx] = outByte << (8 - outBits); } -// --- getBitmap: page buffer → hot group → decompress --- +// --- getBitmap: page buffer → transient malloc + decompress + compact --- const uint8_t* FontDecompressor::getBitmap(const EpdFontData* fontData, const EpdGlyph* glyph, uint32_t glyphIndex) { const uint32_t tStart = micros(); @@ -159,6 +159,12 @@ const uint8_t* FontDecompressor::getBitmap(const EpdFontData* fontData, const Ep stats.cacheMisses++; const EpdFontGroup& group = fontData->groups[groupIndex]; + if (glyph->dataLength > HOT_GLYPH_BUF_SIZE) { + LOG_ERR("FDC", "Glyph dataLength %u exceeds HOT_GLYPH_BUF_SIZE %u", glyph->dataLength, HOT_GLYPH_BUF_SIZE); + stats.getBitmapTimeUs += micros() - tStart; + return nullptr; + } + if (group.uncompressedSize > stats.peakTempBytes) stats.peakTempBytes = group.uncompressedSize; uint8_t* groupBuf = static_cast(malloc(group.uncompressedSize)); @@ -174,13 +180,6 @@ const uint8_t* FontDecompressor::getBitmap(const EpdFontData* fontData, const Ep return nullptr; } - if (glyph->dataLength > HOT_GLYPH_BUF_SIZE) { - LOG_ERR("FDC", "Glyph dataLength %u exceeds HOT_GLYPH_BUF_SIZE %u", glyph->dataLength, HOT_GLYPH_BUF_SIZE); - free(groupBuf); - stats.getBitmapTimeUs += micros() - tStart; - return nullptr; - } - uint32_t alignedOff = getAlignedOffset(fontData, groupIndex, glyphIndex); compactSingleGlyph(&groupBuf[alignedOff], _hotGlyphBuf, glyph->width, glyph->height); free(groupBuf); @@ -348,6 +347,8 @@ int FontDecompressor::prewarmCache(const EpdFontData* fontData, const char* utf8 if (!groupIdToPos) { LOG_ERR("FDC", "OOM: cannot allocate %u bytes for groupIdToPos map", fontData->groupCount); // Roll back this slot only (other slots from prior prewarmCache calls stay valid) + stats.pageBufferBytes -= totalBytes; + stats.pageGlyphsBytes -= glyphCount * sizeof(PageGlyphEntry); free(slot.buffer); free(slot.glyphs); slot = {}; diff --git a/lib/EpdFont/FontDecompressor.h b/lib/EpdFont/FontDecompressor.h index e4bf5c05..8f574dba 100644 --- a/lib/EpdFont/FontDecompressor.h +++ b/lib/EpdFont/FontDecompressor.h @@ -16,7 +16,11 @@ class FontDecompressor { void deinit(); // Returns pointer to decompressed bitmap data for the given glyph. - // Checks the page buffer (from prewarm) first, then falls back to the hot group slot. + // Checks the page buffer (from prewarm) first and otherwise transiently + // allocates/decompresses the glyph's group into a temporary buffer and + // compacts the requested glyph. The returned pointer is valid only until the + // next getBitmap call or cache eviction; callers must copy bitmap data if a + // longer lifetime is required. const uint8_t* getBitmap(const EpdFontData* fontData, const EpdGlyph* glyph, uint32_t glyphIndex); // Free all cached data (page buffers). diff --git a/lib/EpdFont/scripts/fontconvert.py b/lib/EpdFont/scripts/fontconvert.py index 64f6c228..69b06c26 100755 --- a/lib/EpdFont/scripts/fontconvert.py +++ b/lib/EpdFont/scripts/fontconvert.py @@ -765,9 +765,14 @@ if compress: group_count = 0 group_uncompressed = 0 - for i, (props, packed) in enumerate(all_glyphs): + for i, (props, _) in enumerate(all_glyphs): sg = get_script_group(props.code_point) glyph_aligned_size = ((props.width + 3) // 4) * props.height if props.width > 0 and props.height > 0 else 0 + if glyph_aligned_size > GROUP_MAX_UNCOMPRESSED_BYTES: + raise ValueError( + f"Glyph {i} (code point U+{props.code_point:04X}) single aligned size " + f"{glyph_aligned_size} exceeds GROUP_MAX_UNCOMPRESSED_BYTES={GROUP_MAX_UNCOMPRESSED_BYTES}" + ) size_overflow = group_uncompressed + glyph_aligned_size > GROUP_MAX_UNCOMPRESSED_BYTES if sg != current_group_id or size_overflow: diff --git a/lib/Epub/Epub/ParsedText.cpp b/lib/Epub/Epub/ParsedText.cpp index ecac74c1..f24550c7 100644 --- a/lib/Epub/Epub/ParsedText.cpp +++ b/lib/Epub/Epub/ParsedText.cpp @@ -18,7 +18,9 @@ constexpr int MAX_COST = std::numeric_limits::max(); namespace { // Closing punctuation that should not have extra space inserted before it during justification. -// Includes common closing brackets/quotes and sentence-ending marks. +// Includes common closing brackets/quotes and sentence-ending marks. En/em dashes +// are also treated as inline separators here to avoid justification stretch +// immediately before them. bool isClosingPunctuation(const uint32_t cp) { switch (cp) { case '.': @@ -804,17 +806,16 @@ ParsedText::LineProcessResult ParsedText::extractLine( // Count gaps: each word after the first creates a gap, unless it's a continuation. // Gaps before closing punctuation (. , ) » etc.) are excluded from justification // distribution so they stay at natural space width. + const uint32_t firstCp = firstCodepoint(words[lastBreakAt + wordIdx]); if (wordIdx > 0 && !continuesVec[lastBreakAt + wordIdx]) { - const bool beforeClosing = isClosingPunctuation(firstCodepoint(words[lastBreakAt + wordIdx])); + const bool beforeClosing = isClosingPunctuation(firstCp); if (!beforeClosing) actualGapCount++; - totalNaturalGaps += - renderer.getSpaceAdvance(fontId, lastCodepoint(words[lastBreakAt + wordIdx - 1]), - firstCodepoint(words[lastBreakAt + wordIdx]), wordStyles[lastBreakAt + wordIdx - 1]); + totalNaturalGaps += renderer.getSpaceAdvance(fontId, lastCodepoint(words[lastBreakAt + wordIdx - 1]), firstCp, + wordStyles[lastBreakAt + wordIdx - 1]); } else if (wordIdx > 0 && continuesVec[lastBreakAt + wordIdx]) { // Cross-boundary kerning for continuation words (e.g. nonbreaking spaces, attached punctuation) - totalNaturalGaps += - renderer.getKerning(fontId, lastCodepoint(words[lastBreakAt + wordIdx - 1]), - firstCodepoint(words[lastBreakAt + wordIdx]), wordStyles[lastBreakAt + wordIdx - 1]); + totalNaturalGaps += renderer.getKerning(fontId, lastCodepoint(words[lastBreakAt + wordIdx - 1]), firstCp, + wordStyles[lastBreakAt + wordIdx - 1]); } } @@ -859,12 +860,12 @@ ParsedText::LineProcessResult ParsedText::extractLine( } else { int gap = 0; if (wordIdx + 1 < lineWordCount) { - gap = renderer.getSpaceAdvance(fontId, lastCodepoint(words[lastBreakAt + wordIdx]), - firstCodepoint(words[lastBreakAt + wordIdx + 1]), + const uint32_t nextFirstCp = firstCodepoint(words[lastBreakAt + wordIdx + 1]); + gap = renderer.getSpaceAdvance(fontId, lastCodepoint(words[lastBreakAt + wordIdx]), nextFirstCp, wordStyles[lastBreakAt + wordIdx]); // Don't stretch the gap before closing punctuation — it looks wrong with // extra space before ".", ")", "»" etc. - const bool nextIsClosing = isClosingPunctuation(firstCodepoint(words[lastBreakAt + wordIdx + 1])); + const bool nextIsClosing = isClosingPunctuation(nextFirstCp); if (blockStyle.alignment == CssTextAlign::Justify && !isLastLine && !nextIsClosing) { gap += justifyExtra; }