#include "SdCardFont.h" #include #include #include #include #include #include #include #include "EpdFontFamily.h" static_assert(sizeof(EpdGlyph) == 16, "EpdGlyph must be 16 bytes to match .cpfont file layout"); static_assert(sizeof(EpdUnicodeInterval) == 12, "EpdUnicodeInterval must be 12 bytes to match .cpfont file layout"); static_assert(sizeof(EpdKernClassEntry) == 3, "EpdKernClassEntry must be 3 bytes to match .cpfont file layout"); static_assert(sizeof(EpdLigaturePair) == 8, "EpdLigaturePair must be 8 bytes to match .cpfont file layout"); namespace { // FNV-1a hash for content-based font ID generation constexpr uint32_t FNV_OFFSET = 2166136261u; constexpr uint32_t FNV_PRIME = 16777619u; uint32_t fnv1a(const uint8_t* data, size_t len, uint32_t hash = FNV_OFFSET) { for (size_t i = 0; i < len; i++) { hash ^= data[i]; hash *= FNV_PRIME; } return hash; } // .cpfont magic bytes constexpr char CPFONT_MAGIC[8] = {'C', 'P', 'F', 'O', 'N', 'T', '\0', '\0'}; // CPFONT_VERSION is defined as a #define in SdCardFont.h so it can be // stringified into FONT_MANIFEST_URL. constexpr uint32_t HEADER_SIZE = 32; constexpr uint32_t STYLE_TOC_ENTRY_SIZE = 32; // Helper to read little-endian values from byte buffer inline uint16_t readU16(const uint8_t* p) { return p[0] | (p[1] << 8); } inline int16_t readI16(const uint8_t* p) { return static_cast(p[0] | (p[1] << 8)); } inline uint32_t readU32(const uint8_t* p) { return p[0] | (p[1] << 8) | (p[2] << 16) | (p[3] << 24); } // Walks a null-terminated UTF-8 string and appends each unique codepoint to // codepoints[0..cpCount-1] via O(n²) dedup. Returns true if the buffer // reached maxCount (cap hit), false if all codepoints fit. bool collectUniqueCodepoints(const char* text, uint32_t* codepoints, uint32_t& cpCount, uint32_t maxCount) { const unsigned char* p = reinterpret_cast(text); while (*p) { uint32_t cp = utf8NextCodepoint(&p); if (cp == 0) break; bool found = false; for (uint32_t i = 0; i < cpCount; i++) { if (codepoints[i] == cp) { found = true; break; } } if (!found) { if (cpCount >= maxCount) return true; codepoints[cpCount++] = cp; } } return false; } const char* asCStr(const std::string& s) { return s.c_str(); } const char* asCStr(const char* s) { return s; } } // namespace SdCardFont::~SdCardFont() { freeAll(); } // --- Per-style free/cleanup --- void SdCardFont::freeStyleMiniData(PerStyle& s) { delete[] s.miniIntervals; s.miniIntervals = nullptr; delete[] s.miniGlyphs; s.miniGlyphs = nullptr; delete[] s.miniBitmap; s.miniBitmap = nullptr; s.miniIntervalCount = 0; s.miniGlyphCount = 0; freeStyleMiniKern(s); memset(&s.miniData, 0, sizeof(s.miniData)); s.epdFont.data = &s.stubData; } void SdCardFont::freeStyleKernLigatureData(PerStyle& s) { delete[] s.kernLeftClasses; s.kernLeftClasses = nullptr; delete[] s.kernRightClasses; s.kernRightClasses = nullptr; delete[] s.ligaturePairs; s.ligaturePairs = nullptr; s.kernLigLoaded = false; } void SdCardFont::freeStyleMiniKern(PerStyle& s) { delete[] s.miniKernLeftClasses; s.miniKernLeftClasses = nullptr; delete[] s.miniKernRightClasses; s.miniKernRightClasses = nullptr; delete[] s.miniKernMatrix; s.miniKernMatrix = nullptr; s.miniKernLeftEntryCount = 0; s.miniKernRightEntryCount = 0; s.miniKernLeftClassCount = 0; s.miniKernRightClassCount = 0; } void SdCardFont::freeStyleAll(PerStyle& s) { freeStyleMiniData(s); delete[] s.fullIntervals; s.fullIntervals = nullptr; delete[] s.bmpIntervals; s.bmpIntervals = nullptr; s.intervalsAreBmp16 = false; freeStyleKernLigatureData(s); s.present = false; } // --- Global free/cleanup --- void SdCardFont::freeAll() { clearOverflow(); clearPersistentCache(); for (uint8_t i = 0; i < MAX_STYLES; i++) { freeStyleAll(styles_[i]); } styleCount_ = 0; contentHash_ = 0; loaded_ = false; } void SdCardFont::clearOverflow() { for (uint32_t i = 0; i < overflowCount_; i++) { delete[] overflow_[i].bitmap; overflow_[i].bitmap = nullptr; overflow_[i].codepoint = 0; } overflowCount_ = 0; overflowNext_ = 0; } // --- Per-style kern/ligature --- void SdCardFont::applyKernLigaturePointers(PerStyle& s, EpdFontData& data) const { // Kern data uses the per-page mini tables (renumbered class IDs). The full // kern matrix is never resident — see PerStyle::miniKernMatrix comment. data.kernLeftClasses = s.miniKernLeftClasses; data.kernRightClasses = s.miniKernRightClasses; data.kernMatrix = s.miniKernMatrix; data.kernLeftEntryCount = s.miniKernLeftEntryCount; data.kernRightEntryCount = s.miniKernRightEntryCount; data.kernLeftClassCount = s.miniKernLeftClassCount; data.kernRightClassCount = s.miniKernRightClassCount; // Ligatures are small (typically < 1KB) so they stay resident. data.ligaturePairs = s.ligaturePairs; data.ligaturePairCount = s.header.ligaturePairCount; } bool SdCardFont::loadStyleKernLigatureData(PerStyle& s) { if (s.kernLigLoaded) return true; bool hasKern = s.header.kernLeftEntryCount > 0; bool hasLig = s.header.ligaturePairCount > 0; if (!hasKern && !hasLig) { s.kernLigLoaded = true; return true; } HalFile file; if (!Storage.openFileForRead("SDCF", filePath_, file)) { LOG_ERR("SDCF", "Failed to open .cpfont for kern/lig: %s", filePath_); return false; } if (hasKern) { // Load only the small class-lookup tables (~3KB each). The full matrix // (~36KB contiguous for Literata) is built per-page from SD in // buildMiniKernMatrix(). s.kernLeftClasses = new (std::nothrow) EpdKernClassEntry[s.header.kernLeftEntryCount]; s.kernRightClasses = new (std::nothrow) EpdKernClassEntry[s.header.kernRightEntryCount]; if (!s.kernLeftClasses || !s.kernRightClasses) { LOG_ERR("SDCF", "Failed to allocate kern classes (%u+%u bytes)", s.header.kernLeftEntryCount * 3u, s.header.kernRightEntryCount * 3u); freeStyleKernLigatureData(s); return false; } if (!file.seekSet(s.kernLeftFileOffset)) { LOG_ERR("SDCF", "Failed to seek to kern data"); freeStyleKernLigatureData(s); return false; } size_t leftSz = s.header.kernLeftEntryCount * sizeof(EpdKernClassEntry); size_t rightSz = s.header.kernRightEntryCount * sizeof(EpdKernClassEntry); if (file.read(reinterpret_cast(s.kernLeftClasses), leftSz) != static_cast(leftSz) || file.read(reinterpret_cast(s.kernRightClasses), rightSz) != static_cast(rightSz)) { LOG_ERR("SDCF", "Failed to read kern classes"); freeStyleKernLigatureData(s); return false; } } if (hasLig) { s.ligaturePairs = new (std::nothrow) EpdLigaturePair[s.header.ligaturePairCount]; if (!s.ligaturePairs) { LOG_ERR("SDCF", "Failed to allocate ligature pairs"); freeStyleKernLigatureData(s); return false; } if (!file.seekSet(s.ligatureFileOffset)) { LOG_ERR("SDCF", "Failed to seek to ligature data"); freeStyleKernLigatureData(s); return false; } size_t sz = s.header.ligaturePairCount * sizeof(EpdLigaturePair); if (file.read(reinterpret_cast(s.ligaturePairs), sz) != static_cast(sz)) { LOG_ERR("SDCF", "Failed to read ligature pairs"); freeStyleKernLigatureData(s); return false; } } s.kernLigLoaded = 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; LOG_DBG("SDCF", "Kern classes + lig loaded: kernL=%u, kernR=%u, ligs=%u", s.header.kernLeftEntryCount, s.header.kernRightEntryCount, s.header.ligaturePairCount); return true; } // --- Per-page mini kern matrix --- // Local copy of EpdFont.cpp's lookupKernClass (that one is file-static there). // Returns the 1-based class ID for `cp`, or 0 if the codepoint has no kerning class. static uint8_t miniLookupKernClass(const EpdKernClassEntry* entries, uint16_t count, uint32_t cp) { if (!entries || count == 0 || cp > 0xFFFF) return 0; const auto target = static_cast(cp); const auto* end = entries + count; const auto it = std::lower_bound(entries, end, target, [](const EpdKernClassEntry& e, uint16_t v) { return e.codepoint < v; }); return (it != end && it->codepoint == target) ? it->classId : 0; } // Build a small per-page kern matrix containing ONLY the (leftClass, rightClass) // pairs reachable from codepoints in the current text. Class IDs are renumbered // to a dense 1..N range so the resulting matrix is usedLeft × usedRight (typical // Latin page: ~25×25 bytes) instead of the font's full ~180×200 (~36KB). // // Correctness: EpdFont::getKerning only touches `kernLeftClasses` / // `kernRightClasses` / `kernMatrix` / the count fields — we swap all of them to // the mini versions together in applyKernLigaturePointers, so a codepoint not // on this page simply returns class 0 (no kerning), which was the pre-existing // behavior for any codepoint outside the kern classes. bool SdCardFont::buildMiniKernMatrix(PerStyle& s, const uint32_t* codepoints, uint32_t cpCount) { freeStyleMiniKern(s); if (!s.kernLeftClasses || !s.kernRightClasses || s.header.kernLeftEntryCount == 0 || s.header.kernRightEntryCount == 0) { return true; // font has no kern classes — nothing to build } // Step 1: mark used left/right classes via a 256-wide bitmap (class IDs are uint8_t). bool usedLeft[256] = {}; bool usedRight[256] = {}; for (uint32_t i = 0; i < cpCount; i++) { uint8_t lc = miniLookupKernClass(s.kernLeftClasses, s.header.kernLeftEntryCount, codepoints[i]); if (lc) usedLeft[lc] = true; uint8_t rc = miniLookupKernClass(s.kernRightClasses, s.header.kernRightEntryCount, codepoints[i]); if (rc) usedRight[rc] = true; } // Step 2: build renumber maps (oldClassId -> newClassId, 1-based) and // reverse maps (newClassId -> oldClassId) for the SD read step. uint8_t leftRenumber[256] = {}; uint8_t rightRenumber[256] = {}; uint8_t newToOldLeft[256] = {}; uint8_t newToOldRight[256] = {}; uint8_t numLeft = 0, numRight = 0; for (int i = 1; i < 256; i++) { if (usedLeft[i]) { numLeft++; leftRenumber[i] = numLeft; newToOldLeft[numLeft] = static_cast(i); } if (usedRight[i]) { numRight++; rightRenumber[i] = numRight; newToOldRight[numRight] = static_cast(i); } } if (numLeft == 0 || numRight == 0) { return true; // no kern pairs applicable on this page } // Step 3: count how many codepoint→classId entries the mini class tables need. // Each resident class table has one entry per kerned codepoint in the page. uint16_t miniLeftCount = 0; uint16_t miniRightCount = 0; for (uint32_t i = 0; i < cpCount; i++) { if (miniLookupKernClass(s.kernLeftClasses, s.header.kernLeftEntryCount, codepoints[i]) != 0) miniLeftCount++; if (miniLookupKernClass(s.kernRightClasses, s.header.kernRightEntryCount, codepoints[i]) != 0) miniRightCount++; } // Step 4: allocate the three mini buffers. The matrix is <1KB in practice // (<30 × <30 × 1 byte) so fragmentation is a non-issue. const uint32_t matrixBytes = static_cast(numLeft) * numRight; s.miniKernLeftClasses = new (std::nothrow) EpdKernClassEntry[miniLeftCount]; s.miniKernRightClasses = new (std::nothrow) EpdKernClassEntry[miniRightCount]; s.miniKernMatrix = new (std::nothrow) int8_t[matrixBytes]; if (!s.miniKernLeftClasses || !s.miniKernRightClasses || !s.miniKernMatrix) { LOG_ERR("SDCF", "Failed to allocate mini kern (%u+%u+%u bytes)", miniLeftCount * 3u, miniRightCount * 3u, matrixBytes); freeStyleMiniKern(s); return false; } // Step 5: populate mini class tables. `codepoints` is already sorted (see // prewarm()) so the output is sorted by codepoint — required for binary // search in lookupKernClass during render. uint16_t lIdx = 0, rIdx = 0; for (uint32_t i = 0; i < cpCount; i++) { uint32_t cp = codepoints[i]; if (cp > 0xFFFF) continue; // kern class entries are uint16_t uint8_t lc = miniLookupKernClass(s.kernLeftClasses, s.header.kernLeftEntryCount, cp); if (lc) { s.miniKernLeftClasses[lIdx].codepoint = static_cast(cp); s.miniKernLeftClasses[lIdx].classId = leftRenumber[lc]; lIdx++; } uint8_t rc = miniLookupKernClass(s.kernRightClasses, s.header.kernRightEntryCount, cp); if (rc) { s.miniKernRightClasses[rIdx].codepoint = static_cast(cp); s.miniKernRightClasses[rIdx].classId = rightRenumber[rc]; rIdx++; } } // Step 6: read the full matrix's rows for each used left class, keep only // columns for used right classes. One SD seek + one read per used left class; // a row is kernRightClassCount bytes (~200 for Literata). HalFile file; if (!Storage.openFileForRead("SDCF", filePath_, file)) { LOG_ERR("SDCF", "Failed to open .cpfont for mini kern: %s", filePath_); freeStyleMiniKern(s); return false; } std::unique_ptr rowBuf(new (std::nothrow) int8_t[s.header.kernRightClassCount]); if (!rowBuf) { LOG_ERR("SDCF", "Failed to allocate row buffer (%u bytes)", s.header.kernRightClassCount); freeStyleMiniKern(s); return false; } for (uint8_t newL = 1; newL <= numLeft; newL++) { const uint8_t oldL = newToOldLeft[newL]; const uint32_t rowFileOff = s.kernMatrixFileOffset + (oldL - 1u) * s.header.kernRightClassCount; if (!file.seekSet(rowFileOff)) { LOG_ERR("SDCF", "Failed to seek to kern row %u", oldL); freeStyleMiniKern(s); return false; } if (file.read(reinterpret_cast(rowBuf.get()), s.header.kernRightClassCount) != static_cast(s.header.kernRightClassCount)) { LOG_ERR("SDCF", "Failed to read kern row %u", oldL); freeStyleMiniKern(s); return false; } int8_t* miniRow = s.miniKernMatrix + (newL - 1u) * numRight; for (uint8_t newR = 1; newR <= numRight; newR++) { miniRow[newR - 1] = rowBuf[newToOldRight[newR] - 1u]; } } s.miniKernLeftEntryCount = lIdx; s.miniKernRightEntryCount = rIdx; s.miniKernLeftClassCount = numLeft; s.miniKernRightClassCount = numRight; LOG_DBG("SDCF", "Built mini kern: %u×%u matrix (%u bytes, full was %u×%u = %u bytes)", numLeft, numRight, matrixBytes, s.header.kernLeftClassCount, s.header.kernRightClassCount, static_cast(s.header.kernLeftClassCount) * s.header.kernRightClassCount); return true; } // --- Glyph miss callback --- void SdCardFont::applyGlyphMissCallback(uint8_t styleIdx) { overflowCtx_[styleIdx].self = this; overflowCtx_[styleIdx].styleIdx = styleIdx; auto& s = styles_[styleIdx]; s.stubData.glyphMissHandler = &SdCardFont::onGlyphMiss; s.stubData.glyphMissCtx = &overflowCtx_[styleIdx]; } // --- Compute per-style file offsets from a base data offset --- void SdCardFont::computeStyleFileOffsets(PerStyle& s, uint32_t baseOffset) { s.intervalsFileOffset = baseOffset; s.glyphsFileOffset = s.intervalsFileOffset + s.header.intervalCount * sizeof(EpdUnicodeInterval); s.kernLeftFileOffset = s.glyphsFileOffset + s.header.glyphCount * sizeof(EpdGlyph); s.kernRightFileOffset = s.kernLeftFileOffset + s.header.kernLeftEntryCount * sizeof(EpdKernClassEntry); s.kernMatrixFileOffset = s.kernRightFileOffset + s.header.kernRightEntryCount * sizeof(EpdKernClassEntry); s.ligatureFileOffset = s.kernMatrixFileOffset + static_cast(s.header.kernLeftClassCount) * s.header.kernRightClassCount; s.bitmapFileOffset = s.ligatureFileOffset + s.header.ligaturePairCount * sizeof(EpdLigaturePair); } // --- Load --- bool SdCardFont::load(const char* path) { freeAll(); if (strlen(path) >= sizeof(filePath_)) { LOG_ERR("SDCF", "Path too long (%zu bytes, max %zu)", strlen(path), sizeof(filePath_) - 1); return false; } strncpy(filePath_, path, sizeof(filePath_) - 1); filePath_[sizeof(filePath_) - 1] = '\0'; HalFile file; if (!Storage.openFileForRead("SDCF", path, file)) { LOG_ERR("SDCF", "Failed to open .cpfont: %s", path); return false; } // Read and validate global header uint8_t headerBuf[HEADER_SIZE]; if (file.read(headerBuf, HEADER_SIZE) != HEADER_SIZE) { LOG_ERR("SDCF", "Failed to read header"); return false; } if (memcmp(headerBuf, CPFONT_MAGIC, 8) != 0) { LOG_ERR("SDCF", "Invalid magic bytes"); return false; } uint16_t fileVersion = readU16(headerBuf + 8); if (fileVersion != CPFONT_VERSION) { LOG_ERR("SDCF", "Unsupported version: %u (expected %u)", fileVersion, CPFONT_VERSION); return false; } // Begin content hash: accumulate global header uint32_t hash = fnv1a(headerBuf, HEADER_SIZE); bool is2Bit = (readU16(headerBuf + 10) & 1) != 0; uint8_t styleCount = headerBuf[12]; if (styleCount == 0 || styleCount > MAX_STYLES) { LOG_ERR("SDCF", "Invalid style count: %u", styleCount); return false; } // Read style TOC for (uint8_t i = 0; i < styleCount; i++) { uint8_t tocBuf[STYLE_TOC_ENTRY_SIZE]; if (file.read(tocBuf, STYLE_TOC_ENTRY_SIZE) != STYLE_TOC_ENTRY_SIZE) { LOG_ERR("SDCF", "Failed to read style TOC entry %u", i); freeAll(); return false; } // Accumulate TOC entry into content hash hash = fnv1a(tocBuf, STYLE_TOC_ENTRY_SIZE, hash); uint8_t styleId = tocBuf[0]; if (styleId >= MAX_STYLES) { LOG_ERR("SDCF", "Invalid styleId %u in TOC", styleId); file.close(); freeAll(); return false; } auto& s = styles_[styleId]; s.present = true; s.header.intervalCount = readU32(tocBuf + 4); s.header.glyphCount = readU32(tocBuf + 8); s.header.advanceY = tocBuf[12]; s.header.ascender = readI16(tocBuf + 13); s.header.descender = readI16(tocBuf + 15); s.header.kernLeftEntryCount = readU16(tocBuf + 17); s.header.kernRightEntryCount = readU16(tocBuf + 19); s.header.kernLeftClassCount = tocBuf[21]; s.header.kernRightClassCount = tocBuf[22]; s.header.ligaturePairCount = tocBuf[23]; s.header.is2Bit = is2Bit; // Sanity-check counts to reject malformed files before allocating. // Kern class counts are uint8 (bounded by type). Entry counts are uint16 // but in practice a sane font has far fewer than 4096 per-side kern entries. static constexpr uint32_t MAX_INTERVALS = 4096; static constexpr uint32_t MAX_GLYPHS = 65536; static constexpr uint32_t MAX_KERN_ENTRIES = 4096; if (s.header.intervalCount > MAX_INTERVALS || s.header.glyphCount > MAX_GLYPHS || s.header.kernLeftEntryCount > MAX_KERN_ENTRIES || s.header.kernRightEntryCount > MAX_KERN_ENTRIES) { LOG_ERR("SDCF", "Style %u: unreasonable counts (iv=%u, gl=%u, kL=%u, kR=%u)", styleId, s.header.intervalCount, s.header.glyphCount, s.header.kernLeftEntryCount, s.header.kernRightEntryCount); file.close(); freeAll(); return false; } uint32_t dataOffset = readU32(tocBuf + 24); computeStyleFileOffsets(s, dataOffset); } styleCount_ = styleCount; contentHash_ = hash; // Load full intervals into RAM for each present style. BMP-only fonts with // fewer than 65536 glyphs use a compact 6-byte interval table instead of the // on-disk 12-byte table; large sparse CJK subsets otherwise keep tens of KB // of always-resident heap just for lookup metadata. for (uint8_t i = 0; i < MAX_STYLES; i++) { auto& s = styles_[i]; if (!s.present) continue; if (!file.seekSet(s.intervalsFileOffset)) { LOG_ERR("SDCF", "Failed to seek to intervals for style %u", i); freeAll(); return false; } // Validate interval contents before any later code (findGlobalGlyphIndex, // glyph reads) trusts them. A malformed file could otherwise drive // out-of-range glyph indices into bogus on-disk reads. bool canUseBmp16 = s.header.glyphCount <= UINT16_MAX; uint32_t expectedOffset = 0; uint32_t prevLast = 0; EpdUnicodeInterval iv{}; for (uint32_t j = 0; j < s.header.intervalCount; ++j) { if (file.read(reinterpret_cast(&iv), sizeof(iv)) != sizeof(iv)) { LOG_ERR("SDCF", "Failed to read interval %u for style %u", j, i); freeAll(); return false; } if (iv.first > iv.last) { LOG_ERR("SDCF", "Style %u: invalid interval %u (first 0x%lX > last 0x%lX)", i, j, static_cast(iv.first), static_cast(iv.last)); file.close(); freeAll(); return false; } const uint32_t span = iv.last - iv.first + 1; const bool overlapsPrev = (j > 0 && iv.first <= prevLast); const bool spanTooBig = (span > s.header.glyphCount); const bool offsetMismatch = (iv.offset != expectedOffset); const bool offsetOverruns = (iv.offset > s.header.glyphCount - span); if (overlapsPrev || spanTooBig || offsetMismatch || offsetOverruns) { LOG_ERR("SDCF", "Style %u: invalid interval layout at %u (overlap=%d span=%u offMis=%d offOver=%d)", i, j, overlapsPrev, span, offsetMismatch, offsetOverruns); file.close(); freeAll(); return false; } if (iv.first > UINT16_MAX || iv.last > UINT16_MAX || iv.offset > UINT16_MAX) { canUseBmp16 = false; } expectedOffset += span; prevLast = iv.last; } if (!file.seekSet(s.intervalsFileOffset)) { LOG_ERR("SDCF", "Failed to seek back to intervals for style %u", i); freeAll(); return false; } if (canUseBmp16) { s.bmpIntervals = new (std::nothrow) PerStyle::BmpInterval16[s.header.intervalCount]; if (!s.bmpIntervals) { LOG_ERR("SDCF", "Failed to allocate compact intervals for style %u", i); freeAll(); return false; } for (uint32_t j = 0; j < s.header.intervalCount; ++j) { if (file.read(reinterpret_cast(&iv), sizeof(iv)) != sizeof(iv)) { LOG_ERR("SDCF", "Failed to read compact interval %u for style %u", j, i); freeAll(); return false; } s.bmpIntervals[j] = {static_cast(iv.first), static_cast(iv.last), static_cast(iv.offset)}; } s.intervalsAreBmp16 = true; } else { s.fullIntervals = new (std::nothrow) EpdUnicodeInterval[s.header.intervalCount]; if (!s.fullIntervals) { LOG_ERR("SDCF", "Failed to allocate %u intervals for style %u", s.header.intervalCount, i); freeAll(); return false; } size_t intervalsBytes = s.header.intervalCount * sizeof(EpdUnicodeInterval); if (file.read(reinterpret_cast(s.fullIntervals), intervalsBytes) != static_cast(intervalsBytes)) { LOG_ERR("SDCF", "Failed to read intervals for style %u", i); freeAll(); return false; } } // Initialize stub data memset(&s.stubData, 0, sizeof(s.stubData)); s.stubData.advanceY = s.header.advanceY; s.stubData.ascender = s.header.ascender; s.stubData.descender = s.header.descender; s.stubData.is2Bit = s.header.is2Bit; s.epdFont.data = &s.stubData; applyGlyphMissCallback(i); } loaded_ = true; LOG_DBG("SDCF", "Loaded: %s (v%u, %u styles)", path, CPFONT_VERSION, styleCount_); for (uint8_t i = 0; i < MAX_STYLES; i++) { if (!styles_[i].present) continue; const auto& h = styles_[i].header; LOG_DBG("SDCF", " style[%u]: %u intervals, %u glyphs, advY=%u, asc=%d, desc=%d, kernL=%u, kernR=%u, ligs=%u", i, h.intervalCount, h.glyphCount, h.advanceY, h.ascender, h.descender, h.kernLeftEntryCount, h.kernRightEntryCount, h.ligaturePairCount); } return true; } // --- Codepoint lookup --- int32_t SdCardFont::findGlobalGlyphIndex(const PerStyle& s, uint32_t codepoint) const { int left = 0; int right = static_cast(s.header.intervalCount) - 1; while (left <= right) { int mid = left + (right - left) / 2; const uint32_t first = s.intervalsAreBmp16 ? s.bmpIntervals[mid].first : s.fullIntervals[mid].first; const uint32_t last = s.intervalsAreBmp16 ? s.bmpIntervals[mid].last : s.fullIntervals[mid].last; if (codepoint < first) { right = mid - 1; } else if (codepoint > last) { left = mid + 1; } else { const uint32_t offset = s.intervalsAreBmp16 ? s.bmpIntervals[mid].offset : s.fullIntervals[mid].offset; return static_cast(offset + (codepoint - first)); } } return -1; } // --- Prewarm --- int SdCardFont::prewarm(const char* utf8Text, uint8_t styleMask, bool metadataOnly) { if (!loaded_) return -1; styleMask = resolveStyleMask(styleMask); if (styleMask == 0) return 0; unsigned long startMs = millis(); // Step 1: Extract unique codepoints from UTF-8 text (shared across all styles). // Dedup uses O(n^2) linear scan — worst case is MAX_PAGE_GLYPHS (512) unique codepoints // = ~131K comparisons, but in practice pages contain far fewer unique codepoints so the // actual cost is much lower. This is dwarfed by SD I/O that follows. Alternatives (hash // set, bitmap) exceed the 256-byte stack limit or add template bloat. // Heap-allocated: MAX_PAGE_GLYPHS * 4 = 2048 bytes, too large for stack (limit < 256 bytes) std::unique_ptr codepoints(new (std::nothrow) uint32_t[MAX_PAGE_GLYPHS]); if (!codepoints) { LOG_ERR("SDCF", "Failed to allocate codepoint buffer (%u bytes)", MAX_PAGE_GLYPHS * 4); return -1; } uint32_t cpCount = 0; const unsigned char* p = reinterpret_cast(utf8Text); while (*p && cpCount < MAX_PAGE_GLYPHS) { uint32_t cp = utf8NextCodepoint(&p); if (cp == 0) break; bool found = false; for (uint32_t i = 0; i < cpCount; i++) { if (codepoints[i] == cp) { found = true; break; } } if (!found) { codepoints[cpCount++] = cp; } } // Always include the replacement character { bool hasReplacement = false; for (uint32_t i = 0; i < cpCount; i++) { if (codepoints[i] == REPLACEMENT_GLYPH) { hasReplacement = true; break; } } if (!hasReplacement && cpCount < MAX_PAGE_GLYPHS) { codepoints[cpCount++] = REPLACEMENT_GLYPH; } } // Add ligature output codepoints from all styles being prewarmed. // Skip during metadata-only prewarm (layout measurement) to avoid loading // kern/lig data for all styles upfront (~22KB per style). Kern/lig is // loaded per-style in prewarmStyle() during the full render prewarm instead. if (!metadataOnly) { for (uint8_t si = 0; si < MAX_STYLES; si++) { if (!(styleMask & (1 << si)) || !styles_[si].present) continue; auto& s = styles_[si]; loadStyleKernLigatureData(s); if (s.ligaturePairs && s.header.ligaturePairCount > 0) { for (uint8_t li = 0; li < s.header.ligaturePairCount && cpCount < MAX_PAGE_GLYPHS; li++) { uint32_t leftCp = s.ligaturePairs[li].pair >> 16; uint32_t rightCp = s.ligaturePairs[li].pair & 0xFFFF; uint32_t outCp = s.ligaturePairs[li].ligatureCp; bool hasLeft = false, hasRight = false; for (uint32_t i = 0; i < cpCount; i++) { if (codepoints[i] == leftCp) hasLeft = true; if (codepoints[i] == rightCp) hasRight = true; if (hasLeft && hasRight) break; } if (!hasLeft || !hasRight) continue; bool hasOut = false; for (uint32_t i = 0; i < cpCount; i++) { if (codepoints[i] == outCp) { hasOut = true; break; } } if (!hasOut) { codepoints[cpCount++] = outCp; } } } } } // Sort codepoints for ordered interval building std::sort(codepoints.get(), codepoints.get() + cpCount); // Prewarm each requested style int totalMissed = 0; for (uint8_t si = 0; si < MAX_STYLES; si++) { if (!(styleMask & (1 << si)) || !styles_[si].present) continue; totalMissed += prewarmStyle(si, codepoints.get(), cpCount, metadataOnly); } stats_.prewarmTotalMs = millis() - startMs; return totalMissed; } int SdCardFont::prewarmStyle(uint8_t styleIdx, const uint32_t* codepoints, uint32_t cpCount, bool metadataOnly) { auto& s = styles_[styleIdx]; // Map codepoints to global glyph indices for this style struct CpGlyphMapping { uint32_t codepoint; int32_t globalIndex; }; CpGlyphMapping* mappings = new (std::nothrow) CpGlyphMapping[cpCount]; if (!mappings) { LOG_ERR("SDCF", "Failed to allocate mapping array for style %u", styleIdx); return static_cast(cpCount); } uint32_t validCount = 0; for (uint32_t i = 0; i < cpCount; i++) { int32_t idx = findGlobalGlyphIndex(s, codepoints[i]); if (idx >= 0) { mappings[validCount].codepoint = codepoints[i]; mappings[validCount].globalIndex = idx; validCount++; } } int missed = static_cast(cpCount - validCount); if (validCount == 0) { freeStyleMiniData(s); delete[] mappings; s.epdFont.data = &s.stubData; return missed; } // Build mini intervals from sorted codepoints freeStyleMiniData(s); uint32_t intervalCapacity = validCount; s.miniIntervals = new (std::nothrow) EpdUnicodeInterval[intervalCapacity]; if (!s.miniIntervals) { LOG_ERR("SDCF", "Failed to allocate mini intervals for style %u", styleIdx); delete[] mappings; return static_cast(cpCount); } s.miniIntervalCount = 0; uint32_t rangeStart = 0; for (uint32_t i = 1; i <= validCount; i++) { if (i == validCount || mappings[i].codepoint != mappings[i - 1].codepoint + 1) { s.miniIntervals[s.miniIntervalCount].first = mappings[rangeStart].codepoint; s.miniIntervals[s.miniIntervalCount].last = mappings[i - 1].codepoint; s.miniIntervals[s.miniIntervalCount].offset = rangeStart; s.miniIntervalCount++; rangeStart = i; } } // Allocate mini glyph array s.miniGlyphCount = validCount; s.miniGlyphs = new (std::nothrow) EpdGlyph[s.miniGlyphCount]; if (!s.miniGlyphs) { LOG_ERR("SDCF", "Failed to allocate mini glyphs for style %u", styleIdx); delete[] mappings; freeStyleMiniData(s); return static_cast(cpCount); } // Build sorted read order for sequential I/O uint32_t* readOrder = new (std::nothrow) uint32_t[validCount]; if (!readOrder) { LOG_ERR("SDCF", "Failed to allocate read order for style %u", styleIdx); delete[] mappings; freeStyleMiniData(s); return static_cast(cpCount); } for (uint32_t i = 0; i < validCount; i++) readOrder[i] = i; std::sort(readOrder, readOrder + validCount, [&](uint32_t a, uint32_t b) { return mappings[a].globalIndex < mappings[b].globalIndex; }); HalFile file; if (!Storage.openFileForRead("SDCF", filePath_, file)) { LOG_ERR("SDCF", "Failed to reopen .cpfont for prewarm (style %u)", styleIdx); delete[] readOrder; delete[] mappings; freeStyleMiniData(s); return static_cast(cpCount); } unsigned long sdStart = millis(); uint32_t seekCount = 0; // Read glyph metadata. lastReadIndex tracks sequential reads to skip redundant // seeks; INT32_MIN guarantees the first iteration always seeks to the correct // offset (otherwise when gIdx == 0, the "gIdx != lastReadIndex + 1" check would // be false and we'd read from the file's current position — the header — which // decodes to a garbage EpdGlyph with a massive advanceX, inflating any word // containing that codepoint beyond page width). int32_t lastReadIndex = INT32_MIN; for (uint32_t i = 0; i < validCount; i++) { uint32_t mapIdx = readOrder[i]; int32_t gIdx = mappings[mapIdx].globalIndex; uint32_t fileOff = s.glyphsFileOffset + static_cast(gIdx) * sizeof(EpdGlyph); if (gIdx != lastReadIndex + 1) { if (!file.seekSet(fileOff)) { LOG_ERR("SDCF", "Prewarm: failed to seek to glyph %d (style %u)", gIdx, styleIdx); file.close(); delete[] readOrder; delete[] mappings; freeStyleMiniData(s); return static_cast(cpCount); } seekCount++; } if (file.read(reinterpret_cast(&s.miniGlyphs[mapIdx]), sizeof(EpdGlyph)) != sizeof(EpdGlyph)) { LOG_ERR("SDCF", "Prewarm: short glyph read (style %u, glyph %d)", styleIdx, gIdx); delete[] readOrder; delete[] mappings; freeStyleMiniData(s); return static_cast(cpCount); } lastReadIndex = gIdx; } uint32_t totalBitmapSize = 0; if (!metadataOnly) { // Compute total bitmap size for (uint32_t i = 0; i < validCount; i++) { totalBitmapSize += s.miniGlyphs[i].dataLength; } s.miniBitmap = new (std::nothrow) uint8_t[totalBitmapSize > 0 ? totalBitmapSize : 1]; if (!s.miniBitmap) { LOG_ERR("SDCF", "Failed to allocate mini bitmap (%u bytes) for style %u", totalBitmapSize, styleIdx); delete[] readOrder; delete[] mappings; freeStyleMiniData(s); return static_cast(cpCount); } // Read bitmap data sorted by file offset std::sort(readOrder, readOrder + validCount, [&](uint32_t a, uint32_t b) { return s.miniGlyphs[a].dataOffset < s.miniGlyphs[b].dataOffset; }); uint32_t miniBitmapOffset = 0; uint32_t lastBitmapEnd = UINT32_MAX; for (uint32_t i = 0; i < validCount; i++) { uint32_t mapIdx = readOrder[i]; EpdGlyph& glyph = s.miniGlyphs[mapIdx]; if (glyph.dataLength == 0) { glyph.dataOffset = miniBitmapOffset; continue; } uint32_t fileOff = s.bitmapFileOffset + glyph.dataOffset; if (fileOff != lastBitmapEnd) { if (!file.seekSet(fileOff)) { LOG_ERR("SDCF", "Prewarm: failed to seek to bitmap (style %u)", styleIdx); file.close(); delete[] readOrder; delete[] mappings; freeStyleMiniData(s); return static_cast(cpCount); } seekCount++; } if (file.read(s.miniBitmap + miniBitmapOffset, glyph.dataLength) != static_cast(glyph.dataLength)) { LOG_ERR("SDCF", "Prewarm: short bitmap read (style %u)", styleIdx); delete[] readOrder; delete[] mappings; freeStyleMiniData(s); return static_cast(cpCount); } lastBitmapEnd = fileOff + glyph.dataLength; glyph.dataOffset = miniBitmapOffset; miniBitmapOffset += glyph.dataLength; } } uint32_t sdTime = millis() - sdStart; delete[] readOrder; delete[] mappings; // Full render prewarm: load the persistent kern classes + ligatures (one-time // per style, small — the big matrix is NOT loaded here) and then build the // per-page mini kern matrix restricted to class pairs reachable from this // page's codepoints. Skip during metadata-only prewarm — layout only needs // advanceX and the mini kern would be thrown away before rendering. bool kernLigOk = false; if (!metadataOnly) { if (loadStyleKernLigatureData(s)) { kernLigOk = buildMiniKernMatrix(s, codepoints, cpCount); } } // Populate miniData and swap memset(&s.miniData, 0, sizeof(s.miniData)); s.miniData.bitmap = s.miniBitmap; s.miniData.glyph = s.miniGlyphs; s.miniData.intervals = s.miniIntervals; s.miniData.intervalCount = s.miniIntervalCount; s.miniData.advanceY = s.header.advanceY; s.miniData.ascender = s.header.ascender; s.miniData.descender = s.header.descender; s.miniData.is2Bit = s.header.is2Bit; if (kernLigOk) { applyKernLigaturePointers(s, s.miniData); } s.miniData.glyphMissHandler = &SdCardFont::onGlyphMiss; s.miniData.glyphMissCtx = &overflowCtx_[styleIdx]; s.epdFont.data = &s.miniData; // Accumulate stats stats_.sdReadTimeMs += sdTime; stats_.seekCount += seekCount; stats_.uniqueGlyphs += validCount; stats_.bitmapBytes += totalBitmapSize; return missed; } // --- Cache management --- void SdCardFont::clearCache() { clearOverflow(); // Note: advance table is intentionally preserved here. It persists across // layout passes so repeated section indexing amortizes SD reads. Use // clearPersistentCache() to wipe it. for (uint8_t i = 0; i < MAX_STYLES; i++) { if (!styles_[i].present) continue; freeStyleMiniData(styles_[i]); applyGlyphMissCallback(i); } } // --- Advance table --- void SdCardFont::clearPersistentCache() { for (uint8_t i = 0; i < MAX_STYLES; i++) { delete[] advanceTable_[i]; advanceTable_[i] = nullptr; advanceTableSize_[i] = 0; } } bool SdCardFont::advanceTableLookup(uint8_t styleIdx, uint32_t codepoint, uint16_t* outAdvance) const { const AdvanceEntry* table = advanceTable_[styleIdx]; const uint32_t size = advanceTableSize_[styleIdx]; if (!table || size == 0) return false; uint32_t lo = 0, hi = size; while (lo < hi) { uint32_t mid = lo + (hi - lo) / 2; if (table[mid].codepoint < codepoint) { lo = mid + 1; } else { hi = mid; } } if (lo < size && table[lo].codepoint == codepoint) { if (outAdvance) *outAdvance = table[lo].advanceX; return true; } return false; } void SdCardFont::mergeIntoAdvanceTable(uint8_t styleIdx, const AdvanceEntry* sortedNew, uint32_t newCount) { if (newCount == 0) return; const uint32_t oldSize = advanceTableSize_[styleIdx]; if (oldSize >= ADVANCE_CACHE_LIMIT) return; // already full // Cap the merged size at ADVANCE_CACHE_LIMIT. Anything past the cap is // dropped from the tail of the sorted merge — a deterministic, bounded loss // that doesn't bias which codepoints get cached on subsequent passes. uint32_t mergedCap = oldSize + newCount; if (mergedCap > ADVANCE_CACHE_LIMIT) mergedCap = ADVANCE_CACHE_LIMIT; AdvanceEntry* merged = new (std::nothrow) AdvanceEntry[mergedCap]; if (!merged) { LOG_ERR("SDCF", "mergeIntoAdvanceTable: alloc failed (%u entries) style %u", mergedCap, styleIdx); return; } const AdvanceEntry* a = advanceTable_[styleIdx]; const AdvanceEntry* b = sortedNew; uint32_t i = 0, j = 0, k = 0; while (k < mergedCap && (i < oldSize || j < newCount)) { if (i < oldSize && (j >= newCount || a[i].codepoint <= b[j].codepoint)) { merged[k++] = a[i++]; } else { merged[k++] = b[j++]; } } delete[] advanceTable_[styleIdx]; advanceTable_[styleIdx] = merged; advanceTableSize_[styleIdx] = k; } bool SdCardFont::hasGlyphMeta(uint8_t styleIdx, uint32_t codepoint) const { styleIdx &= (MAX_STYLES - 1); const PerStyle& s = styles_[styleIdx]; if (!loaded_ || !s.present) return false; return findGlobalGlyphIndex(s, codepoint) >= 0; } uint16_t SdCardFont::ensureAdvance(uint8_t styleIdx, uint32_t codepoint) { styleIdx &= (MAX_STYLES - 1); if (!loaded_ || !styles_[styleIdx].present) return 0; const uint16_t cached = getAdvance(codepoint, styleIdx); if (cached != 0) return cached; // 0 is either "not in table" or a genuine zero-width glyph; only pay the // SD read when the resident interval table says the glyph exists. if (findGlobalGlyphIndex(styles_[styleIdx], codepoint) < 0) return 0; uint32_t cp = codepoint; fetchAdvancesForCodepoints(&cp, 1, static_cast(1u << styleIdx)); return getAdvance(codepoint, styleIdx); } uint8_t SdCardFont::styleIdxFromMissCtx(void* ctx) { return static_cast(ctx)->styleIdx; } bool SdCardFont::hasAdvanceTable() const { for (uint8_t i = 0; i < MAX_STYLES; i++) { if (advanceTable_[i]) return true; } return false; } uint16_t SdCardFont::getAdvance(uint32_t codepoint, uint8_t style) const { style &= (MAX_STYLES - 1); if (!advanceTable_[style]) return 0; const AdvanceEntry* table = advanceTable_[style]; const uint32_t size = advanceTableSize_[style]; // Binary search sorted by codepoint uint32_t lo = 0, hi = size; while (lo < hi) { uint32_t mid = lo + (hi - lo) / 2; if (table[mid].codepoint < codepoint) { lo = mid + 1; } else { hi = mid; } } if (lo < size && table[lo].codepoint == codepoint) { return table[lo].advanceX; } return 0; } // Given a sorted array of unique codepoints, resolve glyph indices per style, // batch-read advanceX from SD, and merge into the persistent advance table. // Caller owns the codepoints buffer. int SdCardFont::fetchAdvancesForCodepoints(uint32_t* codepoints, uint32_t cpCount, uint8_t styleMask) { int totalMissed = 0; for (uint8_t si = 0; si < MAX_STYLES; si++) { if (!(styleMask & (1 << si)) || !styles_[si].present) continue; const auto& s = styles_[si]; // Stop fetching once the cache is full — further inserts would be dropped // by the merge anyway. The renderer fast path tolerates missing entries // (returns 0); the slow path is still correct for those codepoints. if (advanceTableSize_[si] >= ADVANCE_CACHE_LIMIT) continue; // For each codepoint in `codepoints`, skip those already cached, then // resolve to a glyph index. Build a parallel array sorted by glyph index // for sequential SD reads. struct CpIdx { uint32_t codepoint; int32_t glyphIndex; }; std::unique_ptr mappings(new (std::nothrow) CpIdx[cpCount]); if (!mappings) { LOG_ERR("SDCF", "buildAdvanceTable: failed to allocate mappings for style %u", si); totalMissed += cpCount; continue; } uint32_t needCount = 0; uint32_t missedThisStyle = 0; const int32_t replacementIdx = findGlobalGlyphIndex(s, REPLACEMENT_GLYPH); for (uint32_t i = 0; i < cpCount; i++) { const uint32_t cp = codepoints[i]; if (advanceTableLookup(si, cp, nullptr)) continue; // already cached int32_t idx = findGlobalGlyphIndex(s, cp); if (idx < 0) { if (replacementIdx < 0) { missedThisStyle++; continue; } idx = replacementIdx; } mappings[needCount].codepoint = cp; mappings[needCount].glyphIndex = idx; needCount++; } totalMissed += static_cast(missedThisStyle); if (needCount == 0) continue; // Sort by glyph index so SD reads are mostly sequential. std::sort(mappings.get(), mappings.get() + needCount, [](const CpIdx& a, const CpIdx& b) { return a.glyphIndex < b.glyphIndex; }); // Open file once and read advanceX for each needed glyph. HalFile file; if (!Storage.openFileForRead("SDCF", filePath_, file)) { LOG_ERR("SDCF", "buildAdvanceTable: failed to open .cpfont for style %u", si); continue; } std::unique_ptr staged(new (std::nothrow) AdvanceEntry[needCount]); if (!staged) { LOG_ERR("SDCF", "buildAdvanceTable: failed to allocate staging for style %u", si); file.close(); continue; } uint32_t fetched = 0; EpdGlyph tempGlyph; int32_t lastReadIndex = INT32_MIN; for (uint32_t i = 0; i < needCount; i++) { int32_t gIdx = mappings[i].glyphIndex; uint32_t fileOff = s.glyphsFileOffset + static_cast(gIdx) * sizeof(EpdGlyph); if (gIdx != lastReadIndex + 1) { if (!file.seekSet(fileOff)) { LOG_ERR("SDCF", "buildAdvanceTable: failed to seek to glyph %d (style %u)", gIdx, si); break; } } if (file.read(reinterpret_cast(&tempGlyph), sizeof(EpdGlyph)) != sizeof(EpdGlyph)) { LOG_ERR("SDCF", "buildAdvanceTable: short glyph read (style %u, glyph %d)", si, gIdx); break; } lastReadIndex = gIdx; staged[fetched].codepoint = mappings[i].codepoint; staged[fetched].advanceX = tempGlyph.advanceX; fetched++; } file.close(); if (fetched > 0) { // Sort staged by codepoint, then merge into the persistent table. std::sort(staged.get(), staged.get() + fetched, [](const AdvanceEntry& a, const AdvanceEntry& b) { return a.codepoint < b.codepoint; }); mergeIntoAdvanceTable(si, staged.get(), fetched); } LOG_DBG("SDCF", "Advance table style %u: +%u from SD, total=%u/%u", si, fetched, advanceTableSize_[si], ADVANCE_CACHE_LIMIT); } return totalMissed; } template int SdCardFont::buildAdvanceTableRange(Iter begin, Iter end, bool includeSpace, bool includeHyphen, uint8_t styleMask) { if (!loaded_) return -1; styleMask = resolveStyleMask(styleMask); if (styleMask == 0) return 0; unsigned long startMs = millis(); // +2 reserved slots for space and hyphen injected after the main scan. static constexpr uint32_t MAX_UNIQUE_CODEPOINTS = 4096; uint32_t* codepoints = new (std::nothrow) uint32_t[MAX_UNIQUE_CODEPOINTS + 2]; if (!codepoints) { LOG_ERR("SDCF", "buildAdvanceTable: failed to allocate codepoint buffer (%u bytes)", MAX_UNIQUE_CODEPOINTS * 4); return -1; } uint32_t cpCount = 0; bool hitCap = false; for (auto it = begin; it != end && !hitCap; ++it) { hitCap = collectUniqueCodepoints(asCStr(*it), codepoints, cpCount, MAX_UNIQUE_CODEPOINTS); } if (includeSpace && std::none_of(codepoints, codepoints + cpCount, [](uint32_t c) { return c == ' '; })) codepoints[cpCount++] = ' '; if (includeHyphen && std::none_of(codepoints, codepoints + cpCount, [](uint32_t c) { return c == '-'; })) codepoints[cpCount++] = '-'; if (hitCap) { LOG_ERR("SDCF", "buildAdvanceTable: unique codepoint cap (%u) hit, layout may be approximate", MAX_UNIQUE_CODEPOINTS); } std::sort(codepoints, codepoints + cpCount); int totalMissed = fetchAdvancesForCodepoints(codepoints, cpCount, styleMask); delete[] codepoints; stats_.prewarmTotalMs = millis() - startMs; return totalMissed; } int SdCardFont::buildAdvanceTable(const char* utf8Text, uint8_t styleMask) { return buildAdvanceTableRange(&utf8Text, &utf8Text + 1, false, false, styleMask); } int SdCardFont::buildAdvanceTable(const std::vector& words, bool includeHyphen, uint8_t styleMask) { return buildAdvanceTableRange(words.begin(), words.end(), words.size() > 1, includeHyphen, styleMask); } // --- Stats --- void SdCardFont::logStats(const char* label) { LOG_DBG("SDCF", "[%s] total=%ums sd_read=%ums seeks=%u glyphs=%u bitmap=%u bytes", label, stats_.prewarmTotalMs, stats_.sdReadTimeMs, stats_.seekCount, stats_.uniqueGlyphs, stats_.bitmapBytes); } void SdCardFont::resetStats() { stats_ = Stats{}; } // --- Public accessors --- EpdFont* SdCardFont::getEpdFont(uint8_t style) { style &= (MAX_STYLES - 1); if (!styles_[style].present) return nullptr; return &styles_[style].epdFont; } bool SdCardFont::hasStyle(uint8_t style) const { return styles_[style & (MAX_STYLES - 1)].present; } uint8_t SdCardFont::resolveStyle(uint8_t style) const { static const uint8_t kFallbacks[MAX_STYLES][MAX_STYLES] = { // REGULAR: REGULAR -> BOLD -> ITALIC -> BOLD_ITALIC {EpdFontFamily::REGULAR, EpdFontFamily::BOLD, EpdFontFamily::ITALIC, EpdFontFamily::BOLD_ITALIC}, // BOLD: BOLD -> REGULAR -> BOLD_ITALIC -> ITALIC {EpdFontFamily::BOLD, EpdFontFamily::REGULAR, EpdFontFamily::BOLD_ITALIC, EpdFontFamily::ITALIC}, // ITALIC: ITALIC -> REGULAR -> BOLD_ITALIC -> BOLD {EpdFontFamily::ITALIC, EpdFontFamily::REGULAR, EpdFontFamily::BOLD_ITALIC, EpdFontFamily::BOLD}, // BOLD_ITALIC: BOLD_ITALIC -> BOLD -> ITALIC -> REGULAR {EpdFontFamily::BOLD_ITALIC, EpdFontFamily::BOLD, EpdFontFamily::ITALIC, EpdFontFamily::REGULAR}, }; const uint8_t styleBits = style & (MAX_STYLES - 1); for (uint8_t candidate : kFallbacks[styleBits]) { if (styles_[candidate].present) return candidate; } return EpdFontFamily::REGULAR; } uint8_t SdCardFont::resolveStyleMask(uint8_t styleMask) const { uint8_t resolvedMask = 0; for (uint8_t si = 0; si < MAX_STYLES; si++) { if (styleMask & (1 << si)) { resolvedMask |= static_cast(1u << resolveStyle(si)); } } return resolvedMask; } // --- On-demand glyph loading (overflow buffer) --- const EpdGlyph* SdCardFont::onGlyphMiss(void* ctx, uint32_t codepoint) { auto* oc = static_cast(ctx); auto* self = oc->self; uint8_t styleIdx = oc->styleIdx; if (!self->loaded_ || styleIdx >= MAX_STYLES || !self->styles_[styleIdx].present) return nullptr; const auto& s = self->styles_[styleIdx]; if (!s.fullIntervals && !s.bmpIntervals) return nullptr; // Check overflow cache first (matching both codepoint and style) for (uint32_t i = 0; i < self->overflowCount_; i++) { if (self->overflow_[i].codepoint == codepoint && self->overflow_[i].styleIdx == styleIdx) { return &self->overflow_[i].glyph; } } // Look up global glyph index via full intervals int32_t globalIdx = self->findGlobalGlyphIndex(s, codepoint); if (globalIdx < 0) return nullptr; // Pick overflow slot (ring buffer). Read into temporaries first so the // existing slot stays valid if SD I/O fails. Bookkeeping (count/next) // is deferred until after all I/O succeeds to avoid inconsistent state. uint32_t slot = self->overflowNext_; bool wasAtCapacity = (self->overflowCount_ == OVERFLOW_CAPACITY); // Read glyph metadata into temporary HalFile file; if (!Storage.openFileForRead("SDCF", self->filePath_, file)) { LOG_ERR("SDCF", "Overflow: failed to open .cpfont"); return nullptr; } EpdGlyph tempGlyph = {}; uint32_t glyphFileOff = s.glyphsFileOffset + static_cast(globalIdx) * sizeof(EpdGlyph); if (!file.seekSet(glyphFileOff)) { LOG_ERR("SDCF", "Overflow: failed to seek to glyph for U+%04X style %u", codepoint, styleIdx); file.close(); return nullptr; } if (file.read(reinterpret_cast(&tempGlyph), sizeof(EpdGlyph)) != sizeof(EpdGlyph)) { LOG_ERR("SDCF", "Overflow: failed to read glyph metadata for U+%04X style %u", codepoint, styleIdx); return nullptr; } // Read bitmap data into temporary (if any) uint8_t* tempBitmap = nullptr; if (tempGlyph.dataLength > 0) { tempBitmap = new (std::nothrow) uint8_t[tempGlyph.dataLength]; if (!tempBitmap) { LOG_ERR("SDCF", "Overflow: failed to allocate %u bytes for U+%04X bitmap", tempGlyph.dataLength, codepoint); return nullptr; } if (!file.seekSet(s.bitmapFileOffset + tempGlyph.dataOffset)) { LOG_ERR("SDCF", "Overflow: failed to seek to bitmap for U+%04X", codepoint); delete[] tempBitmap; file.close(); return nullptr; } if (file.read(tempBitmap, tempGlyph.dataLength) != static_cast(tempGlyph.dataLength)) { LOG_ERR("SDCF", "Overflow: failed to read bitmap for U+%04X", codepoint); delete[] tempBitmap; return nullptr; } } // All reads succeeded — commit to slot and advance ring buffer if (wasAtCapacity) { delete[] self->overflow_[slot].bitmap; } else { self->overflowCount_++; } self->overflowNext_ = (slot + 1) % OVERFLOW_CAPACITY; self->overflow_[slot].glyph = tempGlyph; self->overflow_[slot].bitmap = tempBitmap; self->overflow_[slot].codepoint = codepoint; self->overflow_[slot].styleIdx = styleIdx; LOG_DBG("SDCF", "Overflow: loaded U+%04X style %u on demand (slot %u/%u)", codepoint, styleIdx, slot, OVERFLOW_CAPACITY); return &self->overflow_[slot].glyph; } bool SdCardFont::isOverflowGlyph(const EpdGlyph* glyph) const { for (uint32_t i = 0; i < overflowCount_; i++) { if (&overflow_[i].glyph == glyph) return true; } return false; } const uint8_t* SdCardFont::getOverflowBitmap(const EpdGlyph* glyph) const { for (uint32_t i = 0; i < overflowCount_; i++) { if (&overflow_[i].glyph == glyph) { return overflow_[i].bitmap; } } return nullptr; } SdCardFont* SdCardFont::fromMissCtx(void* ctx) { return static_cast(ctx)->self; }