diff --git a/lib/EpdFont/FontDecompressor.cpp b/lib/EpdFont/FontDecompressor.cpp index 10af14df..a0153155 100644 --- a/lib/EpdFont/FontDecompressor.cpp +++ b/lib/EpdFont/FontDecompressor.cpp @@ -5,6 +5,7 @@ #include #include +#include FontDecompressor::~FontDecompressor() { deinit(); } @@ -13,15 +14,9 @@ bool FontDecompressor::init() { return true; } -void FontDecompressor::deinit() { - freePageBuffer(); - freeHotGroup(); -} +void FontDecompressor::deinit() { freePageBuffer(); } -void FontDecompressor::clearCache() { - freePageBuffer(); - freeHotGroup(); -} +void FontDecompressor::clearCache() { freePageBuffer(); } void FontDecompressor::freePageBuffer() { for (uint8_t s = 0; s < pageSlotCount; s++) { @@ -32,15 +27,6 @@ void FontDecompressor::freePageBuffer() { pageSlotCount = 0; } -void FontDecompressor::freeHotGroup() { - hotGroup.clear(); - hotGroup.shrink_to_fit(); - hotGroupFont = nullptr; - hotGroupIndex = UINT16_MAX; - hotGlyphBuf.clear(); - hotGlyphBuf.shrink_to_fit(); -} - uint16_t FontDecompressor::getGroupIndex(const EpdFontData* fontData, uint32_t glyphIndex) { // O(1) path for frequency-grouped fonts with glyphToGroup mapping if (fontData->glyphToGroup != nullptr) { @@ -60,10 +46,10 @@ uint16_t FontDecompressor::getGroupIndex(const EpdFontData* fontData, uint32_t g bool FontDecompressor::decompressGroup(const EpdFontData* fontData, uint16_t groupIndex, uint8_t* outBuf, uint32_t outSize) { const EpdFontGroup& group = fontData->groups[groupIndex]; - const uint32_t tDecomp = millis(); inflateReader.init(false); inflateReader.setSource(&fontData->bitmap[group.compressedOffset], group.compressedSize); + if (!inflateReader.read(outBuf, outSize)) { stats.decompressTimeMs += millis() - tDecomp; LOG_ERR("FDC", "Decompression failed for group %u", groupIndex); @@ -126,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(); @@ -161,7 +147,8 @@ const uint8_t* FontDecompressor::getBitmap(const EpdFontData* fontData, const Ep break; // Found the right slot but glyph wasn't in it; don't check other slots } - // Fallback: hot group slot + // Fallback: glyph wasn't in the page buffer — decompress its group transiently. + // This is the rare path (prewarm should cover all glyphs on a normal page). uint16_t groupIndex = getGroupIndex(fontData, glyphIndex); if (groupIndex >= fontData->groupCount) { LOG_ERR("FDC", "Glyph %u not found in any group", glyphIndex); @@ -169,49 +156,36 @@ const uint8_t* FontDecompressor::getBitmap(const EpdFontData* fontData, const Ep return nullptr; } - // Check if hot group already has this group decompressed — if not, decompress it - if (!(!hotGroup.empty() && hotGroupFont == fontData && hotGroupIndex == groupIndex)) { - stats.cacheMisses++; - const EpdFontGroup& group = fontData->groups[groupIndex]; + stats.cacheMisses++; + const EpdFontGroup& group = fontData->groups[groupIndex]; - hotGroup.resize(group.uncompressedSize); - if (hotGroup.empty()) { - LOG_ERR("FDC", "Failed to allocate %u bytes for hot group %u", group.uncompressedSize, groupIndex); - hotGroupFont = nullptr; - hotGroupIndex = UINT16_MAX; - stats.getBitmapTimeUs += micros() - tStart; - return nullptr; - } - - if (!decompressGroup(fontData, groupIndex, hotGroup.data(), group.uncompressedSize)) { - hotGroup.clear(); - hotGroup.shrink_to_fit(); - hotGroupFont = nullptr; - hotGroupIndex = UINT16_MAX; - stats.getBitmapTimeUs += micros() - tStart; - return nullptr; - } - - hotGroupFont = fontData; - hotGroupIndex = groupIndex; - stats.hotGroupBytes = group.uncompressedSize; - } else { - stats.cacheHits++; + 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; } - // Compact just the requested glyph from byte-aligned data into scratch buffer - if (glyph->dataLength > hotGlyphBuf.size()) { - hotGlyphBuf.resize(glyph->dataLength); + if (group.uncompressedSize > stats.peakTempBytes) stats.peakTempBytes = group.uncompressedSize; + + uint8_t* groupBuf = static_cast(malloc(group.uncompressedSize)); + if (!groupBuf) { + LOG_ERR("FDC", "OOM: cannot allocate %lu bytes for group %u fallback", group.uncompressedSize, groupIndex); + stats.getBitmapTimeUs += micros() - tStart; + return nullptr; } - if (hotGlyphBuf.empty()) { + + if (!decompressGroup(fontData, groupIndex, groupBuf, group.uncompressedSize)) { + free(groupBuf); stats.getBitmapTimeUs += micros() - tStart; return nullptr; } uint32_t alignedOff = getAlignedOffset(fontData, groupIndex, glyphIndex); - compactSingleGlyph(&hotGroup[alignedOff], hotGlyphBuf.data(), glyph->width, glyph->height); + compactSingleGlyph(&groupBuf[alignedOff], _hotGlyphBuf, glyph->width, glyph->height); + free(groupBuf); + stats.getBitmapTimeUs += micros() - tStart; - return hotGlyphBuf.data(); + return _hotGlyphBuf; } // --- Prewarm: pre-decompress glyph bitmaps for a page of text --- @@ -289,12 +263,14 @@ int FontDecompressor::prewarmCache(const EpdFontData* fontData, const char* utf8 // Step 2: Compute total buffer size and collect unique groups uint32_t totalBytes = 0; uint16_t neededGroups[128]; + uint16_t neededGlyphGroups[MAX_PAGE_GLYPHS]; // parallel to neededGlyphs; avoids re-calling getGroupIndex later uint8_t groupCount = 0; bool groupCapWarned = false; for (uint16_t i = 0; i < glyphCount; i++) { totalBytes += fontData->glyph[neededGlyphs[i]].dataLength; - uint16_t gi = getGroupIndex(fontData, neededGlyphs[i]); + const uint16_t gi = getGroupIndex(fontData, neededGlyphs[i]); + neededGlyphGroups[i] = gi; bool found = false; for (uint8_t j = 0; j < groupCount; j++) { if (neededGroups[j] == gi) { @@ -314,6 +290,18 @@ int FontDecompressor::prewarmCache(const EpdFontData* fontData, const char* utf8 stats.uniqueGroupsAccessed = groupCount; + // Sort neededGroups by ascending group index so flash reads are sequential. + // Uses insertion sort — groupCount is bounded at 128, typically <14 for Latin fonts. + for (uint8_t i = 1; i < groupCount; i++) { + uint16_t key = neededGroups[i]; + int j = i - 1; + while (j >= 0 && neededGroups[j] > key) { + neededGroups[j + 1] = neededGroups[j]; + j--; + } + neededGroups[j + 1] = key; + } + // Step 3: Allocate page buffer and lookup table for this slot slot.buffer = static_cast(malloc(totalBytes)); slot.glyphs = static_cast(malloc(glyphCount * sizeof(PageGlyphEntry))); @@ -333,7 +321,7 @@ int FontDecompressor::prewarmCache(const EpdFontData* fontData, const char* utf8 // Initialize lookup entries (bufferOffset = UINT32_MAX means not yet extracted) for (uint16_t i = 0; i < glyphCount; i++) { - slot.glyphs[i] = {neededGlyphs[i], UINT32_MAX, 0}; + slot.glyphs[i] = {neededGlyphs[i], UINT32_MAX, 0, neededGlyphGroups[i]}; } // Sort by glyphIndex for binary search in getBitmap() @@ -352,21 +340,31 @@ int FontDecompressor::prewarmCache(const EpdFontData* fontData, const char* utf8 uint32_t groupAlignedTracker[128] = {}; // running byte-aligned offset for each needed group if (fontData->glyphToGroup) { - // Frequency-grouped: single O(totalGlyphs) pass through glyphToGroup + // Frequency-grouped: single O(totalGlyphs) pass through glyphToGroup. + // Reverse map (fontGroupIdx → position in neededGroups) replaces the inner + // linear scan, dropping this pass from O(totalGlyphs × groupCount) to O(totalGlyphs). + uint8_t* groupIdToPos = static_cast(malloc(fontData->groupCount)); + 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 = {}; + pageSlotCount--; + return glyphCount; + } + memset(groupIdToPos, 0xFF, fontData->groupCount); + for (uint8_t j = 0; j < groupCount; j++) groupIdToPos[neededGroups[j]] = j; + const auto& lastInterval = fontData->intervals[fontData->intervalCount - 1]; const uint32_t totalGlyphs = lastInterval.offset + (lastInterval.last - lastInterval.first + 1); for (uint32_t i = 0; i < totalGlyphs; i++) { const uint16_t gi = fontData->glyphToGroup[i]; - // Find this glyph's group position in neededGroups - uint8_t gpPos = groupCount; - for (uint8_t j = 0; j < groupCount; j++) { - if (neededGroups[j] == gi) { - gpPos = j; - break; - } - } - if (gpPos == groupCount) continue; // not a needed group + const uint8_t gpPos = groupIdToPos[gi]; + if (gpPos == 0xFF) continue; // not a needed group const EpdGlyph& glyph = fontData->glyph[i]; @@ -388,6 +386,8 @@ int FontDecompressor::prewarmCache(const EpdFontData* fontData, const char* utf8 groupAlignedTracker[gpPos] += ((glyph.width + 3) / 4) * glyph.height; } } + + free(groupIdToPos); } else { // Contiguous-group: iterate each needed group's glyphs directly for (uint8_t g = 0; g < groupCount; g++) { @@ -417,7 +417,9 @@ int FontDecompressor::prewarmCache(const EpdFontData* fontData, const char* utf8 } } - // Step 4: For each unique group, decompress to temp buffer and extract needed glyphs + // Step 4: For each unique group, malloc a transient buffer, decompress, extract needed glyphs, free. + // One malloc/free per group per prewarm call. Groups are visited in sorted order, so + // only one group buffer is alive at a time — peak heap = page buffer + largest single group. uint32_t writeOffset = 0; int missed = 0; @@ -425,35 +427,34 @@ int FontDecompressor::prewarmCache(const EpdFontData* fontData, const char* utf8 uint16_t groupIdx = neededGroups[g]; const EpdFontGroup& group = fontData->groups[groupIdx]; - auto* tempBuf = static_cast(malloc(group.uncompressedSize)); - if (!tempBuf) { - LOG_ERR("FDC", "Failed to allocate temp buffer (%u bytes) for group %u", group.uncompressedSize, groupIdx); - missed++; - continue; - } - if (group.uncompressedSize > stats.peakTempBytes) { - stats.peakTempBytes = group.uncompressedSize; - } + if (group.uncompressedSize > stats.peakTempBytes) stats.peakTempBytes = group.uncompressedSize; - if (!decompressGroup(fontData, groupIdx, tempBuf, group.uncompressedSize)) { - free(tempBuf); + uint8_t* groupBuf = static_cast(malloc(group.uncompressedSize)); + if (!groupBuf) { + LOG_ERR("FDC", "OOM: cannot allocate %lu bytes for group %u during prewarm", group.uncompressedSize, groupIdx); missed++; continue; } - // Extract needed glyphs directly from the byte-aligned temp buffer, compacting on the fly. + if (!decompressGroup(fontData, groupIdx, groupBuf, group.uncompressedSize)) { + free(groupBuf); + missed++; + continue; + } + + // Extract needed glyphs directly from the byte-aligned buffer, compacting on the fly. // alignedOffset was pre-computed in step 3b — no full-group compact scan needed. for (uint16_t i = 0; i < slot.glyphCount; i++) { if (slot.glyphs[i].bufferOffset != UINT32_MAX) continue; // already extracted - if (getGroupIndex(fontData, slot.glyphs[i].glyphIndex) != groupIdx) continue; + if (slot.glyphs[i].groupIndex != groupIdx) continue; const EpdGlyph& glyph = fontData->glyph[slot.glyphs[i].glyphIndex]; - compactSingleGlyph(&tempBuf[slot.glyphs[i].alignedOffset], &slot.buffer[writeOffset], glyph.width, glyph.height); + compactSingleGlyph(&groupBuf[slot.glyphs[i].alignedOffset], &slot.buffer[writeOffset], glyph.width, glyph.height); slot.glyphs[i].bufferOffset = writeOffset; writeOffset += glyph.dataLength; } - free(tempBuf); + free(groupBuf); } LOG_DBG("FDC", "Prewarm: %u glyphs in %u bytes from %u groups (%d missed)", glyphCount, writeOffset, groupCount, @@ -471,8 +472,8 @@ void FontDecompressor::logStats(const char* label) { LOG_DBG("FDC", "[%s] hits=%lu misses=%lu (%.1f%% hit rate)", label, stats.cacheHits, stats.cacheMisses, total > 0 ? 100.0f * stats.cacheHits / total : 0.0f); LOG_DBG("FDC", "[%s] decompress=%lums groups_accessed=%u", label, stats.decompressTimeMs, stats.uniqueGroupsAccessed); - LOG_DBG("FDC", "[%s] mem: pageBuf=%lu pageGlyphs=%lu hotGroup=%lu peakTemp=%lu", label, stats.pageBufferBytes, - stats.pageGlyphsBytes, stats.hotGroupBytes, stats.peakTempBytes); + LOG_DBG("FDC", "[%s] mem: pageBuf=%lu pageGlyphs=%lu peakTemp=%lu", label, stats.pageBufferBytes, + stats.pageGlyphsBytes, stats.peakTempBytes); if (stats.getBitmapCalls > 0) { LOG_DBG("FDC", "[%s] getBitmap: %lu calls, %luus total, %luus/call avg", label, stats.getBitmapCalls, stats.getBitmapTimeUs, stats.getBitmapTimeUs / stats.getBitmapCalls); diff --git a/lib/EpdFont/FontDecompressor.h b/lib/EpdFont/FontDecompressor.h index 54e75a86..8f574dba 100644 --- a/lib/EpdFont/FontDecompressor.h +++ b/lib/EpdFont/FontDecompressor.h @@ -2,8 +2,6 @@ #include -#include - #include "EpdFontData.h" class FontDecompressor { @@ -18,10 +16,14 @@ 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 buffer + hot group). + // Free all cached data (page buffers). void clearCache(); // Pre-scan UTF-8 text and extract needed glyph bitmaps into a flat page buffer. @@ -36,8 +38,7 @@ class FontDecompressor { uint16_t uniqueGroupsAccessed = 0; uint32_t pageBufferBytes = 0; // pageBuffer allocation uint32_t pageGlyphsBytes = 0; // pageGlyphs lookup table allocation - uint32_t hotGroupBytes = 0; // current hot group allocation - uint32_t peakTempBytes = 0; // largest temp buffer in prewarm + uint32_t peakTempBytes = 0; // largest temp buffer in prewarm or getBitmap miss uint32_t getBitmapTimeUs = 0; // cumulative getBitmap time (micros) uint32_t getBitmapCalls = 0; // number of getBitmap calls }; @@ -55,6 +56,7 @@ class FontDecompressor { uint32_t glyphIndex; uint32_t bufferOffset; uint32_t alignedOffset; // byte-aligned offset within its decompressed group (set during prewarm pre-scan) + uint16_t groupIndex; // cached to avoid re-calling getGroupIndex in prewarm Step 4 }; struct PageSlot { uint8_t* buffer = nullptr; @@ -65,18 +67,13 @@ class FontDecompressor { PageSlot pageSlots[MAX_PAGE_SLOTS] = {}; uint8_t pageSlotCount = 0; - // Hot group: last decompressed group (byte-aligned) for non-prewarmed fallback path. - // Kept in byte-aligned format; individual glyphs are compacted on demand into hotGlyphBuf. - const EpdFontData* hotGroupFont = nullptr; - uint16_t hotGroupIndex = UINT16_MAX; - std::vector hotGroup; + static constexpr uint16_t HOT_GLYPH_BUF_SIZE = 512; // largest packed single glyph - // Scratch buffer for compacting a single glyph from the hot group. + // Scratch buffer for compacting a single glyph after a getBitmap() miss. // Valid until the next getBitmap() call. - std::vector hotGlyphBuf; + uint8_t _hotGlyphBuf[HOT_GLYPH_BUF_SIZE] = {}; void freePageBuffer(); - void freeHotGroup(); uint16_t getGroupIndex(const EpdFontData* fontData, uint32_t glyphIndex); uint32_t getAlignedOffset(const EpdFontData* fontData, uint16_t groupIndex, uint32_t glyphIndex); bool decompressGroup(const EpdFontData* fontData, uint16_t groupIndex, uint8_t* outBuf, uint32_t outSize); diff --git a/lib/EpdFont/scripts/fontconvert.py b/lib/EpdFont/scripts/fontconvert.py index beb7d2bb..69b06c26 100755 --- a/lib/EpdFont/scripts/fontconvert.py +++ b/lib/EpdFont/scripts/fontconvert.py @@ -726,6 +726,12 @@ if compress: # are grouped together for efficient LRU caching on the embedded target. # Since glyphs are in codepoint order, glyphs in the same Unicode block # are contiguous in the array and form natural groups. + # + # A hard size cap (GROUP_MAX_UNCOMPRESSED_BYTES) is applied on top of script + # boundaries: if adding the next glyph would push the uncompressed group size + # over the cap, the group is closed and a new one started with the same script + # ID. This keeps the embedded decompressor's transient malloc bounded regardless + # of font density (CJK, Vietnamese, user-supplied fonts with large Unicode blocks). SCRIPT_GROUP_RANGES = [ (0x0000, 0x007F), # ASCII (0x0080, 0x00FF), # Latin-1 Supplement @@ -743,6 +749,10 @@ if compress: (0xFFFD, 0xFFFD), # Replacement Character ] + # 64 KB cap: large enough to hold any single built-in font group with headroom, + # small enough to be a comfortable transient malloc on the ESP32-C3. + GROUP_MAX_UNCOMPRESSED_BYTES = 65536 + def get_script_group(code_point): for i, (start, end) in enumerate(SCRIPT_GROUP_RANGES): if start <= code_point <= end: @@ -753,17 +763,28 @@ if compress: current_group_id = None group_start = 0 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) - if sg != current_group_id: + 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: if group_count > 0: groups.append((group_start, group_count)) current_group_id = sg group_start = i group_count = 1 + group_uncompressed = glyph_aligned_size else: group_count += 1 + group_uncompressed += glyph_aligned_size if group_count > 0: groups.append((group_start, group_count)) diff --git a/lib/Epub/Epub/ParsedText.cpp b/lib/Epub/Epub/ParsedText.cpp index 40a125e4..f24550c7 100644 --- a/lib/Epub/Epub/ParsedText.cpp +++ b/lib/Epub/Epub/ParsedText.cpp @@ -17,6 +17,34 @@ 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. 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 '.': + case ',': + case '!': + case '?': + case ':': + case ';': + case ')': + case ']': + case '}': + case 0x00BB: // » + case 0x203A: // › + case 0x2019: // ' right single quotation mark + case 0x201D: // " right double quotation mark + case 0x2026: // … ellipsis + case 0x2013: // – en dash + case 0x2014: // — em dash + return true; + default: + return false; + } +} + // Soft hyphen byte pattern used throughout EPUBs (UTF-8 for U+00AD). constexpr char SOFT_HYPHEN_UTF8[] = "\xC2\xAD"; constexpr size_t SOFT_HYPHEN_BYTES = 2; @@ -330,11 +358,19 @@ std::vector ParsedText::computeLineBreaks(const GfxRenderer& renderer, c int cost; if (j == totalWordCount - 1) { - cost = 0; // Last line + cost = 0; // Last line — no penalty regardless of looseness } else { const int remainingSpace = effectivePageWidth - currlen; - // Use long long for the square to prevent overflow - const long long cost_ll = static_cast(remainingSpace) * remainingSpace + dp[j + 1]; + // Knuth-Plass style demerits: + // badness = (gap/lineWidth)³ × 10000, clamped to [0, 10000] + // demerits = (1 + badness)² + // Cubic badness strongly penalises very loose lines while being + // lenient on moderately loose ones, producing visually balanced paragraphs. + const long long b_num = static_cast(remainingSpace) * remainingSpace * remainingSpace; + const long long b_den = static_cast(effectivePageWidth) * effectivePageWidth * effectivePageWidth; + const int badness = (b_den > 0) ? static_cast(std::min(b_num * 10000LL / b_den, 10000LL)) : 10000; + const long long demerits = static_cast(1 + badness) * (1 + badness); + const long long cost_ll = demerits + dp[j + 1]; if (cost_ll > MAX_COST) { cost = MAX_COST; @@ -767,17 +803,19 @@ ParsedText::LineProcessResult ParsedText::extractLine( for (size_t wordIdx = 0; wordIdx < lineWordCount; wordIdx++) { lineWordWidthSum += wordWidths[lastBreakAt + wordIdx]; - // Count gaps: each word after the first creates a gap, unless it's a continuation + // 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]) { - actualGapCount++; - totalNaturalGaps += - renderer.getSpaceAdvance(fontId, lastCodepoint(words[lastBreakAt + wordIdx - 1]), - firstCodepoint(words[lastBreakAt + wordIdx]), wordStyles[lastBreakAt + wordIdx - 1]); + const bool beforeClosing = isClosingPunctuation(firstCp); + if (!beforeClosing) actualGapCount++; + 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]); } } @@ -822,12 +860,15 @@ 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]); - } - if (blockStyle.alignment == CssTextAlign::Justify && !isLastLine) { - gap += justifyExtra; + // Don't stretch the gap before closing punctuation — it looks wrong with + // extra space before ".", ")", "»" etc. + const bool nextIsClosing = isClosingPunctuation(nextFirstCp); + if (blockStyle.alignment == CssTextAlign::Justify && !isLastLine && !nextIsClosing) { + gap += justifyExtra; + } } xpos += wordWidths[lastBreakAt + wordIdx] + gap; } diff --git a/src/activities/reader/EpubReaderActivity.cpp b/src/activities/reader/EpubReaderActivity.cpp index 1db9674e..cca2e9b0 100644 --- a/src/activities/reader/EpubReaderActivity.cpp +++ b/src/activities/reader/EpubReaderActivity.cpp @@ -824,8 +824,7 @@ std::string EpubReaderActivity::buildRenderBenchmarkReport(const LastRenderStats std::to_string(endSnapshot.fontDecompressMs) + " ms, groups " + std::to_string(endSnapshot.fontUniqueGroups)); appendLine("Font buffers: page " + std::to_string(endSnapshot.fontPageBufferBytes) + ", glyph table " + - std::to_string(endSnapshot.fontPageGlyphsBytes) + ", hot group " + - std::to_string(endSnapshot.fontHotGroupBytes) + ", peak temp " + + std::to_string(endSnapshot.fontPageGlyphsBytes) + ", peak temp " + std::to_string(endSnapshot.fontPeakTempBytes)); appendLine("Glyph lookups: " + std::to_string(endSnapshot.fontGetBitmapCalls) + " calls, " + std::to_string(endSnapshot.fontGetBitmapTimeUs) + " us total"); @@ -1120,7 +1119,7 @@ int EpubReaderActivity::getEffectiveReaderFontId() const { } bool EpubReaderActivity::stepPageState(const bool isForwardTurn) { - if (!epub || !section || section->pageCount <= 0) { + if (!epub || !section || section->pageCount == 0) { return false; } @@ -1566,8 +1565,8 @@ void EpubReaderActivity::renderContents(std::unique_ptr page, const int or tEnd - t0); } - if (auto* cacheManager = renderer.getFontCacheManager()) { - if (auto* decompressor = cacheManager->getDecompressor()) { + if (const auto* cacheManager = renderer.getFontCacheManager()) { + if (const auto* decompressor = cacheManager->getDecompressor()) { const auto& stats = decompressor->getStats(); lastRenderStats.fontCacheHits = stats.cacheHits; lastRenderStats.fontCacheMisses = stats.cacheMisses; @@ -1575,7 +1574,6 @@ void EpubReaderActivity::renderContents(std::unique_ptr page, const int or lastRenderStats.fontUniqueGroups = stats.uniqueGroupsAccessed; lastRenderStats.fontPageBufferBytes = stats.pageBufferBytes; lastRenderStats.fontPageGlyphsBytes = stats.pageGlyphsBytes; - lastRenderStats.fontHotGroupBytes = stats.hotGroupBytes; lastRenderStats.fontPeakTempBytes = stats.peakTempBytes; lastRenderStats.fontGetBitmapTimeUs = stats.getBitmapTimeUs; lastRenderStats.fontGetBitmapCalls = stats.getBitmapCalls; diff --git a/src/activities/reader/EpubReaderActivity.h b/src/activities/reader/EpubReaderActivity.h index 1a43f58d..8185139f 100644 --- a/src/activities/reader/EpubReaderActivity.h +++ b/src/activities/reader/EpubReaderActivity.h @@ -85,7 +85,6 @@ class EpubReaderActivity final : public Activity { uint16_t fontUniqueGroups = 0; uint32_t fontPageBufferBytes = 0; uint32_t fontPageGlyphsBytes = 0; - uint32_t fontHotGroupBytes = 0; uint32_t fontPeakTempBytes = 0; uint32_t fontGetBitmapTimeUs = 0; uint32_t fontGetBitmapCalls = 0;