Files
Crosspoint/lib/EpdFont/SdCardFont.cpp
T
jpirnayandCopilot c84b971455 Fix signature
Co-authored-by: Copilot <copilot@github.com>
2026-04-28 17:46:37 +02:00

1297 lines
46 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#include "SdCardFont.h"
#include <HalStorage.h>
#include <Logging.h>
#include <Utf8.h>
#include <algorithm>
#include <climits>
#include <cstring>
#include <memory>
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");
// FNV-1a hash for content-based font ID generation
static constexpr uint32_t FNV_OFFSET = 2166136261u;
static constexpr uint32_t FNV_PRIME = 16777619u;
static 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
static constexpr char CPFONT_MAGIC[8] = {'C', 'P', 'F', 'O', 'N', 'T', '\0', '\0'};
static constexpr uint16_t CPFONT_VERSION = 4;
static constexpr uint32_t HEADER_SIZE = 32;
static constexpr uint32_t STYLE_TOC_ENTRY_SIZE = 32;
// Helper to read little-endian values from byte buffer
static inline uint16_t readU16(const uint8_t* p) { return p[0] | (p[1] << 8); }
static inline int16_t readI16(const uint8_t* p) { return static_cast<int16_t>(p[0] | (p[1] << 8)); }
static inline uint32_t readU32(const uint8_t* p) { return p[0] | (p[1] << 8) | (p[2] << 16) | (p[3] << 24); }
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;
s.miniMode = PerStyle::MiniMode::NONE;
s.reportedMissCount = 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;
freeStyleKernLigatureData(s);
s.present = false;
}
// --- Global free/cleanup ---
void SdCardFont::freeAll() {
clearOverflow();
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;
}
FsFile 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);
file.close();
return false;
}
if (!file.seekSet(s.kernLeftFileOffset)) {
LOG_ERR("SDCF", "Failed to seek to kern data");
freeStyleKernLigatureData(s);
file.close();
return false;
}
size_t leftSz = s.header.kernLeftEntryCount * sizeof(EpdKernClassEntry);
size_t rightSz = s.header.kernRightEntryCount * sizeof(EpdKernClassEntry);
if (file.read(reinterpret_cast<uint8_t*>(s.kernLeftClasses), leftSz) != static_cast<int>(leftSz) ||
file.read(reinterpret_cast<uint8_t*>(s.kernRightClasses), rightSz) != static_cast<int>(rightSz)) {
LOG_ERR("SDCF", "Failed to read kern classes");
freeStyleKernLigatureData(s);
file.close();
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);
file.close();
return false;
}
if (!file.seekSet(s.ligatureFileOffset)) {
LOG_ERR("SDCF", "Failed to seek to ligature data");
freeStyleKernLigatureData(s);
file.close();
return false;
}
size_t sz = s.header.ligaturePairCount * sizeof(EpdLigaturePair);
if (file.read(reinterpret_cast<uint8_t*>(s.ligaturePairs), sz) != static_cast<int>(sz)) {
LOG_ERR("SDCF", "Failed to read ligature pairs");
freeStyleKernLigatureData(s);
file.close();
return false;
}
}
file.close();
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<uint16_t>(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<uint8_t>(i);
}
if (usedRight[i]) {
numRight++;
rightRenumber[i] = numRight;
newToOldRight[numRight] = static_cast<uint8_t>(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<uint32_t>(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<uint16_t>(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<uint16_t>(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).
FsFile 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<int8_t[]> rowBuf(new (std::nothrow) int8_t[s.header.kernRightClassCount]);
if (!rowBuf) {
LOG_ERR("SDCF", "Failed to allocate row buffer (%u bytes)", s.header.kernRightClassCount);
file.close();
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);
file.close();
freeStyleMiniKern(s);
return false;
}
if (file.read(reinterpret_cast<uint8_t*>(rowBuf.get()), s.header.kernRightClassCount) !=
static_cast<int>(s.header.kernRightClassCount)) {
LOG_ERR("SDCF", "Failed to read kern row %u", oldL);
file.close();
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];
}
}
file.close();
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<uint32_t>(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<uint32_t>(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';
FsFile 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");
file.close();
return false;
}
if (memcmp(headerBuf, CPFONT_MAGIC, 8) != 0) {
LOG_ERR("SDCF", "Invalid magic bytes");
file.close();
return false;
}
uint16_t fileVersion = readU16(headerBuf + 8);
if (fileVersion != CPFONT_VERSION) {
LOG_ERR("SDCF", "Unsupported version: %u (expected %u)", fileVersion, CPFONT_VERSION);
file.close();
return false;
}
// Begin content hash: accumulate global header.
// KNOWN LIMITATION: hash covers global header + per-style TOC only, not the
// payload sections (intervals / glyph metrics / kern / ligature / bitmap). A
// font edit that alters payload bytes without changing any TOC count would
// produce the same contentHash and could leave stale EPUB section caches.
// Acceptable in practice because generate-sd-fonts.sh regeneration almost
// always changes interval/glyph/kern counts; revisit if we see real-world
// mismatches.
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);
file.close();
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);
file.close();
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);
continue;
}
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
static constexpr uint32_t MAX_INTERVALS = 4096;
static constexpr uint32_t MAX_GLYPHS = 65536;
if (s.header.intervalCount > MAX_INTERVALS || s.header.glyphCount > MAX_GLYPHS) {
LOG_ERR("SDCF", "Style %u: unreasonable counts (intervals=%u, glyphs=%u)", styleId, s.header.intervalCount,
s.header.glyphCount);
s.present = false;
continue;
}
uint32_t dataOffset = readU32(tocBuf + 24);
computeStyleFileOffsets(s, dataOffset);
}
styleCount_ = styleCount;
contentHash_ = hash;
// Load full intervals into RAM for each present style
for (uint8_t i = 0; i < MAX_STYLES; i++) {
auto& s = styles_[i];
if (!s.present) continue;
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);
file.close();
freeAll();
return false;
}
if (!file.seekSet(s.intervalsFileOffset)) {
LOG_ERR("SDCF", "Failed to seek to intervals for style %u", i);
file.close();
freeAll();
return false;
}
size_t intervalsBytes = s.header.intervalCount * sizeof(EpdUnicodeInterval);
if (file.read(reinterpret_cast<uint8_t*>(s.fullIntervals), intervalsBytes) != static_cast<int>(intervalsBytes)) {
LOG_ERR("SDCF", "Failed to read intervals for style %u", i);
file.close();
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);
}
file.close();
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<int>(s.header.intervalCount) - 1;
while (left <= right) {
int mid = left + (right - left) / 2;
const auto& interval = s.fullIntervals[mid];
if (codepoint < interval.first) {
right = mid - 1;
} else if (codepoint > interval.last) {
left = mid + 1;
} else {
return static_cast<int32_t>(interval.offset + (codepoint - interval.first));
}
}
return -1;
}
// --- Prewarm ---
int SdCardFont::prewarm(const char* utf8Text, uint8_t styleMask, bool metadataOnly, bool loadKernLigatureData) {
if (!loaded_) return -1;
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<uint32_t[]> 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<const unsigned char*>(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.
// Load ligature metadata when either doing a full prewarm or when
// metadata-only layout measurement needs applyLigatures()/getKerning().
if (!metadataOnly || loadKernLigatureData) {
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, loadKernLigatureData);
}
stats_.prewarmTotalMs = millis() - startMs;
return totalMissed;
}
// Returns true iff every codepoint in `codepoints[0..cpCount)` is already covered
// by the style's current miniIntervals. O(cpCount * log intervalCount).
bool SdCardFont::allCpsCovered(const PerStyle& s, const uint32_t* codepoints, uint32_t cpCount) {
if (s.miniIntervalCount == 0) return false;
for (uint32_t i = 0; i < cpCount; i++) {
const uint32_t cp = codepoints[i];
// Binary search miniIntervals for the range containing cp.
int lo = 0, hi = static_cast<int>(s.miniIntervalCount) - 1;
bool found = false;
while (lo <= hi) {
int mid = (lo + hi) / 2;
const auto& iv = s.miniIntervals[mid];
if (cp < iv.first) {
hi = mid - 1;
} else if (cp > iv.last) {
lo = mid + 1;
} else {
found = true;
break;
}
}
if (!found) return false;
}
return true;
}
int SdCardFont::prewarmStyle(uint8_t styleIdx, const uint32_t* codepoints, uint32_t cpCount, bool metadataOnly,
bool loadKernLigatureData) {
auto& s = styles_[styleIdx];
// ---- Fast-path coverage check ----
// If the existing cache already covers all requested cps in a compatible mode,
// there's nothing to do. This is the dominant case during pagination after the
// first paragraph has populated the metadata cache.
if (s.miniMode != PerStyle::MiniMode::NONE && allCpsCovered(s, codepoints, cpCount)) {
// For metadata-only calls, METADATA or FULL cache both satisfy layout queries.
// For full (bitmap) calls, only FULL satisfies — METADATA lacks bitmap data.
if (metadataOnly || s.miniMode == PerStyle::MiniMode::FULL) {
// Already wired into miniData; nothing else to do.
return 0;
}
}
// ---- Decide merge vs rebuild ----
// Merge mode: extend the existing METADATA cache with the new cps without
// re-reading already-loaded glyphs. Only safe when:
// - request is metadata-only (no bitmap data needed)
// - existing mode is METADATA
// - merged total stays within MAX_PAGE_GLYPHS
// In all other cases we fall back to today's full-rebuild behavior (which is
// also what happens for full bitmap prewarms — those are page-scoped).
const bool tryMerge =
metadataOnly && s.miniMode == PerStyle::MiniMode::METADATA && s.miniGlyphs != nullptr && s.miniGlyphCount > 0;
// Map codepoints to global glyph indices for this style
struct CpGlyphMapping {
uint32_t codepoint;
int32_t globalIndex;
};
// Worst-case allocation: existing + new (for the merge path) or just new.
const uint32_t mappingCapacity = tryMerge ? (s.miniGlyphCount + cpCount) : cpCount;
CpGlyphMapping* mappings = new (std::nothrow) CpGlyphMapping[mappingCapacity];
if (!mappings) {
LOG_ERR("SDCF", "Failed to allocate mapping array for style %u", styleIdx);
return static_cast<int>(cpCount);
}
uint32_t validCount = 0;
// For merge: seed mappings with already-loaded cps + their global indices,
// recovered by walking miniIntervals and re-resolving via fullIntervals.
// (We don't store globalIndex per mini-glyph; recomputing it is a binary
// search per loaded cp, cheap relative to SD I/O.)
if (tryMerge) {
for (uint32_t iv = 0; iv < s.miniIntervalCount; iv++) {
const auto& interval = s.miniIntervals[iv];
for (uint32_t cp = interval.first; cp <= interval.last; cp++) {
int32_t gIdx = findGlobalGlyphIndex(s, cp);
if (gIdx < 0) continue; // shouldn't happen; defensive
mappings[validCount].codepoint = cp;
mappings[validCount].globalIndex = gIdx;
validCount++;
}
}
}
// Mark where the existing-glyph block ends; new cps follow.
const uint32_t existingCount = validCount;
// Add new requested cps that aren't already in the existing block.
for (uint32_t i = 0; i < cpCount; i++) {
const uint32_t cp = codepoints[i];
// Skip cps already in the existing block (only relevant in merge mode).
if (existingCount > 0) {
bool dup = false;
// Existing cps are sorted; binary search.
int lo = 0, hi = static_cast<int>(existingCount) - 1;
while (lo <= hi) {
int mid = (lo + hi) / 2;
if (mappings[mid].codepoint < cp) {
lo = mid + 1;
} else if (mappings[mid].codepoint > cp) {
hi = mid - 1;
} else {
dup = true;
break;
}
}
if (dup) continue;
}
int32_t idx = findGlobalGlyphIndex(s, cp);
if (idx >= 0) {
if (validCount >= MAX_PAGE_GLYPHS) {
LOG_DBG("SDCF", "Cumulative cap (%u glyphs) reached for style %u; dropping merge cache", MAX_PAGE_GLYPHS,
styleIdx);
// Soft cap: discard the accumulated cache and fall back to rebuilding
// with just the new request set. Caller's text will be covered;
// already-loaded but no-longer-requested cps are lost.
delete[] mappings;
freeStyleMiniData(s);
// Recurse with rebuild semantics by clearing tryMerge state and trying
// again — implemented as inline reset below.
return prewarmStyle(styleIdx, codepoints, cpCount, metadataOnly, loadKernLigatureData);
}
mappings[validCount].codepoint = cp;
mappings[validCount].globalIndex = idx;
validCount++;
}
}
// Sort the full mappings array by codepoint (existing block is already sorted,
// so std::sort on a partially sorted array is fast in practice; insertion-sort
// would be optimal but std::sort is fine for our sizes).
if (validCount > 0) {
std::sort(mappings, mappings + validCount,
[](const CpGlyphMapping& a, const CpGlyphMapping& b) { return a.codepoint < b.codepoint; });
}
// Count cps that are *newly* missing this accumulation cycle (i.e. not present
// in mappings[] AND not previously reported). Already-reported misses are
// suppressed so the same 4 special chars don't spam the log every paragraph.
// `mappings[]` is sorted by codepoint after the std::sort above, enabling
// O(log validCount) lookup per requested cp.
int missed = 0;
for (uint32_t i = 0; i < cpCount; i++) {
const uint32_t cp = codepoints[i];
int lo = 0, hi = static_cast<int>(validCount) - 1;
bool found = false;
while (lo <= hi) {
int mid = (lo + hi) / 2;
if (mappings[mid].codepoint < cp) {
lo = mid + 1;
} else if (mappings[mid].codepoint > cp) {
hi = mid - 1;
} else {
found = true;
break;
}
}
if (found) continue;
// cp wasn't resolved. Check if we've already reported it this cycle.
bool alreadyReported = false;
for (uint8_t r = 0; r < s.reportedMissCount; r++) {
if (s.reportedMisses[r] == cp) {
alreadyReported = true;
break;
}
}
if (alreadyReported) continue;
if (s.reportedMissCount < PerStyle::MAX_REPORTED_MISSES) {
s.reportedMisses[s.reportedMissCount++] = cp;
}
missed++;
}
if (validCount == 0) {
freeStyleMiniData(s);
delete[] mappings;
s.epdFont.data = &s.stubData;
return missed;
}
// Stash the old miniGlyphs so we can copy already-loaded entries by codepoint
// before freeing them. (For merge path; nullptr when not merging.)
EpdGlyph* oldGlyphs = tryMerge ? s.miniGlyphs : nullptr;
EpdUnicodeInterval* oldIntervals = tryMerge ? s.miniIntervals : nullptr;
uint32_t oldIntervalCount = tryMerge ? s.miniIntervalCount : 0;
// Detach so freeStyleMiniData doesn't delete them yet.
if (tryMerge) {
s.miniIntervals = nullptr;
s.miniGlyphs = nullptr;
s.miniIntervalCount = 0;
s.miniGlyphCount = 0;
}
// freeStyleMiniData wipes everything, including miniBitmap (which is nullptr
// in metadata mode anyway). Safe in either path.
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[] oldGlyphs;
delete[] oldIntervals;
delete[] mappings;
return static_cast<int>(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[] oldGlyphs;
delete[] oldIntervals;
delete[] mappings;
freeStyleMiniData(s);
return static_cast<int>(cpCount);
}
// Build a tracking array of which mappings still need SD I/O. For the merge
// path, copy already-loaded glyph metadata from oldGlyphs first; remaining
// entries get read from SD below.
bool* needsRead = new (std::nothrow) bool[validCount];
if (!needsRead) {
LOG_ERR("SDCF", "Failed to allocate needsRead array for style %u", styleIdx);
delete[] oldGlyphs;
delete[] oldIntervals;
delete[] mappings;
freeStyleMiniData(s);
return static_cast<int>(cpCount);
}
for (uint32_t i = 0; i < validCount; i++) needsRead[i] = true;
if (tryMerge && oldGlyphs && oldIntervals) {
// For each cp in the new merged set, look it up in the old miniIntervals to
// see if we already have its EpdGlyph. If yes, copy it over and skip the read.
for (uint32_t i = 0; i < validCount; i++) {
const uint32_t cp = mappings[i].codepoint;
// Binary search oldIntervals for cp.
int lo = 0, hi = static_cast<int>(oldIntervalCount) - 1;
while (lo <= hi) {
int mid = (lo + hi) / 2;
const auto& iv = oldIntervals[mid];
if (cp < iv.first) {
hi = mid - 1;
} else if (cp > iv.last) {
lo = mid + 1;
} else {
s.miniGlyphs[i] = oldGlyphs[iv.offset + (cp - iv.first)];
needsRead[i] = false;
break;
}
}
}
}
delete[] oldGlyphs;
delete[] oldIntervals;
// Count how many SD reads are still needed, and build a sorted read order.
uint32_t toReadCount = 0;
for (uint32_t i = 0; i < validCount; i++) {
if (needsRead[i]) toReadCount++;
}
uint32_t* readOrder = nullptr;
if (toReadCount > 0) {
readOrder = new (std::nothrow) uint32_t[toReadCount];
if (!readOrder) {
LOG_ERR("SDCF", "Failed to allocate read order for style %u", styleIdx);
delete[] needsRead;
delete[] mappings;
freeStyleMiniData(s);
return static_cast<int>(cpCount);
}
uint32_t k = 0;
for (uint32_t i = 0; i < validCount; i++) {
if (needsRead[i]) readOrder[k++] = i;
}
std::sort(readOrder, readOrder + toReadCount,
[&](uint32_t a, uint32_t b) { return mappings[a].globalIndex < mappings[b].globalIndex; });
}
unsigned long sdStart = millis();
uint32_t seekCount = 0;
FsFile file;
if (toReadCount > 0) {
if (!Storage.openFileForRead("SDCF", filePath_, file)) {
LOG_ERR("SDCF", "Failed to reopen .cpfont for prewarm (style %u)", styleIdx);
delete[] readOrder;
delete[] needsRead;
delete[] mappings;
freeStyleMiniData(s);
return static_cast<int>(cpCount);
}
// 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 < toReadCount; i++) {
uint32_t mapIdx = readOrder[i];
int32_t gIdx = mappings[mapIdx].globalIndex;
uint32_t fileOff = s.glyphsFileOffset + static_cast<uint32_t>(gIdx) * sizeof(EpdGlyph);
if (gIdx != lastReadIndex + 1) {
file.seekSet(fileOff);
seekCount++;
}
if (file.read(reinterpret_cast<uint8_t*>(&s.miniGlyphs[mapIdx]), sizeof(EpdGlyph)) != sizeof(EpdGlyph)) {
LOG_ERR("SDCF", "Prewarm: short glyph read (style %u, glyph %d)", styleIdx, gIdx);
file.close();
delete[] readOrder;
delete[] needsRead;
delete[] mappings;
freeStyleMiniData(s);
return static_cast<int>(cpCount);
}
lastReadIndex = gIdx;
}
}
delete[] needsRead;
delete[] readOrder;
readOrder = nullptr;
uint32_t totalBitmapSize = 0;
if (!metadataOnly) {
// Full render prewarm always reads bitmap data for every glyph in the
// mini set. The metadata-pass file open above only covered cps that
// needed metadata reads — open the file now if it isn't already.
if (!file) {
if (!Storage.openFileForRead("SDCF", filePath_, file)) {
LOG_ERR("SDCF", "Failed to reopen .cpfont for bitmap prewarm (style %u)", styleIdx);
delete[] mappings;
freeStyleMiniData(s);
return static_cast<int>(cpCount);
}
}
// 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);
file.close();
delete[] mappings;
freeStyleMiniData(s);
return static_cast<int>(cpCount);
}
// Allocate a fresh readOrder covering all validCount glyphs, sorted by
// bitmap file offset for sequential I/O.
uint32_t* bitmapOrder = new (std::nothrow) uint32_t[validCount];
if (!bitmapOrder) {
LOG_ERR("SDCF", "Failed to allocate bitmap read order for style %u", styleIdx);
file.close();
delete[] mappings;
freeStyleMiniData(s);
return static_cast<int>(cpCount);
}
for (uint32_t i = 0; i < validCount; i++) bitmapOrder[i] = i;
std::sort(bitmapOrder, bitmapOrder + 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 = bitmapOrder[i];
EpdGlyph& glyph = s.miniGlyphs[mapIdx];
if (glyph.dataLength == 0) {
glyph.dataOffset = miniBitmapOffset;
continue;
}
uint32_t fileOff = s.bitmapFileOffset + glyph.dataOffset;
if (fileOff != lastBitmapEnd) {
file.seekSet(fileOff);
seekCount++;
}
if (file.read(s.miniBitmap + miniBitmapOffset, glyph.dataLength) != static_cast<int>(glyph.dataLength)) {
LOG_ERR("SDCF", "Prewarm: short bitmap read (style %u)", styleIdx);
file.close();
delete[] bitmapOrder;
delete[] mappings;
freeStyleMiniData(s);
return static_cast<int>(cpCount);
}
lastBitmapEnd = fileOff + glyph.dataLength;
glyph.dataOffset = miniBitmapOffset;
miniBitmapOffset += glyph.dataLength;
}
delete[] bitmapOrder;
}
uint32_t sdTime = millis() - sdStart;
if (file) file.close();
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. Also do this during layout-only metadata prewarm when
// kern/ligature metadata is explicitly requested.
bool kernLigOk = false;
if (!metadataOnly || loadKernLigatureData) {
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;
s.miniMode = metadataOnly ? PerStyle::MiniMode::METADATA : PerStyle::MiniMode::FULL;
// Accumulate stats
stats_.sdReadTimeMs += sdTime;
stats_.seekCount += seekCount;
stats_.uniqueGlyphs += validCount;
stats_.bitmapBytes += totalBitmapSize;
return missed;
}
// --- Cache management ---
void SdCardFont::clearCache() {
clearOverflow();
for (uint8_t i = 0; i < MAX_STYLES; i++) {
if (!styles_[i].present) continue;
freeStyleMiniData(styles_[i]);
applyGlyphMissCallback(i);
}
}
void SdCardFont::clearAccumulation() {
// Same as clearCache() but skips the overflow ring buffer (per-glyph on-demand
// loads are independent of the cumulative metadata cache).
for (uint8_t i = 0; i < MAX_STYLES; i++) {
if (!styles_[i].present) continue;
freeStyleMiniData(styles_[i]);
applyGlyphMissCallback(i);
}
}
// --- 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) {
if (style >= MAX_STYLES || !styles_[style].present) return nullptr;
return &styles_[style].epdFont;
}
bool SdCardFont::hasStyle(uint8_t style) const { return style < MAX_STYLES && styles_[style].present; }
// --- On-demand glyph loading (overflow buffer) ---
const EpdGlyph* SdCardFont::onGlyphMiss(void* ctx, uint32_t codepoint) {
auto* oc = static_cast<OverflowContext*>(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) 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.
uint32_t slot = self->overflowNext_;
bool wasAtCapacity = (self->overflowCount_ == OVERFLOW_CAPACITY);
if (!wasAtCapacity) {
self->overflowCount_++;
}
self->overflowNext_ = (slot + 1) % OVERFLOW_CAPACITY;
// Read glyph metadata into temporary
FsFile file;
if (!Storage.openFileForRead("SDCF", self->filePath_, file)) {
LOG_ERR("SDCF", "Overflow: failed to open .cpfont");
if (!wasAtCapacity) self->overflowCount_--;
return nullptr;
}
EpdGlyph tempGlyph;
uint32_t glyphFileOff = s.glyphsFileOffset + static_cast<uint32_t>(globalIdx) * sizeof(EpdGlyph);
if (!file.seekSet(glyphFileOff)) {
LOG_ERR("SDCF", "Overflow: seek failed for glyph metadata U+%04X style %u", codepoint, styleIdx);
file.close();
if (!wasAtCapacity) self->overflowCount_--;
return nullptr;
}
if (file.read(reinterpret_cast<uint8_t*>(&tempGlyph), sizeof(EpdGlyph)) != sizeof(EpdGlyph)) {
LOG_ERR("SDCF", "Overflow: failed to read glyph metadata for U+%04X style %u", codepoint, styleIdx);
file.close();
if (!wasAtCapacity) self->overflowCount_--;
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);
file.close();
if (!wasAtCapacity) self->overflowCount_--;
return nullptr;
}
if (!file.seekSet(s.bitmapFileOffset + tempGlyph.dataOffset)) {
LOG_ERR("SDCF", "Overflow: seek failed for bitmap U+%04X style %u", codepoint, styleIdx);
delete[] tempBitmap;
file.close();
if (!wasAtCapacity) self->overflowCount_--;
return nullptr;
}
if (file.read(tempBitmap, tempGlyph.dataLength) != static_cast<int>(tempGlyph.dataLength)) {
LOG_ERR("SDCF", "Overflow: failed to read bitmap for U+%04X", codepoint);
delete[] tempBitmap;
file.close();
if (!wasAtCapacity) self->overflowCount_--;
return nullptr;
}
}
file.close();
// All reads succeeded — commit to slot (evict old entry if at capacity)
if (wasAtCapacity) {
delete[] self->overflow_[slot].bitmap;
}
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<OverflowContext*>(ctx)->self; }