1426 lines
52 KiB
C++
1426 lines
52 KiB
C++
#include "SdCardFont.h"
|
||
|
||
#include <HalStorage.h>
|
||
#include <Logging.h>
|
||
#include <Utf8.h>
|
||
|
||
#include <algorithm>
|
||
#include <climits>
|
||
#include <cstring>
|
||
#include <memory>
|
||
|
||
#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<int16_t>(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<const unsigned char*>(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; }
|
||
|
||
// Keep-if-fits buffer reuse: only reallocate when the needed size exceeds the
|
||
// current capacity. Freeing + reallocating slightly different sizes every page
|
||
// turn punches non-coalescing holes in the heap (the freed block rarely fits the
|
||
// next page's need), eroding the largest contiguous block all session. With
|
||
// reuse, capacities converge on the book's max page after a few turns and page
|
||
// turns stop touching the allocator. Only three small instantiations exist
|
||
// (interval/glyph/byte arrays), so template bloat is negligible.
|
||
template <typename T, typename CapT>
|
||
bool ensureArrayCapacity(T*& buf, CapT& capacity, const uint32_t needed) {
|
||
if (buf && capacity >= needed) return true;
|
||
delete[] buf;
|
||
buf = new (std::nothrow) T[needed > 0 ? needed : 1];
|
||
capacity = buf ? static_cast<CapT>(needed) : 0;
|
||
return buf != nullptr;
|
||
}
|
||
|
||
} // 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;
|
||
s.miniIntervalCapacity = 0;
|
||
s.miniGlyphCapacity = 0;
|
||
s.miniBitmapCapacity = 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;
|
||
s.miniKernLeftCapacity = 0;
|
||
s.miniKernRightCapacity = 0;
|
||
s.miniKernMatrixCapacity = 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<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);
|
||
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<uint8_t*>(s.ligaturePairs), sz) != static_cast<int>(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<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: size the three mini buffers (reused across pages when they fit; the
|
||
// per-page sizes vary by a few entries, which as free+realloc churn was punching
|
||
// non-coalescing holes in the heap every page turn).
|
||
const uint32_t matrixBytes = static_cast<uint32_t>(numLeft) * numRight;
|
||
if (!ensureArrayCapacity(s.miniKernLeftClasses, s.miniKernLeftCapacity, miniLeftCount) ||
|
||
!ensureArrayCapacity(s.miniKernRightClasses, s.miniKernRightCapacity, miniRightCount) ||
|
||
!ensureArrayCapacity(s.miniKernMatrix, s.miniKernMatrixCapacity, matrixBytes)) {
|
||
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).
|
||
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<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);
|
||
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<uint8_t*>(rowBuf.get()), s.header.kernRightClassCount) !=
|
||
static_cast<int>(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<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';
|
||
|
||
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<uint8_t*>(&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<unsigned long>(iv.first), static_cast<unsigned long>(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<uint8_t*>(&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<uint16_t>(iv.first), static_cast<uint16_t>(iv.last),
|
||
static_cast<uint16_t>(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<uint8_t*>(s.fullIntervals), intervalsBytes) != static_cast<int>(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<int>(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<int32_t>(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<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.
|
||
// 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<int>(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<int>(cpCount - validCount);
|
||
|
||
if (validCount == 0) {
|
||
freeStyleMiniData(s);
|
||
delete[] mappings;
|
||
s.epdFont.data = &s.stubData;
|
||
return missed;
|
||
}
|
||
|
||
// Build mini intervals from sorted codepoints. Reset counts and fall back to the
|
||
// stub until the rebuild completes, but KEEP the existing buffers (keep-if-fits
|
||
// reuse) — the free-and-realloc-per-page pattern here was a primary fragmenter.
|
||
s.miniIntervalCount = 0;
|
||
s.miniGlyphCount = 0;
|
||
s.miniKernLeftEntryCount = 0;
|
||
s.miniKernRightEntryCount = 0;
|
||
s.miniKernLeftClassCount = 0;
|
||
s.miniKernRightClassCount = 0;
|
||
memset(&s.miniData, 0, sizeof(s.miniData));
|
||
s.epdFont.data = &s.stubData;
|
||
|
||
if (!ensureArrayCapacity(s.miniIntervals, s.miniIntervalCapacity, validCount)) {
|
||
LOG_ERR("SDCF", "Failed to allocate mini intervals for style %u", styleIdx);
|
||
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;
|
||
}
|
||
}
|
||
|
||
// Mini glyph array (reused across pages when it fits)
|
||
if (!ensureArrayCapacity(s.miniGlyphs, s.miniGlyphCapacity, validCount)) {
|
||
LOG_ERR("SDCF", "Failed to allocate mini glyphs for style %u", styleIdx);
|
||
delete[] mappings;
|
||
freeStyleMiniData(s);
|
||
return static_cast<int>(cpCount);
|
||
}
|
||
s.miniGlyphCount = validCount;
|
||
|
||
// 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<int>(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<int>(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<uint32_t>(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<int>(cpCount);
|
||
}
|
||
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);
|
||
delete[] readOrder;
|
||
delete[] mappings;
|
||
freeStyleMiniData(s);
|
||
return static_cast<int>(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;
|
||
}
|
||
|
||
if (!ensureArrayCapacity(s.miniBitmap, s.miniBitmapCapacity, totalBitmapSize)) {
|
||
LOG_ERR("SDCF", "Failed to allocate mini bitmap (%u bytes) for style %u", totalBitmapSize, styleIdx);
|
||
delete[] readOrder;
|
||
delete[] mappings;
|
||
freeStyleMiniData(s);
|
||
return static_cast<int>(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<int>(cpCount);
|
||
}
|
||
seekCount++;
|
||
}
|
||
if (file.read(s.miniBitmap + miniBitmapOffset, glyph.dataLength) != static_cast<int>(glyph.dataLength)) {
|
||
LOG_ERR("SDCF", "Prewarm: short bitmap read (style %u)", styleIdx);
|
||
delete[] readOrder;
|
||
delete[] mappings;
|
||
freeStyleMiniData(s);
|
||
return static_cast<int>(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::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<CpIdx[]> 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<int>(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<AdvanceEntry[]> 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<uint32_t>(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<uint8_t*>(&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 <typename Iter>
|
||
int SdCardFont::buildAdvanceTableRange(Iter begin, Iter end, bool includeSpace, bool includeHyphen, uint8_t styleMask,
|
||
const char* extraText) {
|
||
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 (extraText && !hitCap) {
|
||
hitCap = collectUniqueCodepoints(extraText, 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, const char* extraText) {
|
||
return buildAdvanceTableRange(&utf8Text, &utf8Text + 1, false, false, styleMask, extraText);
|
||
}
|
||
|
||
int SdCardFont::buildAdvanceTable(const std::vector<std::string>& words, bool includeHyphen, uint8_t styleMask,
|
||
const char* extraText) {
|
||
return buildAdvanceTableRange(words.begin(), words.end(), words.size() > 1, includeHyphen, styleMask, extraText);
|
||
}
|
||
|
||
// --- 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<uint8_t>(1u << resolveStyle(si));
|
||
}
|
||
}
|
||
return resolvedMask;
|
||
}
|
||
|
||
// --- 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 && !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<uint32_t>(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<uint8_t*>(&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<int>(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<OverflowContext*>(ctx)->self; }
|