Integrate upstream #1392 by adriancaruana
This commit is contained in:
@@ -15,3 +15,4 @@ build
|
||||
.history/
|
||||
/.venv
|
||||
*.local*
|
||||
*.cpfont
|
||||
|
||||
+20
-12
@@ -148,24 +148,32 @@ uint32_t EpdFont::applyLigatures(uint32_t cp, const char*& text) const {
|
||||
|
||||
const EpdGlyph* EpdFont::getGlyph(const uint32_t cp) const {
|
||||
const int count = data->intervalCount;
|
||||
if (count == 0) return nullptr;
|
||||
if (count == 0 && !data->glyphMissHandler) return nullptr;
|
||||
|
||||
const EpdUnicodeInterval* intervals = data->intervals;
|
||||
const auto* end = intervals + count;
|
||||
if (count > 0) {
|
||||
const EpdUnicodeInterval* intervals = data->intervals;
|
||||
const auto* end = intervals + count;
|
||||
|
||||
// upper_bound: range lookup. Finds the first interval with first > cp, so the
|
||||
// interval just before it is the last one with first <= cp. That's the only
|
||||
// candidate that could contain cp. Then we verify cp <= candidate.last.
|
||||
const auto it = std::upper_bound(
|
||||
intervals, end, cp, [](uint32_t value, const EpdUnicodeInterval& interval) { return value < interval.first; });
|
||||
// upper_bound: range lookup. Finds the first interval with first > cp, so the
|
||||
// interval just before it is the last one with first <= cp. That's the only
|
||||
// candidate that could contain cp. Then we verify cp <= candidate.last.
|
||||
const auto it = std::upper_bound(
|
||||
intervals, end, cp, [](uint32_t value, const EpdUnicodeInterval& interval) { return value < interval.first; });
|
||||
|
||||
if (it != intervals) {
|
||||
const auto& interval = *(it - 1);
|
||||
if (cp <= interval.last) {
|
||||
return &data->glyph[interval.offset + (cp - interval.first)];
|
||||
if (it != intervals) {
|
||||
const auto& interval = *(it - 1);
|
||||
if (cp <= interval.last) {
|
||||
return &data->glyph[interval.offset + (cp - interval.first)];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Codepoint not in interval table — try on-demand loading (SD card fonts).
|
||||
if (data->glyphMissHandler) {
|
||||
const EpdGlyph* loaded = data->glyphMissHandler(data->glyphMissCtx, cp);
|
||||
if (loaded) return loaded;
|
||||
}
|
||||
|
||||
if (cp != REPLACEMENT_GLYPH) {
|
||||
return getGlyph(REPLACEMENT_GLYPH);
|
||||
}
|
||||
|
||||
@@ -129,4 +129,16 @@ typedef struct {
|
||||
uint8_t kernRightClassCount; ///< Number of distinct right classes (matrix cols)
|
||||
const EpdLigaturePair* ligaturePairs; ///< Sorted ligature pair table (nullptr if none)
|
||||
uint32_t ligaturePairCount; ///< Number of entries in ligaturePairs
|
||||
|
||||
/// On-demand glyph loading for fonts that don't keep all glyphs in RAM (e.g. SD card fonts).
|
||||
/// Called by getGlyph() when a codepoint is not found in the interval table.
|
||||
/// Returns a valid EpdGlyph* with correct metadata, or nullptr to fall back to the
|
||||
/// replacement glyph. The returned pointer is valid until the next glyphMissHandler
|
||||
/// call that causes a ring-buffer eviction — callers must consume it (measure or draw)
|
||||
/// before requesting another missed glyph.
|
||||
const EpdGlyph* (*glyphMissHandler)(void* ctx, uint32_t codepoint);
|
||||
|
||||
/// Context pointer for glyphMissHandler (typically SdCardFont*). Also used by
|
||||
/// GfxRenderer::getGlyphBitmap() to retrieve overflow bitmaps via SdCardFont.
|
||||
void* glyphMissCtx;
|
||||
} EpdFontData;
|
||||
|
||||
@@ -258,6 +258,43 @@ int FontDecompressor::prewarmCache(const EpdFontData* fontData, const char* utf8
|
||||
}
|
||||
}
|
||||
|
||||
// Add ligature output glyphs: if both input codepoints of a ligature pair are
|
||||
// in the needed set, the output glyph will be queried during rendering.
|
||||
// Must run BEFORE the neededGlyphGroups[] parallel-array loop below so appended
|
||||
// glyphs receive a group index — otherwise hot-group lookup misses them.
|
||||
if (fontData->ligaturePairs && fontData->ligaturePairCount > 0) {
|
||||
for (uint32_t li = 0; li < fontData->ligaturePairCount && glyphCount < MAX_PAGE_GLYPHS; li++) {
|
||||
uint32_t leftCp = fontData->ligaturePairs[li].pair >> 16;
|
||||
uint32_t rightCp = fontData->ligaturePairs[li].pair & 0xFFFF;
|
||||
|
||||
int32_t leftIdx = findGlyphIndex(fontData, leftCp);
|
||||
int32_t rightIdx = findGlyphIndex(fontData, rightCp);
|
||||
if (leftIdx < 0 || rightIdx < 0) continue;
|
||||
|
||||
bool hasLeft = false, hasRight = false;
|
||||
for (uint16_t i = 0; i < glyphCount; i++) {
|
||||
if (neededGlyphs[i] == static_cast<uint32_t>(leftIdx)) hasLeft = true;
|
||||
if (neededGlyphs[i] == static_cast<uint32_t>(rightIdx)) hasRight = true;
|
||||
if (hasLeft && hasRight) break;
|
||||
}
|
||||
if (!hasLeft || !hasRight) continue;
|
||||
|
||||
int32_t outIdx = findGlyphIndex(fontData, fontData->ligaturePairs[li].ligatureCp);
|
||||
if (outIdx < 0) continue;
|
||||
|
||||
bool found = false;
|
||||
for (uint16_t i = 0; i < glyphCount; i++) {
|
||||
if (neededGlyphs[i] == static_cast<uint32_t>(outIdx)) {
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!found) {
|
||||
neededGlyphs[glyphCount++] = static_cast<uint32_t>(outIdx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (glyphCount == 0) return 0;
|
||||
|
||||
// Step 2: Compute total buffer size and collect unique groups
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,187 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
#include "EpdFont.h"
|
||||
#include "EpdFontData.h"
|
||||
|
||||
class SdCardFont {
|
||||
public:
|
||||
static constexpr uint16_t MAX_PAGE_GLYPHS = 512;
|
||||
static constexpr uint8_t MAX_STYLES = 4;
|
||||
|
||||
SdCardFont() = default;
|
||||
~SdCardFont();
|
||||
|
||||
// Load .cpfont file: reads header + intervals into RAM, records file layout offsets.
|
||||
// Supports v4 (multi-style) format.
|
||||
// Returns true on success.
|
||||
bool load(const char* path);
|
||||
|
||||
// Pre-read glyphs needed for the given UTF-8 text from SD card.
|
||||
// styleMask: bitmask of styles to prewarm (bit 0=regular, 1=bold, 2=italic, 3=bolditalic).
|
||||
// Default 0x0F = all present styles.
|
||||
// When metadataOnly=true, only glyph metrics are loaded (no bitmap data).
|
||||
// Returns number of glyphs that couldn't be loaded (0 on full success).
|
||||
int prewarm(const char* utf8Text, uint8_t styleMask = 0x0F, bool metadataOnly = false);
|
||||
|
||||
// Free mini data for all styles, restore stub EpdFontData.
|
||||
void clearCache();
|
||||
|
||||
// Returns pointer to the managed EpdFont for a given style.
|
||||
// Returns nullptr if the style is not present.
|
||||
EpdFont* getEpdFont(uint8_t style = 0);
|
||||
|
||||
// Returns true if the given style is present in this font file.
|
||||
bool hasStyle(uint8_t style) const;
|
||||
|
||||
// Number of styles present in this font file.
|
||||
uint8_t styleCount() const { return styleCount_; }
|
||||
|
||||
// Returns true if the glyph pointer points into the overflow buffer.
|
||||
bool isOverflowGlyph(const EpdGlyph* glyph) const;
|
||||
|
||||
// Returns the bitmap for an on-demand-loaded (overflow) glyph.
|
||||
const uint8_t* getOverflowBitmap(const EpdGlyph* glyph) const;
|
||||
|
||||
// Extract SdCardFont* from an opaque glyphMissCtx pointer.
|
||||
// Used by GfxRenderer::getGlyphBitmap() to recover the SdCardFont from EpdFontData::glyphMissCtx.
|
||||
static SdCardFont* fromMissCtx(void* ctx);
|
||||
|
||||
struct Stats {
|
||||
uint32_t prewarmTotalMs = 0;
|
||||
uint32_t sdReadTimeMs = 0;
|
||||
uint32_t seekCount = 0;
|
||||
uint32_t uniqueGlyphs = 0;
|
||||
uint32_t bitmapBytes = 0;
|
||||
};
|
||||
void logStats(const char* label = "SDCF");
|
||||
void resetStats();
|
||||
const Stats& getStats() const { return stats_; }
|
||||
|
||||
// Content hash of the file header + style TOC entries (computed during load).
|
||||
// Used to generate deterministic font IDs for section cache invalidation.
|
||||
uint32_t contentHash() const { return contentHash_; }
|
||||
|
||||
private:
|
||||
// Per-style metadata (parsed from file header/TOC)
|
||||
struct CpFontHeader {
|
||||
uint32_t intervalCount = 0;
|
||||
uint32_t glyphCount = 0;
|
||||
uint8_t advanceY = 0;
|
||||
int16_t ascender = 0;
|
||||
int16_t descender = 0;
|
||||
bool is2Bit = false;
|
||||
uint16_t kernLeftEntryCount = 0;
|
||||
uint16_t kernRightEntryCount = 0;
|
||||
uint8_t kernLeftClassCount = 0;
|
||||
uint8_t kernRightClassCount = 0;
|
||||
uint8_t ligaturePairCount = 0;
|
||||
};
|
||||
|
||||
// All per-style data: file offsets, intervals, kern/lig, prewarm cache, EpdFont
|
||||
struct PerStyle {
|
||||
CpFontHeader header{};
|
||||
|
||||
// File layout offsets for this style's data sections
|
||||
uint32_t intervalsFileOffset = 0;
|
||||
uint32_t glyphsFileOffset = 0;
|
||||
uint32_t kernLeftFileOffset = 0;
|
||||
uint32_t kernRightFileOffset = 0;
|
||||
uint32_t kernMatrixFileOffset = 0;
|
||||
uint32_t ligatureFileOffset = 0;
|
||||
uint32_t bitmapFileOffset = 0;
|
||||
|
||||
// Full intervals loaded from file (kept in RAM for codepoint lookup)
|
||||
EpdUnicodeInterval* fullIntervals = nullptr;
|
||||
|
||||
// Persistent kern-class + ligature tables (lazy-loaded on first prewarm).
|
||||
// The full kern MATRIX is NOT resident — on Literata-class fonts a single
|
||||
// style's matrix is ~36-42KB contiguous, and 4 styles' worth won't fit
|
||||
// alongside bitmaps + framebuffer on a 380KB device. Only kernLeftClasses
|
||||
// and kernRightClasses (small codepoint→classId tables, ~3KB each) stay
|
||||
// resident; the matrix is reconstructed per-page as miniKernMatrix.
|
||||
EpdKernClassEntry* kernLeftClasses = nullptr;
|
||||
EpdKernClassEntry* kernRightClasses = nullptr;
|
||||
EpdLigaturePair* ligaturePairs = nullptr;
|
||||
bool kernLigLoaded = false;
|
||||
|
||||
// Stub EpdFontData returned when not prewarmed
|
||||
EpdFontData stubData{};
|
||||
|
||||
// Mini EpdFontData built during prewarm
|
||||
EpdFontData miniData{};
|
||||
EpdUnicodeInterval* miniIntervals = nullptr;
|
||||
EpdGlyph* miniGlyphs = nullptr;
|
||||
uint8_t* miniBitmap = nullptr;
|
||||
uint32_t miniIntervalCount = 0;
|
||||
uint32_t miniGlyphCount = 0;
|
||||
|
||||
// Per-page mini kern matrix (built by buildMiniKernMatrix on each full
|
||||
// prewarm). miniKernLeftClasses/miniKernRightClasses map ONLY the codepoints
|
||||
// used on the current page to renumbered class IDs (1..miniKern*ClassCount).
|
||||
// miniKernMatrix is a small miniKernLeftClassCount × miniKernRightClassCount
|
||||
// flat matrix. Typical Latin page: ~25×25 matrix = ~625 bytes per style vs
|
||||
// ~36KB for the full Literata matrix — ~50× reduction.
|
||||
EpdKernClassEntry* miniKernLeftClasses = nullptr;
|
||||
EpdKernClassEntry* miniKernRightClasses = nullptr;
|
||||
uint16_t miniKernLeftEntryCount = 0;
|
||||
uint16_t miniKernRightEntryCount = 0;
|
||||
uint8_t miniKernLeftClassCount = 0;
|
||||
uint8_t miniKernRightClassCount = 0;
|
||||
int8_t* miniKernMatrix = nullptr;
|
||||
|
||||
// The EpdFont whose data pointer we manage
|
||||
EpdFont epdFont{&stubData};
|
||||
|
||||
bool present = false;
|
||||
};
|
||||
|
||||
PerStyle styles_[MAX_STYLES] = {};
|
||||
uint8_t styleCount_ = 0;
|
||||
|
||||
char filePath_[128] = {};
|
||||
|
||||
// Overflow context: glyphMissHandler needs to know which style it's serving
|
||||
struct OverflowContext {
|
||||
SdCardFont* self;
|
||||
uint8_t styleIdx;
|
||||
};
|
||||
OverflowContext overflowCtx_[MAX_STYLES] = {};
|
||||
|
||||
// Shared on-demand overflow buffer (ring buffer of glyphs loaded via glyphMissHandler)
|
||||
static constexpr uint32_t OVERFLOW_CAPACITY = 8;
|
||||
struct OverflowEntry {
|
||||
EpdGlyph glyph;
|
||||
uint8_t* bitmap = nullptr;
|
||||
uint32_t codepoint = 0;
|
||||
uint8_t styleIdx = 0;
|
||||
};
|
||||
OverflowEntry overflow_[OVERFLOW_CAPACITY] = {};
|
||||
uint32_t overflowCount_ = 0;
|
||||
uint32_t overflowNext_ = 0;
|
||||
|
||||
Stats stats_;
|
||||
uint32_t contentHash_ = 0;
|
||||
bool loaded_ = false;
|
||||
|
||||
// Per-style helpers
|
||||
void freeStyleMiniData(PerStyle& s);
|
||||
void freeStyleAll(PerStyle& s);
|
||||
void freeStyleKernLigatureData(PerStyle& s);
|
||||
void freeStyleMiniKern(PerStyle& s);
|
||||
bool loadStyleKernLigatureData(PerStyle& s);
|
||||
bool buildMiniKernMatrix(PerStyle& s, const uint32_t* codepoints, uint32_t cpCount);
|
||||
void applyKernLigaturePointers(PerStyle& s, EpdFontData& data) const;
|
||||
void applyGlyphMissCallback(uint8_t styleIdx);
|
||||
int32_t findGlobalGlyphIndex(const PerStyle& s, uint32_t codepoint) const;
|
||||
int prewarmStyle(uint8_t styleIdx, const uint32_t* codepoints, uint32_t cpCount, bool metadataOnly);
|
||||
|
||||
// Global helpers
|
||||
void freeAll();
|
||||
void clearOverflow();
|
||||
static void computeStyleFileOffsets(PerStyle& s, uint32_t baseOffset);
|
||||
|
||||
// Static callback for EpdFontData::glyphMissHandler (per-style via OverflowContext)
|
||||
static const EpdGlyph* onGlyphMiss(void* ctx, uint32_t codepoint);
|
||||
};
|
||||
@@ -0,0 +1,104 @@
|
||||
#include "SdCardFontManager.h"
|
||||
|
||||
#include <EpdFontFamily.h>
|
||||
#include <GfxRenderer.h>
|
||||
#include <Logging.h>
|
||||
#include <SdCardFont.h>
|
||||
#include <SdCardFontRegistry.h>
|
||||
|
||||
#include <cstdlib>
|
||||
|
||||
SdCardFontManager::~SdCardFontManager() {
|
||||
for (auto& lf : loaded_) {
|
||||
delete lf.font;
|
||||
}
|
||||
}
|
||||
|
||||
// FNV-1a continuation: seeds with contentHash, then hashes family name + point size.
|
||||
// Produces a deterministic ID that is stable across load/unload cycles and reboots,
|
||||
// and changes when font content changes (different header/TOC = different contentHash).
|
||||
int SdCardFontManager::computeFontId(uint32_t contentHash, const char* familyName, uint8_t pointSize) {
|
||||
static constexpr uint32_t FNV_PRIME = 16777619u;
|
||||
uint32_t hash = contentHash;
|
||||
while (*familyName) {
|
||||
hash ^= static_cast<uint8_t>(*familyName++);
|
||||
hash *= FNV_PRIME;
|
||||
}
|
||||
hash ^= pointSize;
|
||||
hash *= FNV_PRIME;
|
||||
int id = static_cast<int>(hash);
|
||||
return id != 0 ? id : 1; // 0 is reserved as "not found" sentinel
|
||||
}
|
||||
|
||||
bool SdCardFontManager::loadFamily(const SdCardFontFamilyInfo& family, GfxRenderer& renderer, uint8_t targetPtSize) {
|
||||
// Unload any previously loaded family first
|
||||
if (!loadedFamilyName_.empty()) {
|
||||
unloadAll(renderer);
|
||||
}
|
||||
|
||||
// Pick the single file whose size is closest to targetPtSize. Loading
|
||||
// only one size bounds resident memory (intervals + kern/ligature tables
|
||||
// per style) to one file's worth, vs. N_sizes × per-file overhead.
|
||||
const SdCardFontFileInfo* selected = nullptr;
|
||||
int bestDiff = INT32_MAX;
|
||||
for (const auto& fileInfo : family.files) {
|
||||
int diff = std::abs(static_cast<int>(fileInfo.pointSize) - static_cast<int>(targetPtSize));
|
||||
if (diff < bestDiff) {
|
||||
bestDiff = diff;
|
||||
selected = &fileInfo;
|
||||
}
|
||||
}
|
||||
if (!selected) {
|
||||
LOG_ERR("SDMGR", "Family %s has no files to load", family.name.c_str());
|
||||
return false;
|
||||
}
|
||||
|
||||
auto* font = new (std::nothrow) SdCardFont();
|
||||
if (!font) {
|
||||
LOG_ERR("SDMGR", "Failed to allocate SdCardFont for %s", selected->path.c_str());
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!font->load(selected->path.c_str())) {
|
||||
LOG_ERR("SDMGR", "Failed to load %s", selected->path.c_str());
|
||||
delete font;
|
||||
return false;
|
||||
}
|
||||
|
||||
int fontId = computeFontId(font->contentHash(), family.name.c_str(), selected->pointSize);
|
||||
// Guard against collision with built-in font IDs (astronomically unlikely
|
||||
// with FNV-1a hashes, but provides a safety net)
|
||||
if (renderer.getFontMap().count(fontId) != 0) {
|
||||
LOG_ERR("SDMGR", "Font ID %d collides with existing font, skipping %s", fontId, selected->path.c_str());
|
||||
delete font;
|
||||
return false;
|
||||
}
|
||||
renderer.registerSdCardFont(fontId, font);
|
||||
loaded_.push_back({font, fontId, selected->pointSize});
|
||||
|
||||
LOG_DBG("SDMGR", "Loaded %s size=%u id=%d styles=%u (target=%u)", selected->path.c_str(), selected->pointSize, fontId,
|
||||
font->styleCount(), targetPtSize);
|
||||
|
||||
EpdFontFamily fontFamily(font->getEpdFont(0), font->getEpdFont(1), font->getEpdFont(2), font->getEpdFont(3));
|
||||
renderer.insertFont(fontId, fontFamily);
|
||||
|
||||
loadedFamilyName_ = family.name;
|
||||
loadedPointSize_ = selected->pointSize;
|
||||
return true;
|
||||
}
|
||||
|
||||
void SdCardFontManager::unloadAll(GfxRenderer& renderer) {
|
||||
renderer.clearSdCardFonts();
|
||||
for (auto& lf : loaded_) {
|
||||
renderer.removeFont(lf.fontId);
|
||||
delete lf.font;
|
||||
}
|
||||
loaded_.clear();
|
||||
loadedFamilyName_.clear();
|
||||
loadedPointSize_ = 0;
|
||||
}
|
||||
|
||||
int SdCardFontManager::getFontId(const std::string& familyName) const {
|
||||
if (familyName != loadedFamilyName_ || loaded_.empty()) return 0;
|
||||
return loaded_.front().fontId;
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
class GfxRenderer;
|
||||
class SdCardFont;
|
||||
struct SdCardFontFamilyInfo;
|
||||
|
||||
class SdCardFontManager {
|
||||
public:
|
||||
SdCardFontManager() = default;
|
||||
~SdCardFontManager();
|
||||
SdCardFontManager(const SdCardFontManager&) = delete;
|
||||
SdCardFontManager& operator=(const SdCardFontManager&) = delete;
|
||||
|
||||
// Load the single size closest to targetPtSize for a discovered family.
|
||||
// Only one .cpfont file is loaded; other sizes remain on disk. This keeps
|
||||
// resident interval + kern/ligature tables to one size's worth of memory
|
||||
// (see PR #1327 discussion re: Literata OOM).
|
||||
// Returns true on success.
|
||||
bool loadFamily(const SdCardFontFamilyInfo& family, GfxRenderer& renderer, uint8_t targetPtSize);
|
||||
|
||||
// Unload everything, unregister from renderer.
|
||||
void unloadAll(GfxRenderer& renderer);
|
||||
|
||||
// Look up the font ID for the loaded family. Returns 0 if nothing loaded
|
||||
// or familyName doesn't match.
|
||||
int getFontId(const std::string& familyName) const;
|
||||
|
||||
// Get name of currently loaded family (empty if none).
|
||||
const std::string& currentFamilyName() const { return loadedFamilyName_; };
|
||||
|
||||
// Point size that was actually loaded (closest match to targetPtSize).
|
||||
// 0 if nothing loaded.
|
||||
uint8_t currentPointSize() const { return loadedPointSize_; };
|
||||
|
||||
private:
|
||||
struct LoadedFont {
|
||||
SdCardFont* font; // heap-allocated, owned
|
||||
int fontId;
|
||||
uint8_t size;
|
||||
};
|
||||
static int computeFontId(uint32_t contentHash, const char* familyName, uint8_t pointSize);
|
||||
|
||||
std::string loadedFamilyName_;
|
||||
uint8_t loadedPointSize_ = 0;
|
||||
std::vector<LoadedFont> loaded_;
|
||||
};
|
||||
@@ -0,0 +1,172 @@
|
||||
#include "SdCardFontRegistry.h"
|
||||
|
||||
#include <HalStorage.h>
|
||||
#include <Logging.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstring>
|
||||
|
||||
// --- SdCardFontFamilyInfo helpers ---
|
||||
|
||||
const SdCardFontFileInfo* SdCardFontFamilyInfo::findFile(uint8_t size, uint8_t style) const {
|
||||
for (const auto& f : files) {
|
||||
if (f.pointSize == size && f.style == style) return &f;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
bool SdCardFontFamilyInfo::hasSize(uint8_t size) const {
|
||||
for (const auto& f : files) {
|
||||
if (f.pointSize == size) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
std::vector<uint8_t> SdCardFontFamilyInfo::availableSizes() const {
|
||||
std::vector<uint8_t> sizes;
|
||||
for (const auto& f : files) {
|
||||
bool found = false;
|
||||
for (uint8_t s : sizes) {
|
||||
if (s == f.pointSize) {
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!found) sizes.push_back(f.pointSize);
|
||||
}
|
||||
std::sort(sizes.begin(), sizes.end());
|
||||
return sizes;
|
||||
}
|
||||
|
||||
// --- SdCardFontRegistry ---
|
||||
|
||||
bool SdCardFontRegistry::parseFilename(const char* filename, uint8_t& size, uint8_t& style) {
|
||||
// V4 naming: <name>_<size>.cpfont (e.g. Bookerly-SD_14.cpfont)
|
||||
// Use an ends-with check rather than strstr() so that in-progress downloads
|
||||
// like "Foo_14.cpfont.tmp" or backups like "Foo_14.cpfont~" aren't accepted.
|
||||
static constexpr char kExt[] = ".cpfont";
|
||||
static constexpr size_t kExtLen = sizeof(kExt) - 1;
|
||||
const size_t nameLen = strlen(filename);
|
||||
if (nameLen <= kExtLen) return false;
|
||||
if (strcmp(filename + nameLen - kExtLen, kExt) != 0) return false;
|
||||
const char* ext = filename + nameLen - kExtLen;
|
||||
|
||||
size_t baseLen = ext - filename;
|
||||
if (baseLen == 0 || baseLen > 127) return false;
|
||||
|
||||
char base[128];
|
||||
memcpy(base, filename, baseLen);
|
||||
base[baseLen] = '\0';
|
||||
|
||||
char* lastUnderscore = strrchr(base, '_');
|
||||
if (!lastUnderscore || lastUnderscore == base) return false;
|
||||
|
||||
const char* sizeStr = lastUnderscore + 1;
|
||||
char* endPtr;
|
||||
long sizeVal = strtol(sizeStr, &endPtr, 10);
|
||||
if (endPtr == sizeStr || *endPtr != '\0' || sizeVal < 1 || sizeVal > 255) return false;
|
||||
size = static_cast<uint8_t>(sizeVal);
|
||||
style = 0;
|
||||
return true;
|
||||
}
|
||||
|
||||
void SdCardFontRegistry::scanDirectory(const char* dirPath, SdCardFontFamilyInfo& family) {
|
||||
FsFile dir = Storage.open(dirPath);
|
||||
if (!dir || !dir.isDirectory()) return;
|
||||
|
||||
char nameBuffer[128];
|
||||
while (true) {
|
||||
FsFile entry = dir.openNextFile();
|
||||
if (!entry) break;
|
||||
if (entry.isDirectory()) {
|
||||
entry.close();
|
||||
continue;
|
||||
}
|
||||
|
||||
entry.getName(nameBuffer, sizeof(nameBuffer));
|
||||
entry.close();
|
||||
|
||||
// Skip macOS resource fork files (._*) and other hidden files
|
||||
if (nameBuffer[0] == '.' || nameBuffer[0] == '_') continue;
|
||||
|
||||
uint8_t size, style;
|
||||
if (!parseFilename(nameBuffer, size, style)) continue;
|
||||
|
||||
SdCardFontFileInfo info;
|
||||
info.path = std::string(dirPath) + "/" + nameBuffer;
|
||||
info.pointSize = size;
|
||||
info.style = style;
|
||||
family.files.push_back(std::move(info));
|
||||
}
|
||||
dir.close();
|
||||
}
|
||||
|
||||
bool SdCardFontRegistry::discover() {
|
||||
families_.clear();
|
||||
families_.reserve(MAX_SD_FAMILIES);
|
||||
|
||||
FsFile root = Storage.open(FONTS_DIR);
|
||||
if (!root) {
|
||||
LOG_DBG("SDREG", "Fonts directory not found: %s", FONTS_DIR);
|
||||
return false;
|
||||
}
|
||||
if (!root.isDirectory()) {
|
||||
LOG_ERR("SDREG", "Fonts path is not a directory: %s", FONTS_DIR);
|
||||
root.close();
|
||||
return false;
|
||||
}
|
||||
|
||||
char nameBuffer[128];
|
||||
while (true) {
|
||||
FsFile entry = root.openNextFile();
|
||||
if (!entry) break;
|
||||
if (entry.isDirectory()) {
|
||||
// Subdirectory = font family
|
||||
entry.getName(nameBuffer, sizeof(nameBuffer));
|
||||
entry.close();
|
||||
|
||||
// Skip hidden/system directories (macOS ._*, .Trashes, etc.)
|
||||
if (nameBuffer[0] == '.' || nameBuffer[0] == '_') continue;
|
||||
|
||||
SdCardFontFamilyInfo family;
|
||||
family.name = nameBuffer;
|
||||
std::string subDirPath = std::string(FONTS_DIR) + "/" + nameBuffer;
|
||||
scanDirectory(subDirPath.c_str(), family);
|
||||
|
||||
if (!family.files.empty()) {
|
||||
families_.push_back(std::move(family));
|
||||
LOG_DBG("SDREG", "Found family: %s (%d files)", families_.back().name.c_str(),
|
||||
static_cast<int>(families_.back().files.size()));
|
||||
}
|
||||
} else {
|
||||
entry.close();
|
||||
}
|
||||
}
|
||||
root.close();
|
||||
|
||||
// Sort families alphabetically
|
||||
std::sort(families_.begin(), families_.end(),
|
||||
[](const SdCardFontFamilyInfo& a, const SdCardFontFamilyInfo& b) { return a.name < b.name; });
|
||||
|
||||
// Cap at MAX_SD_FAMILIES
|
||||
if (static_cast<int>(families_.size()) > MAX_SD_FAMILIES) {
|
||||
families_.resize(MAX_SD_FAMILIES);
|
||||
}
|
||||
|
||||
LOG_DBG("SDREG", "Discovery complete: %d families", static_cast<int>(families_.size()));
|
||||
return !families_.empty();
|
||||
}
|
||||
|
||||
const SdCardFontFamilyInfo* SdCardFontRegistry::findFamily(const std::string& name) const {
|
||||
for (const auto& f : families_) {
|
||||
if (f.name == name) return &f;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
int SdCardFontRegistry::getFamilyIndex(const std::string& name) const {
|
||||
for (int i = 0; i < static_cast<int>(families_.size()); i++) {
|
||||
if (families_[i].name == name) return i;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
struct SdCardFontFileInfo {
|
||||
std::string path; // v4 on-disk naming: "/.crosspoint/fonts/<Family>/<Family>_<size>.cpfont"
|
||||
// e.g. "/.crosspoint/fonts/NotoSansCJK/NotoSansCJK_14.cpfont"
|
||||
uint8_t pointSize; // parsed from filename: 14
|
||||
uint8_t style; // always 0 in v4 (all 4 styles bundled in one file);
|
||||
// kept for potential future formats
|
||||
};
|
||||
|
||||
struct SdCardFontFamilyInfo {
|
||||
std::string name; // directory name, e.g. "NotoSansCJK"
|
||||
std::vector<SdCardFontFileInfo> files;
|
||||
|
||||
const SdCardFontFileInfo* findFile(uint8_t size, uint8_t style = 0) const;
|
||||
bool hasSize(uint8_t size) const;
|
||||
std::vector<uint8_t> availableSizes() const;
|
||||
};
|
||||
|
||||
class SdCardFontRegistry {
|
||||
public:
|
||||
static constexpr int MAX_SD_FAMILIES = 128;
|
||||
static constexpr const char* FONTS_DIR = "/.crosspoint/fonts";
|
||||
|
||||
// Scan SD card, populate families_. Returns true if any families found.
|
||||
bool discover();
|
||||
|
||||
const std::vector<SdCardFontFamilyInfo>& getFamilies() const { return families_; }
|
||||
const SdCardFontFamilyInfo* findFamily(const std::string& name) const;
|
||||
int getFamilyIndex(const std::string& name) const;
|
||||
int getFamilyCount() const { return static_cast<int>(families_.size()); }
|
||||
|
||||
private:
|
||||
std::vector<SdCardFontFamilyInfo> families_; // sorted alphabetically
|
||||
|
||||
static bool parseFilename(const char* filename, uint8_t& size, uint8_t& style);
|
||||
void scanDirectory(const char* dirPath, SdCardFontFamilyInfo& family);
|
||||
};
|
||||
@@ -0,0 +1,882 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Generate .cpfont binary files for SD card font loading.
|
||||
|
||||
Outputs binary .cpfont files containing glyph metadata and uncompressed
|
||||
2-bit bitmaps, matching the EpdFontData/EpdGlyph/EpdUnicodeInterval struct
|
||||
layout on the ESP32-C3 (little-endian, RISC-V).
|
||||
|
||||
Usage:
|
||||
# Single file with specific presets
|
||||
python fontconvert_sdcard.py \\
|
||||
--intervals latin-ext,greek,cyrillic \\
|
||||
--size 14 --style regular \\
|
||||
NotoSans-Regular.ttf \\
|
||||
-o NotoSansExt_14.cpfont
|
||||
|
||||
# All 4 sizes at once
|
||||
python fontconvert_sdcard.py \\
|
||||
--intervals cjk \\
|
||||
--sizes 12,14,16,18 --style regular \\
|
||||
NotoSansCJKsc-Regular.otf \\
|
||||
--output-dir NotoSansCJK/
|
||||
|
||||
"""
|
||||
|
||||
import freetype
|
||||
import struct
|
||||
import sys
|
||||
import os
|
||||
import math
|
||||
import argparse
|
||||
from collections import namedtuple
|
||||
|
||||
from fontTools.ttLib import TTFont
|
||||
|
||||
# --- Unicode interval presets ---
|
||||
|
||||
INTERVAL_PRESETS = {
|
||||
"ascii": [(0x0020, 0x007E)],
|
||||
"latin1": [(0x0080, 0x00FF)],
|
||||
"latin-ext": [(0x0020, 0x007E), (0x0080, 0x00FF), (0x0100, 0x024F),
|
||||
(0x1E00, 0x1EFF), (0x2000, 0x206F)],
|
||||
"greek": [(0x0370, 0x03FF), (0x1F00, 0x1FFF)],
|
||||
"cyrillic": [(0x0400, 0x04FF), (0x0500, 0x052F)],
|
||||
"georgian": [(0x10A0, 0x10FF), (0x2D00, 0x2D2F)],
|
||||
"armenian": [(0x0530, 0x058F)],
|
||||
"ethiopic": [(0x1200, 0x137F), (0x1380, 0x139F), (0x2D80, 0x2DDF)],
|
||||
"vietnamese": [(0x01A0, 0x01B0), (0x1EA0, 0x1EF9)],
|
||||
"punctuation": [(0x2000, 0x206F)],
|
||||
"cjk": [(0x3000, 0x303F), (0x3040, 0x309F), (0x30A0, 0x30FF),
|
||||
(0x4E00, 0x9FFF), (0xF900, 0xFAFF), (0xFF00, 0xFFEF)],
|
||||
"hangul": [(0xAC00, 0xD7AF), (0x1100, 0x11FF), (0x3130, 0x318F)],
|
||||
# Matches the built-in font intervals from fontconvert.py exactly
|
||||
"builtin": [(0x0000, 0x007F), (0x0080, 0x00FF), (0x0100, 0x017F),
|
||||
(0x01A0, 0x01A1), (0x01AF, 0x01B0), (0x01C4, 0x021F),
|
||||
(0x0300, 0x036F), (0x0400, 0x04FF),
|
||||
(0x1EA0, 0x1EF9), (0x2000, 0x206F), (0x20A0, 0x20CF),
|
||||
(0x2070, 0x209F), (0x2190, 0x21FF), (0x2200, 0x22FF),
|
||||
(0xFB00, 0xFB06)],
|
||||
}
|
||||
|
||||
|
||||
def resolve_intervals(preset_str):
|
||||
"""Resolve comma-separated preset names into a merged, sorted, deduplicated interval list."""
|
||||
all_intervals = []
|
||||
for name in preset_str.split(","):
|
||||
name = name.strip().lower()
|
||||
if name not in INTERVAL_PRESETS:
|
||||
print(f"Error: unknown interval preset '{name}'", file=sys.stderr)
|
||||
print(f"Available presets: {', '.join(sorted(INTERVAL_PRESETS.keys()))}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
all_intervals.extend(INTERVAL_PRESETS[name])
|
||||
|
||||
# Always add replacement character
|
||||
all_intervals.append((0xFFFD, 0xFFFD))
|
||||
|
||||
# Sort and merge overlapping/adjacent intervals
|
||||
all_intervals.sort()
|
||||
merged = []
|
||||
for start, end in all_intervals:
|
||||
if merged and start <= merged[-1][1] + 1:
|
||||
merged[-1] = (merged[-1][0], max(merged[-1][1], end))
|
||||
else:
|
||||
merged.append((start, end))
|
||||
return merged
|
||||
|
||||
|
||||
GlyphProps = namedtuple("GlyphProps", [
|
||||
"width", "height", "advance_x", "left", "top", "data_length", "data_offset", "code_point"
|
||||
])
|
||||
|
||||
# Intermediate data from rasterizing one font style
|
||||
StyleRasterData = namedtuple("StyleRasterData", [
|
||||
"style_id", # 0=regular, 1=bold, 2=italic, 3=bolditalic
|
||||
"intervals", # validated intervals [(start, end), ...]
|
||||
"all_glyphs", # [(GlyphProps, packed_bytes), ...]
|
||||
"total_bitmap_size", # int
|
||||
"advanceY", "ascender", "descender",
|
||||
"kern_left_classes", "kern_right_classes", "kern_matrix",
|
||||
"kern_left_class_count", "kern_right_class_count",
|
||||
"ligature_pairs",
|
||||
])
|
||||
|
||||
|
||||
def norm_floor(val):
|
||||
return int(math.floor(val / (1 << 6)))
|
||||
|
||||
|
||||
def norm_ceil(val):
|
||||
return int(math.ceil(val / (1 << 6)))
|
||||
|
||||
|
||||
# Fixed-point (fp4) output conventions (must match EpdFontData.h / fp4 namespace):
|
||||
#
|
||||
# advanceX 12.4 unsigned fixed-point (uint16_t).
|
||||
# 12 integer bits, 4 fractional bits = 1/16-pixel resolution.
|
||||
# Encoded from FreeType's 16.16 linearHoriAdvance.
|
||||
#
|
||||
# kernMatrix 4.4 signed fixed-point (int8_t).
|
||||
# 4 integer bits, 4 fractional bits = 1/16-pixel resolution.
|
||||
# Range: -8.0 to +7.9375 pixels.
|
||||
# Encoded from font design-unit kerning values.
|
||||
#
|
||||
# Both share 4 fractional bits so the renderer can add them directly into a
|
||||
# single int32_t accumulator and defer rounding until pixel placement.
|
||||
|
||||
def fp4_from_ft16_16(val):
|
||||
"""Convert FreeType 16.16 fixed-point to 12.4 fixed-point with rounding."""
|
||||
return (val + (1 << 11)) >> 12
|
||||
|
||||
def fp4_from_design_units(du, scale):
|
||||
"""Convert a font design-unit value to 4.4 fixed-point, clamped to int8_t.
|
||||
|
||||
Multiplies by scale (ppem / units_per_em) and shifts into 4 fractional
|
||||
bits. The result is rounded to nearest and clamped to [-128, 127].
|
||||
"""
|
||||
raw = round(du * scale * 16)
|
||||
return max(-128, min(127, raw))
|
||||
|
||||
|
||||
# Standard Unicode ligature codepoints for known input sequences.
|
||||
# Used as a fallback when the GSUB substitute glyph has no cmap entry.
|
||||
STANDARD_LIGATURE_MAP = {
|
||||
(0x66, 0x66): 0xFB00, # ff
|
||||
(0x66, 0x69): 0xFB01, # fi
|
||||
(0x66, 0x6C): 0xFB02, # fl
|
||||
(0x66, 0x66, 0x69): 0xFB03, # ffi
|
||||
(0x66, 0x66, 0x6C): 0xFB04, # ffl
|
||||
(0x17F, 0x74): 0xFB05, # long-s + t
|
||||
(0x73, 0x74): 0xFB06, # st
|
||||
}
|
||||
|
||||
|
||||
def _extract_pairpos_subtable(subtable, glyph_to_cp, raw_kern):
|
||||
"""Extract kerning from a PairPos subtable (Format 1 or 2)."""
|
||||
if subtable.Format == 1:
|
||||
# Individual pairs
|
||||
for i, coverage_glyph in enumerate(subtable.Coverage.glyphs):
|
||||
if coverage_glyph not in glyph_to_cp:
|
||||
continue
|
||||
pair_set = subtable.PairSet[i]
|
||||
for pvr in pair_set.PairValueRecord:
|
||||
if pvr.SecondGlyph not in glyph_to_cp:
|
||||
continue
|
||||
xa = 0
|
||||
if hasattr(pvr, 'Value1') and pvr.Value1:
|
||||
xa = getattr(pvr.Value1, 'XAdvance', 0) or 0
|
||||
if xa != 0:
|
||||
key = (coverage_glyph, pvr.SecondGlyph)
|
||||
raw_kern[key] = raw_kern.get(key, 0) + xa
|
||||
elif subtable.Format == 2:
|
||||
# Class-based pairs — iterate by class, not by glyph, to avoid
|
||||
# O(glyphs²) explosion for CJK fonts with many requested glyphs.
|
||||
class_def1 = subtable.ClassDef1.classDefs if subtable.ClassDef1 else {}
|
||||
class_def2 = subtable.ClassDef2.classDefs if subtable.ClassDef2 else {}
|
||||
coverage_set = set(subtable.Coverage.glyphs)
|
||||
|
||||
# Build reverse mappings: class_id -> list of glyph names
|
||||
left_by_class = {} # only glyphs in coverage AND glyph_to_cp
|
||||
for glyph in glyph_to_cp:
|
||||
if glyph not in coverage_set:
|
||||
continue
|
||||
c1 = class_def1.get(glyph, 0)
|
||||
left_by_class.setdefault(c1, []).append(glyph)
|
||||
|
||||
right_by_class = {} # all glyphs in glyph_to_cp
|
||||
for glyph in glyph_to_cp:
|
||||
c2 = class_def2.get(glyph, 0)
|
||||
right_by_class.setdefault(c2, []).append(glyph)
|
||||
|
||||
# Iterate class pairs (typically << glyph pairs)
|
||||
for c1, class1_rec in enumerate(subtable.Class1Record):
|
||||
if c1 not in left_by_class:
|
||||
continue
|
||||
for c2, c2_rec in enumerate(class1_rec.Class2Record):
|
||||
xa = 0
|
||||
if hasattr(c2_rec, 'Value1') and c2_rec.Value1:
|
||||
xa = getattr(c2_rec.Value1, 'XAdvance', 0) or 0
|
||||
if xa == 0:
|
||||
continue
|
||||
if c2 not in right_by_class:
|
||||
continue
|
||||
for lg in left_by_class[c1]:
|
||||
for rg in right_by_class[c2]:
|
||||
key = (lg, rg)
|
||||
raw_kern[key] = raw_kern.get(key, 0) + xa
|
||||
|
||||
|
||||
def extract_kerning_fonttools(font_path, codepoints, ppem):
|
||||
"""Extract kerning pairs from a font file using fonttools.
|
||||
|
||||
Returns dict of {(leftCp, rightCp): pixel_adjust} for the given
|
||||
codepoints. Values are scaled from font design units to integer
|
||||
pixels at ppem.
|
||||
"""
|
||||
font = TTFont(font_path)
|
||||
units_per_em = font['head'].unitsPerEm
|
||||
cmap = font.getBestCmap() or {}
|
||||
|
||||
# Build glyph_name -> [codepoints] map (preserves aliases where multiple
|
||||
# codepoints share a glyph, e.g. space/nbsp)
|
||||
glyph_to_cps = {}
|
||||
for cp in codepoints:
|
||||
gname = cmap.get(cp)
|
||||
if gname:
|
||||
glyph_to_cps.setdefault(gname, []).append(cp)
|
||||
# Flat dict for membership checks and subtable extraction (uses keys only)
|
||||
glyph_to_cp = glyph_to_cps
|
||||
|
||||
# Collect raw kerning values in font design units
|
||||
raw_kern = {} # (left_glyph_name, right_glyph_name) -> design_units
|
||||
|
||||
# 1. Legacy kern table
|
||||
if 'kern' in font:
|
||||
for subtable in font['kern'].kernTables:
|
||||
if hasattr(subtable, 'kernTable'):
|
||||
for (lg, rg), val in subtable.kernTable.items():
|
||||
if lg in glyph_to_cp and rg in glyph_to_cp:
|
||||
raw_kern[(lg, rg)] = raw_kern.get((lg, rg), 0) + val
|
||||
|
||||
# 2. GPOS 'kern' feature
|
||||
if 'GPOS' in font:
|
||||
gpos = font['GPOS'].table
|
||||
kern_lookup_indices = set()
|
||||
if gpos.FeatureList:
|
||||
for fr in gpos.FeatureList.FeatureRecord:
|
||||
if fr.FeatureTag == 'kern':
|
||||
kern_lookup_indices.update(fr.Feature.LookupListIndex)
|
||||
for li in kern_lookup_indices:
|
||||
lookup = gpos.LookupList.Lookup[li]
|
||||
for st in lookup.SubTable:
|
||||
actual = st
|
||||
# Unwrap Extension (lookup type 9) wrappers
|
||||
if lookup.LookupType == 9 and hasattr(st, 'ExtSubTable'):
|
||||
actual = st.ExtSubTable
|
||||
if hasattr(actual, 'Format'):
|
||||
_extract_pairpos_subtable(actual, glyph_to_cp, raw_kern)
|
||||
|
||||
font.close()
|
||||
|
||||
# Scale design-unit kerning values to 4.4 fixed-point pixels.
|
||||
# Expand glyph aliases: if multiple codepoints share a glyph, emit kern
|
||||
# pairs for all codepoint combinations.
|
||||
scale = ppem / units_per_em
|
||||
result = {} # (leftCp, rightCp) -> 4.4 fixed-point adjust
|
||||
for (lg, rg), du in raw_kern.items():
|
||||
adjust = fp4_from_design_units(du, scale)
|
||||
if adjust != 0:
|
||||
for lcp in glyph_to_cps[lg]:
|
||||
for rcp in glyph_to_cps[rg]:
|
||||
result[(lcp, rcp)] = adjust
|
||||
return result
|
||||
|
||||
|
||||
def derive_kern_classes(kern_map):
|
||||
"""Derive class-based kerning from a pair map.
|
||||
|
||||
Returns (kern_left_classes, kern_right_classes, kern_matrix,
|
||||
kern_left_class_count, kern_right_class_count) where:
|
||||
- kern_left_classes: sorted list of (codepoint, classId) tuples
|
||||
- kern_right_classes: sorted list of (codepoint, classId) tuples
|
||||
- kern_matrix: flat list of int8 values (left_class_count * right_class_count)
|
||||
- kern_left_class_count: number of distinct left classes
|
||||
- kern_right_class_count: number of distinct right classes
|
||||
"""
|
||||
if not kern_map:
|
||||
return [], [], [], 0, 0
|
||||
|
||||
all_left_cps = {lcp for lcp, _ in kern_map}
|
||||
all_right_cps = {rcp for _, rcp in kern_map}
|
||||
|
||||
sorted_right_cps = sorted(all_right_cps)
|
||||
sorted_left_cps = sorted(all_left_cps)
|
||||
|
||||
# Group left codepoints by identical adjustment row
|
||||
left_profile_to_class = {}
|
||||
left_class_map = {}
|
||||
left_class_id = 1
|
||||
for lcp in sorted(all_left_cps):
|
||||
row = tuple(kern_map.get((lcp, rcp), 0) for rcp in sorted_right_cps)
|
||||
if row not in left_profile_to_class:
|
||||
left_profile_to_class[row] = left_class_id
|
||||
left_class_id += 1
|
||||
left_class_map[lcp] = left_profile_to_class[row]
|
||||
|
||||
# Group right codepoints by identical adjustment column
|
||||
right_profile_to_class = {}
|
||||
right_class_map = {}
|
||||
right_class_id = 1
|
||||
for rcp in sorted(all_right_cps):
|
||||
col = tuple(kern_map.get((lcp, rcp), 0) for lcp in sorted_left_cps)
|
||||
if col not in right_profile_to_class:
|
||||
right_profile_to_class[col] = right_class_id
|
||||
right_class_id += 1
|
||||
right_class_map[rcp] = right_profile_to_class[col]
|
||||
|
||||
kern_left_class_count = left_class_id - 1
|
||||
kern_right_class_count = right_class_id - 1
|
||||
|
||||
if kern_left_class_count > 255 or kern_right_class_count > 255:
|
||||
print(f"WARNING: kerning class count exceeds uint8_t range "
|
||||
f"(left={kern_left_class_count}, right={kern_right_class_count}), "
|
||||
f"dropping kerning for this style",
|
||||
file=sys.stderr)
|
||||
return ([], [], [], 0, 0)
|
||||
|
||||
# Build the class x class matrix
|
||||
kern_matrix = [0] * (kern_left_class_count * kern_right_class_count)
|
||||
for (lcp, rcp), adjust in kern_map.items():
|
||||
lc = left_class_map[lcp] - 1
|
||||
rc = right_class_map[rcp] - 1
|
||||
kern_matrix[lc * kern_right_class_count + rc] = adjust
|
||||
|
||||
# Build sorted class entry lists
|
||||
kern_left_classes = sorted(left_class_map.items())
|
||||
kern_right_classes = sorted(right_class_map.items())
|
||||
|
||||
return (kern_left_classes, kern_right_classes, kern_matrix,
|
||||
kern_left_class_count, kern_right_class_count)
|
||||
|
||||
|
||||
def extract_ligatures_fonttools(font_path, codepoints):
|
||||
"""Extract ligature substitution pairs from a font file using fonttools.
|
||||
|
||||
Returns list of (packed_pair, ligature_codepoint) for the given codepoints.
|
||||
Multi-character ligatures are decomposed into chained pairs.
|
||||
"""
|
||||
font = TTFont(font_path)
|
||||
cmap = font.getBestCmap() or {}
|
||||
|
||||
# Build glyph_name -> codepoint and codepoint -> glyph_name maps
|
||||
glyph_to_cp = {}
|
||||
cp_to_glyph = {}
|
||||
for cp, gname in cmap.items():
|
||||
glyph_to_cp[gname] = cp
|
||||
cp_to_glyph[cp] = gname
|
||||
|
||||
# Collect raw ligature rules: (sequence_of_codepoints) -> ligature_codepoint
|
||||
raw_ligatures = {} # tuple of codepoints -> ligature codepoint
|
||||
|
||||
if 'GSUB' in font:
|
||||
gsub = font['GSUB'].table
|
||||
|
||||
LIGATURE_FEATURES = ('liga', 'rlig')
|
||||
liga_lookup_indices = set()
|
||||
if gsub.FeatureList:
|
||||
for fr in gsub.FeatureList.FeatureRecord:
|
||||
if fr.FeatureTag in LIGATURE_FEATURES:
|
||||
liga_lookup_indices.update(fr.Feature.LookupListIndex)
|
||||
|
||||
for li in liga_lookup_indices:
|
||||
lookup = gsub.LookupList.Lookup[li]
|
||||
for st in lookup.SubTable:
|
||||
actual = st
|
||||
# Unwrap Extension (lookup type 7) wrappers
|
||||
if lookup.LookupType == 7 and hasattr(st, 'ExtSubTable'):
|
||||
actual = st.ExtSubTable
|
||||
# LigatureSubst is lookup type 4
|
||||
if not hasattr(actual, 'ligatures'):
|
||||
continue
|
||||
for first_glyph, ligature_list in actual.ligatures.items():
|
||||
if first_glyph not in glyph_to_cp:
|
||||
continue
|
||||
first_cp = glyph_to_cp[first_glyph]
|
||||
for lig in ligature_list:
|
||||
component_cps = []
|
||||
valid = True
|
||||
for comp_glyph in lig.Component:
|
||||
if comp_glyph not in glyph_to_cp:
|
||||
valid = False
|
||||
break
|
||||
component_cps.append(glyph_to_cp[comp_glyph])
|
||||
if not valid:
|
||||
continue
|
||||
seq = tuple([first_cp] + component_cps)
|
||||
if lig.LigGlyph in glyph_to_cp:
|
||||
lig_cp = glyph_to_cp[lig.LigGlyph]
|
||||
elif seq in STANDARD_LIGATURE_MAP:
|
||||
lig_cp = STANDARD_LIGATURE_MAP[seq]
|
||||
else:
|
||||
seq_str = ', '.join(f'U+{cp:04X}' for cp in seq)
|
||||
print(f"ligatures: WARNING: dropping ligature ({seq_str}) -> "
|
||||
f"glyph '{lig.LigGlyph}': output glyph has no cmap entry "
|
||||
f"and input sequence is not in STANDARD_LIGATURE_MAP",
|
||||
file=sys.stderr)
|
||||
continue
|
||||
raw_ligatures[seq] = lig_cp
|
||||
|
||||
font.close()
|
||||
|
||||
# Filter: only keep ligatures where all input and output codepoints are
|
||||
# in our generated glyph set
|
||||
codepoints_set = set(codepoints)
|
||||
filtered = {}
|
||||
for seq, lig_cp in raw_ligatures.items():
|
||||
if lig_cp not in codepoints_set:
|
||||
continue
|
||||
if all(cp in codepoints_set for cp in seq):
|
||||
filtered[seq] = lig_cp
|
||||
|
||||
# Decompose into chained pairs
|
||||
pairs = []
|
||||
# First pass: collect all 2-codepoint ligatures
|
||||
two_char = {seq: lig_cp for seq, lig_cp in filtered.items() if len(seq) == 2}
|
||||
for seq, lig_cp in two_char.items():
|
||||
packed = (seq[0] << 16) | seq[1]
|
||||
pairs.append((packed, lig_cp))
|
||||
|
||||
# Second pass: decompose 3+ codepoint ligatures into chained pairs
|
||||
for seq, lig_cp in filtered.items():
|
||||
if len(seq) < 3:
|
||||
continue
|
||||
prefix = seq[:-1]
|
||||
last_cp = seq[-1]
|
||||
if prefix in filtered:
|
||||
intermediate_cp = filtered[prefix]
|
||||
packed = (intermediate_cp << 16) | last_cp
|
||||
pairs.append((packed, lig_cp))
|
||||
else:
|
||||
print(f"ligatures: skipping {len(seq)}-char ligature "
|
||||
f"({', '.join(f'U+{cp:04X}' for cp in seq)}) -> U+{lig_cp:04X}: "
|
||||
f"no intermediate ligature for prefix", file=sys.stderr)
|
||||
|
||||
# Sort by packed pair key — on-device lookup uses binary search
|
||||
pairs.sort(key=lambda p: p[0])
|
||||
return pairs
|
||||
|
||||
|
||||
def rasterize_font_style(fontfile, size, intervals, style_id=0, force_autohint=False):
|
||||
"""Rasterize all glyphs for one font style. Returns StyleRasterData."""
|
||||
style_names = {0: "regular", 1: "bold", 2: "italic", 3: "bolditalic"}
|
||||
style_label = style_names.get(style_id, str(style_id))
|
||||
|
||||
face = freetype.Face(fontfile)
|
||||
load_flags = freetype.FT_LOAD_RENDER
|
||||
if force_autohint:
|
||||
load_flags |= freetype.FT_LOAD_FORCE_AUTOHINT
|
||||
|
||||
def load_glyph(code_point):
|
||||
glyph_index = face.get_char_index(code_point)
|
||||
if glyph_index > 0:
|
||||
face.load_glyph(glyph_index, load_flags)
|
||||
return face
|
||||
return None
|
||||
|
||||
# Validate intervals: remove codepoints not present in the font
|
||||
print(f" [{style_label}] Validating intervals against font...", file=sys.stderr)
|
||||
validated_intervals = []
|
||||
for i_start, i_end in intervals:
|
||||
start = i_start
|
||||
for code_point in range(i_start, i_end + 1):
|
||||
f = load_glyph(code_point)
|
||||
if f is None:
|
||||
if start < code_point:
|
||||
validated_intervals.append((start, code_point - 1))
|
||||
start = code_point + 1
|
||||
if start <= i_end:
|
||||
validated_intervals.append((start, i_end))
|
||||
|
||||
intervals = validated_intervals
|
||||
total_glyphs = sum(end - start + 1 for start, end in intervals)
|
||||
print(f" [{style_label}] Validated: {len(intervals)} intervals, {total_glyphs} glyphs", file=sys.stderr)
|
||||
|
||||
# Set font size at 150 DPI (matching fontconvert.py)
|
||||
face.set_char_size(size << 6, size << 6, 150, 150)
|
||||
|
||||
# Rasterize all glyphs
|
||||
total_bitmap_size = 0
|
||||
all_glyphs = []
|
||||
|
||||
for i_start, i_end in intervals:
|
||||
for code_point in range(i_start, i_end + 1):
|
||||
f = load_glyph(code_point)
|
||||
if f is None:
|
||||
glyph = GlyphProps(0, 0, 0, 0, 0, 0, total_bitmap_size, code_point)
|
||||
all_glyphs.append((glyph, b''))
|
||||
continue
|
||||
|
||||
bitmap = f.glyph.bitmap
|
||||
|
||||
# Build 4-bit greyscale bitmap (same logic as fontconvert.py)
|
||||
pixels4g = []
|
||||
px = 0
|
||||
for i, v in enumerate(bitmap.buffer):
|
||||
x = i % bitmap.width
|
||||
if x % 2 == 0:
|
||||
px = (v >> 4)
|
||||
else:
|
||||
px = px | (v & 0xF0)
|
||||
pixels4g.append(px)
|
||||
px = 0
|
||||
if x == bitmap.width - 1 and bitmap.width % 2 > 0:
|
||||
pixels4g.append(px)
|
||||
px = 0
|
||||
|
||||
# Downsample to 2-bit bitmap
|
||||
pixels2b = []
|
||||
px = 0
|
||||
pitch = (bitmap.width // 2) + (bitmap.width % 2)
|
||||
for y in range(bitmap.rows):
|
||||
for x in range(bitmap.width):
|
||||
px = px << 2
|
||||
bm = pixels4g[y * pitch + (x // 2)]
|
||||
bm = (bm >> ((x % 2) * 4)) & 0xF
|
||||
|
||||
if bm >= 12:
|
||||
px += 3
|
||||
elif bm >= 8:
|
||||
px += 2
|
||||
elif bm >= 4:
|
||||
px += 1
|
||||
|
||||
if (y * bitmap.width + x) % 4 == 3:
|
||||
pixels2b.append(px)
|
||||
px = 0
|
||||
if (bitmap.width * bitmap.rows) % 4 != 0:
|
||||
px = px << (4 - (bitmap.width * bitmap.rows) % 4) * 2
|
||||
pixels2b.append(px)
|
||||
|
||||
packed = bytes(pixels2b)
|
||||
glyph = GlyphProps(
|
||||
width=bitmap.width,
|
||||
height=bitmap.rows,
|
||||
advance_x=fp4_from_ft16_16(f.glyph.linearHoriAdvance),
|
||||
left=f.glyph.bitmap_left,
|
||||
top=f.glyph.bitmap_top,
|
||||
data_length=len(packed),
|
||||
data_offset=total_bitmap_size,
|
||||
code_point=code_point,
|
||||
)
|
||||
total_bitmap_size += len(packed)
|
||||
all_glyphs.append((glyph, packed))
|
||||
|
||||
# Get font metrics from pipe character (same heuristic as fontconvert.py)
|
||||
load_glyph(ord('|'))
|
||||
|
||||
advanceY = norm_ceil(face.size.height)
|
||||
ascender = norm_ceil(face.size.ascender)
|
||||
descender = norm_floor(face.size.descender)
|
||||
|
||||
print(f" [{style_label}] Metrics: advanceY={advanceY}, ascender={ascender}, descender={descender}", file=sys.stderr)
|
||||
print(f" [{style_label}] Bitmap: {total_bitmap_size} bytes ({total_bitmap_size / 1024:.1f} KB)", file=sys.stderr)
|
||||
|
||||
# --- Extract kerning and ligatures ---
|
||||
ppem = size * 150.0 / 72.0
|
||||
all_cps = set(g.code_point for g, _ in all_glyphs)
|
||||
|
||||
kern_map = extract_kerning_fonttools(fontfile, all_cps, ppem)
|
||||
print(f" [{style_label}] Kerning: {len(kern_map)} pairs extracted", file=sys.stderr)
|
||||
|
||||
(kern_left_classes, kern_right_classes, kern_matrix,
|
||||
kern_left_class_count, kern_right_class_count) = derive_kern_classes(kern_map)
|
||||
|
||||
if kern_map:
|
||||
matrix_size = kern_left_class_count * kern_right_class_count
|
||||
entries_size = (len(kern_left_classes) + len(kern_right_classes)) * 3
|
||||
print(f" [{style_label}] Kerning classes: {kern_left_class_count} left, {kern_right_class_count} right, "
|
||||
f"{matrix_size + entries_size} bytes", file=sys.stderr)
|
||||
|
||||
ligature_pairs = extract_ligatures_fonttools(fontfile, all_cps)
|
||||
if len(ligature_pairs) > 255:
|
||||
print(f" [{style_label}] WARNING: {len(ligature_pairs)} ligature pairs exceeds uint8_t max (255), truncating",
|
||||
file=sys.stderr)
|
||||
ligature_pairs = ligature_pairs[:255]
|
||||
print(f" [{style_label}] Ligatures: {len(ligature_pairs)} pairs", file=sys.stderr)
|
||||
|
||||
return StyleRasterData(
|
||||
style_id=style_id,
|
||||
intervals=intervals,
|
||||
all_glyphs=all_glyphs,
|
||||
total_bitmap_size=total_bitmap_size,
|
||||
advanceY=advanceY,
|
||||
ascender=ascender,
|
||||
descender=descender,
|
||||
kern_left_classes=kern_left_classes,
|
||||
kern_right_classes=kern_right_classes,
|
||||
kern_matrix=kern_matrix,
|
||||
kern_left_class_count=kern_left_class_count,
|
||||
kern_right_class_count=kern_right_class_count,
|
||||
ligature_pairs=ligature_pairs,
|
||||
)
|
||||
|
||||
|
||||
# --- Binary packing helpers ---
|
||||
|
||||
# EpdGlyph struct: 16 bytes, little-endian
|
||||
GLYPH_STRUCT_FORMAT = "<BBHhhH2xI"
|
||||
assert struct.calcsize(GLYPH_STRUCT_FORMAT) == 16
|
||||
|
||||
|
||||
def pack_style_sections(sd):
|
||||
"""Pack one StyleRasterData into binary section bytearrays.
|
||||
Returns (intervals_data, glyphs_data, kern_left, kern_right, kern_matrix, ligatures, bitmaps)."""
|
||||
intervals_data = bytearray()
|
||||
offset = 0
|
||||
for i_start, i_end in sd.intervals:
|
||||
intervals_data += struct.pack("<III", i_start, i_end, offset)
|
||||
offset += i_end - i_start + 1
|
||||
|
||||
glyphs_data = bytearray()
|
||||
for glyph, packed in sd.all_glyphs:
|
||||
glyphs_data += struct.pack(GLYPH_STRUCT_FORMAT,
|
||||
glyph.width, glyph.height, glyph.advance_x,
|
||||
glyph.left, glyph.top,
|
||||
glyph.data_length, glyph.data_offset)
|
||||
|
||||
kern_left_data = bytearray()
|
||||
for cp, cls in sd.kern_left_classes:
|
||||
kern_left_data += struct.pack("<HB", cp, cls)
|
||||
|
||||
kern_right_data = bytearray()
|
||||
for cp, cls in sd.kern_right_classes:
|
||||
kern_right_data += struct.pack("<HB", cp, cls)
|
||||
|
||||
kern_matrix_data = bytearray()
|
||||
if sd.kern_matrix:
|
||||
kern_matrix_data = bytearray(struct.pack(f"<{len(sd.kern_matrix)}b", *sd.kern_matrix))
|
||||
|
||||
ligature_data = bytearray()
|
||||
for packed_pair, lig_cp in sd.ligature_pairs:
|
||||
ligature_data += struct.pack("<II", packed_pair, lig_cp)
|
||||
|
||||
bitmap_data = bytearray()
|
||||
for glyph, packed in sd.all_glyphs:
|
||||
bitmap_data += packed
|
||||
assert len(bitmap_data) == sd.total_bitmap_size
|
||||
|
||||
return (intervals_data, glyphs_data, kern_left_data, kern_right_data,
|
||||
kern_matrix_data, ligature_data, bitmap_data)
|
||||
|
||||
|
||||
def style_sections_total_size(sections):
|
||||
"""Total byte size of all sections returned by pack_style_sections()."""
|
||||
return sum(len(s) for s in sections)
|
||||
|
||||
|
||||
# --- File writers ---
|
||||
|
||||
def generate_cpfont_multistyle(style_fonts, size, intervals, output_path,
|
||||
force_autohint=False):
|
||||
"""Generate a multi-style v4 .cpfont file.
|
||||
|
||||
style_fonts: dict of {style_id: fontfile_path} e.g. {0: "Regular.ttf", 2: "Italic.ttf"}
|
||||
"""
|
||||
MAGIC = b"CPFONT\x00\x00"
|
||||
VERSION = 4
|
||||
HEADER_SIZE = 32
|
||||
STYLE_TOC_ENTRY_SIZE = 32
|
||||
flags = 1 # always 2-bit greyscale
|
||||
style_count = len(style_fonts)
|
||||
|
||||
# Rasterize each style
|
||||
raster_data = {} # style_id -> StyleRasterData
|
||||
for style_id in sorted(style_fonts.keys()):
|
||||
fontfile = style_fonts[style_id]
|
||||
print(f" Rasterizing style {style_id}...", file=sys.stderr)
|
||||
raster_data[style_id] = rasterize_font_style(
|
||||
fontfile, size, intervals, style_id=style_id,
|
||||
force_autohint=force_autohint)
|
||||
|
||||
# Pack binary sections for each style
|
||||
packed_sections = {} # style_id -> tuple of section bytearrays
|
||||
for style_id, sd in raster_data.items():
|
||||
packed_sections[style_id] = pack_style_sections(sd)
|
||||
|
||||
# Calculate data offsets (after header + TOC)
|
||||
data_start = HEADER_SIZE + style_count * STYLE_TOC_ENTRY_SIZE
|
||||
current_offset = data_start
|
||||
|
||||
style_offsets = {} # style_id -> absolute file offset
|
||||
for style_id in sorted(packed_sections.keys()):
|
||||
style_offsets[style_id] = current_offset
|
||||
current_offset += style_sections_total_size(packed_sections[style_id])
|
||||
|
||||
# Build global header
|
||||
# V4 header: magic(8) + version(2) + flags(2) + styleCount(1) + reserved(19) = 32
|
||||
header = struct.pack("<8sHHB19s", MAGIC, VERSION, flags, style_count, bytes(19))
|
||||
assert len(header) == HEADER_SIZE
|
||||
|
||||
# Build style TOC entries
|
||||
# Each entry: styleId(1) + pad(3) + intervalCount(4) + glyphCount(4) +
|
||||
# advanceY(1) + ascender(2) + descender(2) + kernL(2) + kernR(2) +
|
||||
# kernLCls(1) + kernRCls(1) + ligCount(1) + dataOffset(4) + reserved(4) = 32
|
||||
STYLE_TOC_FORMAT = "<B3xIIBhhHHBBBI4x"
|
||||
assert struct.calcsize(STYLE_TOC_FORMAT) == STYLE_TOC_ENTRY_SIZE
|
||||
|
||||
toc_data = bytearray()
|
||||
for style_id in sorted(raster_data.keys()):
|
||||
sd = raster_data[style_id]
|
||||
if sd.advanceY > 255:
|
||||
print(f"ERROR: advanceY ({sd.advanceY}) exceeds uint8 range for "
|
||||
f"style {style_id} size {size}. This likely means the font "
|
||||
f"size is too large for this format.",
|
||||
file=sys.stderr)
|
||||
sys.exit(1)
|
||||
toc_data += struct.pack(STYLE_TOC_FORMAT,
|
||||
style_id,
|
||||
len(sd.intervals), len(sd.all_glyphs),
|
||||
sd.advanceY, sd.ascender, sd.descender,
|
||||
len(sd.kern_left_classes), len(sd.kern_right_classes),
|
||||
sd.kern_left_class_count, sd.kern_right_class_count,
|
||||
len(sd.ligature_pairs),
|
||||
style_offsets[style_id])
|
||||
|
||||
# Write output
|
||||
os.makedirs(os.path.dirname(output_path) if os.path.dirname(output_path) else ".", exist_ok=True)
|
||||
total_file_size = 0
|
||||
with open(output_path, "wb") as f:
|
||||
f.write(header)
|
||||
f.write(toc_data)
|
||||
for style_id in sorted(packed_sections.keys()):
|
||||
for section in packed_sections[style_id]:
|
||||
f.write(section)
|
||||
total_file_size = f.tell()
|
||||
|
||||
# Print summary
|
||||
print(f" Output: {output_path} (v4, {style_count} styles)", file=sys.stderr)
|
||||
print(f" Header+TOC: {HEADER_SIZE + len(toc_data)} bytes", file=sys.stderr)
|
||||
for style_id in sorted(raster_data.keys()):
|
||||
sd = raster_data[style_id]
|
||||
secs = packed_sections[style_id]
|
||||
style_names = {0: "regular", 1: "bold", 2: "italic", 3: "bolditalic"}
|
||||
sname = style_names.get(style_id, str(style_id))
|
||||
ssize = style_sections_total_size(secs)
|
||||
print(f" {sname}: {len(sd.all_glyphs)} glyphs, {len(sd.intervals)} intervals, "
|
||||
f"{ssize} bytes", file=sys.stderr)
|
||||
print(f" Total: {total_file_size} bytes ({total_file_size / 1024 / 1024:.2f} MB)", file=sys.stderr)
|
||||
return total_file_size
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Generate .cpfont files for SD card font loading.",
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
epilog=f"Available interval presets: {', '.join(sorted(INTERVAL_PRESETS.keys()))}"
|
||||
)
|
||||
|
||||
# Font file (positional, optional for multi-style mode)
|
||||
parser.add_argument("fontfile", nargs="?", default=None,
|
||||
help="Path to the font file (single-style mode).")
|
||||
parser.add_argument("--intervals", dest="intervals",
|
||||
help="Comma-separated interval presets (e.g., 'latin-ext,greek,cyrillic').")
|
||||
parser.add_argument("--size", type=int, dest="size",
|
||||
help="Single font size to generate.")
|
||||
parser.add_argument("--sizes", dest="sizes",
|
||||
help="Comma-separated sizes (e.g., '12,14,16,18').")
|
||||
parser.add_argument("--style", dest="style", default="regular",
|
||||
choices=["regular", "bold", "italic", "bolditalic"],
|
||||
help="Font style for single-style mode (default: regular).")
|
||||
parser.add_argument("--name", dest="name",
|
||||
help="Font family name for output filenames (default: derived from font filename).")
|
||||
parser.add_argument("--force-autohint", dest="force_autohint", action="store_true",
|
||||
help="Force FreeType auto-hinter instead of native font hinting.")
|
||||
parser.add_argument("-o", "--output", dest="output",
|
||||
help="Output file path (for single-size mode).")
|
||||
parser.add_argument("--output-dir", dest="output_dir",
|
||||
help="Output directory for multi-size mode.")
|
||||
parser.add_argument("--list-presets", action="store_true",
|
||||
help="List available interval presets and exit.")
|
||||
|
||||
# Multi-style mode: per-style font file arguments (generates v4 .cpfont)
|
||||
parser.add_argument("--regular", dest="font_regular",
|
||||
help="Font file for regular style (enables multi-style v4 mode).")
|
||||
parser.add_argument("--bold", dest="font_bold",
|
||||
help="Font file for bold style.")
|
||||
parser.add_argument("--italic", dest="font_italic",
|
||||
help="Font file for italic style.")
|
||||
parser.add_argument("--bolditalic", dest="font_bolditalic",
|
||||
help="Font file for bold-italic style.")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.list_presets:
|
||||
print("Available interval presets:")
|
||||
for name, ranges in sorted(INTERVAL_PRESETS.items()):
|
||||
total = sum(e - s + 1 for s, e in ranges)
|
||||
print(f" {name:15s} {len(ranges)} range(s), ~{total} codepoints")
|
||||
sys.exit(0)
|
||||
|
||||
# Detect multi-style mode
|
||||
style_fonts = {}
|
||||
if args.font_regular:
|
||||
style_fonts[0] = args.font_regular
|
||||
if args.font_bold:
|
||||
style_fonts[1] = args.font_bold
|
||||
if args.font_italic:
|
||||
style_fonts[2] = args.font_italic
|
||||
if args.font_bolditalic:
|
||||
style_fonts[3] = args.font_bolditalic
|
||||
|
||||
is_multistyle = len(style_fonts) > 0
|
||||
fontfile = args.fontfile
|
||||
|
||||
# Require --intervals
|
||||
if not args.intervals:
|
||||
print("Error: --intervals is required (e.g., --intervals latin-ext,greek,cyrillic)", file=sys.stderr)
|
||||
print(f"Available presets: {', '.join(sorted(INTERVAL_PRESETS.keys()))}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
intervals = resolve_intervals(args.intervals)
|
||||
|
||||
# Determine sizes
|
||||
if args.sizes:
|
||||
sizes = [int(s.strip()) for s in args.sizes.split(",")]
|
||||
elif args.size:
|
||||
sizes = [args.size]
|
||||
else:
|
||||
print("Error: --size or --sizes is required", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
# Validate early: single-style mode requires a font file
|
||||
if not is_multistyle and not fontfile:
|
||||
print("Error: fontfile is required in single-style mode", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
# Determine font name
|
||||
if args.name:
|
||||
font_name = args.name
|
||||
elif is_multistyle:
|
||||
# Derive from the regular font file
|
||||
ref_file = style_fonts[min(style_fonts.keys())]
|
||||
base = os.path.splitext(os.path.basename(ref_file))[0]
|
||||
for suffix in ["-Regular", "-Bold", "-Italic", "-BoldItalic",
|
||||
"-regular", "-bold", "-italic", "-bolditalic"]:
|
||||
if base.endswith(suffix):
|
||||
base = base[:-len(suffix)]
|
||||
break
|
||||
font_name = base
|
||||
else:
|
||||
base = os.path.splitext(os.path.basename(fontfile))[0]
|
||||
for suffix in ["-Regular", "-Bold", "-Italic", "-BoldItalic",
|
||||
"-regular", "-bold", "-italic", "-bolditalic"]:
|
||||
if base.endswith(suffix):
|
||||
base = base[:-len(suffix)]
|
||||
break
|
||||
font_name = base
|
||||
|
||||
if not is_multistyle:
|
||||
# Single font file provided: wrap as a single-style v4 font
|
||||
style_map = {"regular": 0, "bold": 1, "italic": 2, "bolditalic": 3}
|
||||
style_fonts[style_map[args.style]] = fontfile
|
||||
|
||||
# Always generate v4 format
|
||||
if args.output and len(sizes) != 1:
|
||||
print("Error: --output can only be used with a single size", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
output_dir = args.output_dir if args.output_dir else f"{font_name}/"
|
||||
total_size = 0
|
||||
for sz in sizes:
|
||||
if args.output and len(sizes) == 1:
|
||||
output_path = args.output
|
||||
else:
|
||||
filename = f"{font_name}_{sz}.cpfont"
|
||||
output_path = os.path.join(output_dir, filename)
|
||||
print(f"Generating {output_path} (size {sz}, {len(style_fonts)} style(s), v4)...", file=sys.stderr)
|
||||
total_size += generate_cpfont_multistyle(
|
||||
style_fonts, sz, intervals, output_path,
|
||||
force_autohint=args.force_autohint)
|
||||
print(f"\nTotal: {len(sizes)} files, {total_size / 1024 / 1024:.2f} MB", file=sys.stderr)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Executable
+85
@@ -0,0 +1,85 @@
|
||||
#!/bin/bash
|
||||
# Generate recommended SD card font packs for CrossPoint.
|
||||
#
|
||||
# Prerequisites:
|
||||
# pip install freetype-py fonttools
|
||||
#
|
||||
# Source fonts are in ../builtinFonts/source/:
|
||||
# Bookerly/, NotoSans/, OpenDyslexic/, Ubuntu/ — committed to git
|
||||
# NotoSansCJK/ — downloaded automatically by this script (gitignored)
|
||||
#
|
||||
# Output goes to ./output/ (copy to SD card at /.crosspoint/fonts/)
|
||||
|
||||
set -e
|
||||
cd "$(dirname "$0")"
|
||||
|
||||
SCRIPT="./fontconvert_sdcard.py"
|
||||
FONT_DIR="../builtinFonts/source"
|
||||
OUTPUT_BASE="./output"
|
||||
|
||||
SIZES="12,14,16,18"
|
||||
|
||||
# --- Download fonts that aren't checked into git ---
|
||||
|
||||
NOTOSANSCJK_DIR="$FONT_DIR/NotoSansCJK"
|
||||
NOTOSANSCJK_FONT="$NOTOSANSCJK_DIR/NotoSansCJKsc-Regular.otf"
|
||||
|
||||
if [ ! -f "$NOTOSANSCJK_FONT" ]; then
|
||||
echo "Downloading NotoSansCJKsc-Regular.otf..."
|
||||
mkdir -p "$NOTOSANSCJK_DIR"
|
||||
curl -fSL -o "$NOTOSANSCJK_FONT" \
|
||||
"https://github.com/notofonts/noto-cjk/raw/main/Sans/OTF/SimplifiedChinese/NotoSansCJKsc-Regular.otf"
|
||||
echo "Downloaded $(du -h "$NOTOSANSCJK_FONT" | cut -f1) to $NOTOSANSCJK_FONT"
|
||||
fi
|
||||
|
||||
# Clean output directories to ensure a fresh build
|
||||
echo "Cleaning output directories..."
|
||||
rm -rf "$OUTPUT_BASE/NotoSansExtended/" "$OUTPUT_BASE/Bookerly-SD/" "$OUTPUT_BASE/NotoSansCJK/"
|
||||
|
||||
# Run all three font families in parallel
|
||||
echo "=== Starting parallel font generation ==="
|
||||
|
||||
echo "[1/3] NotoSansExtended (Latin-ext + Greek + Cyrillic + Georgian + Armenian + Ethiopic)"
|
||||
python3 "$SCRIPT" \
|
||||
"$FONT_DIR/NotoSans/NotoSans-Regular.ttf" \
|
||||
--intervals latin-ext,greek,cyrillic,georgian,armenian,ethiopic \
|
||||
--sizes "$SIZES" --style regular \
|
||||
--name NotoSansExtended \
|
||||
--output-dir "$OUTPUT_BASE/NotoSansExtended/" &
|
||||
PID_NOTO=$!
|
||||
|
||||
echo "[2/3] Bookerly-SD (multi-style)"
|
||||
python3 "$SCRIPT" \
|
||||
--regular "$FONT_DIR/Bookerly/Bookerly-Regular.ttf" \
|
||||
--bold "$FONT_DIR/Bookerly/Bookerly-Bold.ttf" \
|
||||
--italic "$FONT_DIR/Bookerly/Bookerly-Italic.ttf" \
|
||||
--bolditalic "$FONT_DIR/Bookerly/Bookerly-BoldItalic.ttf" \
|
||||
--intervals builtin \
|
||||
--sizes "$SIZES" --force-autohint \
|
||||
--name Bookerly-SD \
|
||||
--output-dir "$OUTPUT_BASE/Bookerly-SD/" &
|
||||
PID_BOOKERLY=$!
|
||||
|
||||
echo "[3/3] NotoSansCJK (CJK + ASCII + Punctuation)"
|
||||
python3 "$SCRIPT" \
|
||||
"$FONT_DIR/NotoSansCJK/NotoSansCJKsc-Regular.otf" \
|
||||
--intervals ascii,latin1,punctuation,cjk \
|
||||
--sizes "$SIZES" --style regular \
|
||||
--name NotoSansCJK \
|
||||
--output-dir "$OUTPUT_BASE/NotoSansCJK/" &
|
||||
PID_CJK=$!
|
||||
|
||||
# Wait for all and track failures
|
||||
FAILED=0
|
||||
wait $PID_NOTO || { echo "ERROR: NotoSansExtended generation failed"; FAILED=1; }
|
||||
wait $PID_BOOKERLY || { echo "ERROR: Bookerly-SD generation failed"; FAILED=1; }
|
||||
wait $PID_CJK || { echo "ERROR: NotoSansCJK generation failed"; FAILED=1; }
|
||||
|
||||
if [ $FAILED -ne 0 ]; then
|
||||
echo "=== Some font generations failed ==="
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "=== Done ==="
|
||||
echo "Copy the contents of $OUTPUT_BASE/ to your SD card at /.crosspoint/fonts/"
|
||||
@@ -150,6 +150,25 @@ void ParsedText::layoutAndExtractLines(
|
||||
// Apply fixed transforms before any per-line layout work.
|
||||
applyParagraphIndent();
|
||||
|
||||
// Ensure SD card font glyph metrics are loaded before measuring word widths.
|
||||
// For flash-based fonts isSdCardFont() returns false and this block is skipped
|
||||
// entirely — no heap allocation. For SD card fonts this reads glyph metadata
|
||||
// (advanceX only, no bitmaps) for all unique codepoints in this paragraph so
|
||||
// that calculateWordWidths() can measure text without on-demand SD I/O.
|
||||
if (renderer.isSdCardFont(fontId)) {
|
||||
size_t totalSize = hyphenationEnabled ? 1 : 0;
|
||||
if (!words.empty()) totalSize += words.size() - 1; // inter-word spaces
|
||||
for (const auto& w : words) totalSize += w.size();
|
||||
std::string allText;
|
||||
allText.reserve(totalSize);
|
||||
for (size_t i = 0; i < words.size(); i++) {
|
||||
if (i > 0) allText += ' ';
|
||||
allText += words[i];
|
||||
}
|
||||
if (hyphenationEnabled) allText += '-';
|
||||
renderer.ensureSdCardFontReady(fontId, allText.c_str());
|
||||
}
|
||||
|
||||
const int pageWidth = viewportWidth;
|
||||
auto wordWidths = calculateWordWidths(renderer, fontId);
|
||||
|
||||
|
||||
@@ -2,18 +2,35 @@
|
||||
|
||||
#include <FontDecompressor.h>
|
||||
#include <Logging.h>
|
||||
#include <SdCardFont.h>
|
||||
|
||||
#include <cstring>
|
||||
|
||||
FontCacheManager::FontCacheManager(const std::map<int, EpdFontFamily>& fontMap) : fontMap_(fontMap) {}
|
||||
FontCacheManager::FontCacheManager(const std::map<int, EpdFontFamily>& fontMap,
|
||||
const std::map<int, SdCardFont*>& sdCardFonts)
|
||||
: fontMap_(fontMap), sdCardFonts_(sdCardFonts) {}
|
||||
|
||||
void FontCacheManager::setFontDecompressor(FontDecompressor* d) { fontDecompressor_ = d; }
|
||||
|
||||
void FontCacheManager::clearCache() {
|
||||
if (fontDecompressor_) fontDecompressor_->clearCache();
|
||||
for (auto& [id, font] : sdCardFonts_) {
|
||||
font->clearCache();
|
||||
}
|
||||
}
|
||||
|
||||
void FontCacheManager::prewarmCache(int fontId, const char* utf8Text, uint8_t styleMask) {
|
||||
// SD card font prewarm path: prewarm all requested styles in one call
|
||||
auto sdIt = sdCardFonts_.find(fontId);
|
||||
if (sdIt != sdCardFonts_.end()) {
|
||||
int missed = sdIt->second->prewarm(utf8Text, styleMask);
|
||||
if (missed > 0) {
|
||||
LOG_DBG("FCM", "prewarmCache(SD): %d glyph(s) not found (styleMask=0x%02X)", missed, styleMask);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Standard compressed font prewarm path: loop over all requested styles
|
||||
if (!fontDecompressor_ || fontMap_.count(fontId) == 0) return;
|
||||
|
||||
for (uint8_t i = 0; i < 4; i++) {
|
||||
@@ -30,10 +47,16 @@ void FontCacheManager::prewarmCache(int fontId, const char* utf8Text, uint8_t st
|
||||
|
||||
void FontCacheManager::logStats(const char* label) {
|
||||
if (fontDecompressor_) fontDecompressor_->logStats(label);
|
||||
for (auto& [id, font] : sdCardFonts_) {
|
||||
font->logStats(label);
|
||||
}
|
||||
}
|
||||
|
||||
void FontCacheManager::resetStats() {
|
||||
if (fontDecompressor_) fontDecompressor_->resetStats();
|
||||
for (auto& [id, font] : sdCardFonts_) {
|
||||
font->resetStats();
|
||||
}
|
||||
}
|
||||
|
||||
bool FontCacheManager::isScanning() const { return scanMode_ == ScanMode::Scanning; }
|
||||
|
||||
@@ -7,10 +7,11 @@
|
||||
#include <string>
|
||||
|
||||
class FontDecompressor;
|
||||
class SdCardFont;
|
||||
|
||||
class FontCacheManager {
|
||||
public:
|
||||
explicit FontCacheManager(const std::map<int, EpdFontFamily>& fontMap);
|
||||
FontCacheManager(const std::map<int, EpdFontFamily>& fontMap, const std::map<int, SdCardFont*>& sdCardFonts);
|
||||
|
||||
void setFontDecompressor(FontDecompressor* d);
|
||||
|
||||
@@ -45,6 +46,7 @@ class FontCacheManager {
|
||||
|
||||
private:
|
||||
const std::map<int, EpdFontFamily>& fontMap_;
|
||||
const std::map<int, SdCardFont*>& sdCardFonts_;
|
||||
FontDecompressor* fontDecompressor_ = nullptr;
|
||||
|
||||
enum class ScanMode : uint8_t { None, Scanning };
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
#include <FontDecompressor.h>
|
||||
#include <HalGPIO.h>
|
||||
#include <Logging.h>
|
||||
#include <SdCardFont.h>
|
||||
#include <Utf8.h>
|
||||
#include <esp_heap_caps.h>
|
||||
|
||||
@@ -21,9 +22,34 @@ const uint8_t* GfxRenderer::getGlyphBitmap(const EpdFontData* fontData, const Ep
|
||||
// must consume it (draw the glyph) before requesting another bitmap.
|
||||
return fd->getBitmap(fontData, glyph, glyphIndex);
|
||||
}
|
||||
// For SD card fonts, check if the glyph was loaded on demand into the overflow
|
||||
// buffer. getOverflowBitmap() returns:
|
||||
// - bitmap pointer for overflow glyphs with bitmap data
|
||||
// - nullptr for overflow glyphs without bitmap data (e.g. space: width=0, height=0)
|
||||
// - nullptr for non-overflow glyphs (normal prewarmed path)
|
||||
// We distinguish overflow-with-no-bitmap from non-overflow by checking isOverflowGlyph().
|
||||
if (fontData->glyphMissCtx) {
|
||||
auto* sdFont = SdCardFont::fromMissCtx(fontData->glyphMissCtx);
|
||||
if (sdFont->isOverflowGlyph(glyph)) {
|
||||
return sdFont->getOverflowBitmap(glyph); // may be nullptr for zero-width glyphs
|
||||
}
|
||||
}
|
||||
return &fontData->bitmap[glyph->dataOffset];
|
||||
}
|
||||
|
||||
void GfxRenderer::ensureSdCardFontReady(int fontId, const char* utf8Text) const {
|
||||
auto it = sdCardFonts_.find(fontId);
|
||||
if (it != sdCardFonts_.end()) {
|
||||
// Metadata-only: loads glyph metrics (advanceX) without bitmap data.
|
||||
// Saves ~50-100KB heap vs full prewarm — layout only needs advance widths.
|
||||
// Prewarm all present styles (0x0F) for layout measurement.
|
||||
int missed = it->second->prewarm(utf8Text, 0x0F, /*metadataOnly=*/true);
|
||||
if (missed > 0) {
|
||||
LOG_DBG("GFX", "ensureSdCardFontReady: %d glyph(s) not found", missed);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void GfxRenderer::begin() {
|
||||
frameBuffer = display.getFrameBuffer();
|
||||
if (!frameBuffer) {
|
||||
@@ -38,7 +64,12 @@ void GfxRenderer::begin() {
|
||||
bwBufferChunks.assign((frameBufferSize + bwBufferChunkSize - 1) / bwBufferChunkSize, nullptr);
|
||||
}
|
||||
|
||||
void GfxRenderer::insertFont(const int fontId, EpdFontFamily font) { fontMap.insert({fontId, font}); }
|
||||
void GfxRenderer::insertFont(const int fontId, EpdFontFamily font) {
|
||||
auto result = fontMap.insert({fontId, font});
|
||||
if (!result.second) {
|
||||
LOG_ERR("GFX", "Font ID %d already registered, ignoring duplicate", fontId);
|
||||
}
|
||||
}
|
||||
|
||||
// Translate logical (x,y) coordinates to physical panel coordinates based on current orientation
|
||||
// This should always be inlined for better performance
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
#include <HalDisplay.h>
|
||||
|
||||
class FontCacheManager;
|
||||
class SdCardFont;
|
||||
|
||||
#include <atomic>
|
||||
#include <cstring>
|
||||
@@ -57,6 +58,10 @@ class GfxRenderer {
|
||||
size_t bwBufferChunkSize = BW_BUFFER_CHUNK_SIZE;
|
||||
std::vector<uint8_t*> bwBufferChunks;
|
||||
std::map<int, EpdFontFamily> fontMap;
|
||||
// Mutable because ensureSdCardFontReady() is const (called from layout code that
|
||||
// holds a const GfxRenderer&) but triggers SD card reads and heap allocation
|
||||
// inside the SdCardFont objects. Same pragmatic compromise as fontCacheManager_.
|
||||
mutable std::map<int, SdCardFont*> sdCardFonts_;
|
||||
|
||||
// Mutable because drawText() is const but needs to delegate scan-mode
|
||||
// recording to the (non-const) FontCacheManager. Same pragmatic compromise
|
||||
@@ -97,9 +102,19 @@ class GfxRenderer {
|
||||
// Setup
|
||||
void begin(); // must be called right after display.begin()
|
||||
void insertFont(int fontId, EpdFontFamily font);
|
||||
void removeFont(int fontId) { fontMap.erase(fontId); }
|
||||
void setFontCacheManager(FontCacheManager* m) { fontCacheManager_ = m; }
|
||||
FontCacheManager* getFontCacheManager() const { return fontCacheManager_; }
|
||||
const std::map<int, EpdFontFamily>& getFontMap() const { return fontMap; }
|
||||
void registerSdCardFont(int fontId, SdCardFont* font) { sdCardFonts_[fontId] = font; }
|
||||
void unregisterSdCardFont(int fontId) { sdCardFonts_.erase(fontId); }
|
||||
void clearSdCardFonts() { sdCardFonts_.clear(); }
|
||||
const std::map<int, SdCardFont*>& getSdCardFonts() const { return sdCardFonts_; }
|
||||
bool isSdCardFont(int fontId) const { return sdCardFonts_.count(fontId) > 0; }
|
||||
// Ensure SD card font glyph data is loaded for the given text. Called from layout code
|
||||
// (which holds a const GfxRenderer&) before measuring word widths. Safe to call on
|
||||
// non-SD fonts (no-op).
|
||||
void ensureSdCardFontReady(int fontId, const char* utf8Text) const;
|
||||
|
||||
// Orientation control (affects logical width/height and coordinate transforms)
|
||||
void setOrientation(const Orientation o) { orientation.store(static_cast<int>(o), std::memory_order_relaxed); }
|
||||
|
||||
@@ -8,8 +8,28 @@
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
|
||||
#include "SdCardFontGlobals.h"
|
||||
#include "fontIds.h"
|
||||
|
||||
// Font ID 0 is reserved as the SD card font "not found" sentinel
|
||||
// (SdCardFontManager::computeFontId() never returns 0). Guard against any
|
||||
// hash accidentally producing 0 — would cause silent fallback to built-in.
|
||||
static_assert(BOOKERLY_12_FONT_ID != 0, "Font ID collision with sentinel");
|
||||
static_assert(BOOKERLY_14_FONT_ID != 0, "Font ID collision with sentinel");
|
||||
static_assert(BOOKERLY_16_FONT_ID != 0, "Font ID collision with sentinel");
|
||||
static_assert(BOOKERLY_18_FONT_ID != 0, "Font ID collision with sentinel");
|
||||
static_assert(NOTOSANS_12_FONT_ID != 0, "Font ID collision with sentinel");
|
||||
static_assert(NOTOSANS_14_FONT_ID != 0, "Font ID collision with sentinel");
|
||||
static_assert(NOTOSANS_16_FONT_ID != 0, "Font ID collision with sentinel");
|
||||
static_assert(NOTOSANS_18_FONT_ID != 0, "Font ID collision with sentinel");
|
||||
static_assert(OPENDYSLEXIC_8_FONT_ID != 0, "Font ID collision with sentinel");
|
||||
static_assert(OPENDYSLEXIC_10_FONT_ID != 0, "Font ID collision with sentinel");
|
||||
static_assert(OPENDYSLEXIC_12_FONT_ID != 0, "Font ID collision with sentinel");
|
||||
static_assert(OPENDYSLEXIC_14_FONT_ID != 0, "Font ID collision with sentinel");
|
||||
static_assert(UI_10_FONT_ID != 0, "Font ID collision with sentinel");
|
||||
static_assert(UI_12_FONT_ID != 0, "Font ID collision with sentinel");
|
||||
static_assert(SMALL_FONT_ID != 0, "Font ID collision with sentinel");
|
||||
|
||||
// Initialize the static instance
|
||||
CrossPointSettings CrossPointSettings::instance;
|
||||
|
||||
@@ -244,6 +264,20 @@ bool CrossPointSettings::loadFromBinaryFile() {
|
||||
}
|
||||
|
||||
float CrossPointSettings::getReaderLineCompression() const {
|
||||
// SD card fonts inherit the Bookerly-style line compression (the most neutral
|
||||
// values) since we have no per-family metadata for SD fonts.
|
||||
if (sdFontFamilyName[0] != '\0') {
|
||||
switch (lineSpacing) {
|
||||
case TIGHT:
|
||||
return 0.95f;
|
||||
case NORMAL:
|
||||
default:
|
||||
return 1.0f;
|
||||
case WIDE:
|
||||
return 1.1f;
|
||||
}
|
||||
}
|
||||
|
||||
switch (fontFamily) {
|
||||
case BOOKERLY:
|
||||
default:
|
||||
@@ -311,11 +345,11 @@ int CrossPointSettings::getRefreshFrequency() const {
|
||||
}
|
||||
}
|
||||
|
||||
int CrossPointSettings::getReaderFontId() const {
|
||||
switch (fontFamily) {
|
||||
int CrossPointSettings::getBuiltinReaderFontId(uint8_t family, uint8_t size) {
|
||||
switch (family) {
|
||||
case BOOKERLY:
|
||||
default:
|
||||
switch (fontSize) {
|
||||
switch (size) {
|
||||
case SMALL:
|
||||
return BOOKERLY_12_FONT_ID;
|
||||
case MEDIUM:
|
||||
@@ -327,7 +361,7 @@ int CrossPointSettings::getReaderFontId() const {
|
||||
return BOOKERLY_18_FONT_ID;
|
||||
}
|
||||
case NOTOSANS:
|
||||
switch (fontSize) {
|
||||
switch (size) {
|
||||
case SMALL:
|
||||
return NOTOSANS_12_FONT_ID;
|
||||
case MEDIUM:
|
||||
@@ -339,7 +373,7 @@ int CrossPointSettings::getReaderFontId() const {
|
||||
return NOTOSANS_18_FONT_ID;
|
||||
}
|
||||
case OPENDYSLEXIC:
|
||||
switch (fontSize) {
|
||||
switch (size) {
|
||||
case SMALL:
|
||||
return OPENDYSLEXIC_8_FONT_ID;
|
||||
case MEDIUM:
|
||||
@@ -352,3 +386,14 @@ int CrossPointSettings::getReaderFontId() const {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int CrossPointSettings::getReaderFontId() const {
|
||||
// SD card font takes priority when one is selected globally.
|
||||
// resolveSdCardFontId() returns 0 if the named family isn't loaded
|
||||
// (e.g. SD card removed since selection) — fall through to built-in.
|
||||
if (sdFontFamilyName[0] != '\0') {
|
||||
int id = resolveSdCardFontId(sdFontFamilyName);
|
||||
if (id != 0) return id;
|
||||
}
|
||||
return getBuiltinReaderFontId(fontFamily, fontSize);
|
||||
}
|
||||
|
||||
@@ -88,8 +88,9 @@ class CrossPointSettings {
|
||||
FRONT_BUTTON_HARDWARE_COUNT
|
||||
};
|
||||
|
||||
// Font family options
|
||||
// Font family options (built-in fonts only; SD card fonts use sdFontFamilyName)
|
||||
enum FONT_FAMILY { BOOKERLY = 0, NOTOSANS = 1, OPENDYSLEXIC = 2, FONT_FAMILY_COUNT };
|
||||
static constexpr uint8_t BUILTIN_FONT_COUNT = FONT_FAMILY_COUNT;
|
||||
// Font size options
|
||||
enum FONT_SIZE { SMALL = 0, MEDIUM = 1, LARGE = 2, EXTRA_LARGE = 3, FONT_SIZE_COUNT };
|
||||
enum LINE_COMPRESSION { TIGHT = 0, NORMAL = 1, WIDE = 2, LINE_COMPRESSION_COUNT };
|
||||
@@ -211,6 +212,8 @@ class CrossPointSettings {
|
||||
uint8_t frontButtonRight = FRONT_HW_RIGHT;
|
||||
// Reader font settings
|
||||
uint8_t fontFamily = BOOKERLY;
|
||||
// SD card font family name (empty = use built-in fontFamily)
|
||||
char sdFontFamilyName[32] = "";
|
||||
uint8_t fontSize = MEDIUM;
|
||||
uint8_t lineSpacing = NORMAL;
|
||||
uint8_t paragraphAlignment = JUSTIFIED;
|
||||
@@ -319,6 +322,11 @@ class CrossPointSettings {
|
||||
|
||||
static constexpr uint16_t getPowerButtonDuration() { return 400; }
|
||||
int getReaderFontId() const;
|
||||
// Pure built-in lookup (size enum + family enum -> font ID). Independent of
|
||||
// SD-card font selection. Used by the per-book fontFamilyOverride path so
|
||||
// an override forces back to a known built-in even when an SD font is the
|
||||
// global default.
|
||||
static int getBuiltinReaderFontId(uint8_t family, uint8_t size);
|
||||
|
||||
// If count_only is true, returns the number of settings items that would be written.
|
||||
uint8_t writeSettings(FsFile& file, bool count_only = false) const;
|
||||
|
||||
@@ -189,6 +189,13 @@ bool JsonSettingsIO::saveSettings(const CrossPointSettings& s, const char* path)
|
||||
doc["frontButtonLeft"] = s.frontButtonLeft;
|
||||
doc["frontButtonRight"] = s.frontButtonRight;
|
||||
|
||||
// Font family uses a DynamicEnumCtx in SettingsList (no valuePtr) so the generic
|
||||
// loop above skips it. Save manually.
|
||||
doc["fontFamily"] = s.fontFamily;
|
||||
if (s.sdFontFamilyName[0] != '\0') {
|
||||
doc["sdFontFamilyName"] = s.sdFontFamilyName;
|
||||
}
|
||||
|
||||
String json;
|
||||
serializeJson(doc, json);
|
||||
return Storage.writeFile(path, json);
|
||||
@@ -268,6 +275,14 @@ bool JsonSettingsIO::loadSettings(CrossPointSettings& s, const char* json, bool*
|
||||
clamp(doc["frontButtonRight"] | (uint8_t)S::FRONT_HW_RIGHT, S::FRONT_BUTTON_HARDWARE_COUNT, S::FRONT_HW_RIGHT);
|
||||
CrossPointSettings::validateFrontButtonMapping(s);
|
||||
|
||||
// Font family uses a DynamicEnumCtx in SettingsList (no valuePtr) so the generic
|
||||
// loop above skips it. Load manually.
|
||||
s.fontFamily = clamp(doc["fontFamily"] | (uint8_t)CrossPointSettings::BOOKERLY,
|
||||
CrossPointSettings::BUILTIN_FONT_COUNT, CrossPointSettings::BOOKERLY);
|
||||
const char* sfn = doc["sdFontFamilyName"] | "";
|
||||
strncpy(s.sdFontFamilyName, sfn, sizeof(s.sdFontFamilyName) - 1);
|
||||
s.sdFontFamilyName[sizeof(s.sdFontFamilyName) - 1] = '\0';
|
||||
|
||||
LOG_DBG("CPS", "Settings loaded from file");
|
||||
|
||||
return true;
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
#pragma once
|
||||
|
||||
#include "SdCardFontSystem.h"
|
||||
|
||||
class GfxRenderer;
|
||||
|
||||
// Global SD card font system instance (defined in main.cpp).
|
||||
extern SdCardFontSystem sdFontSystem;
|
||||
|
||||
// Ensure the correct SD card font family is loaded for current settings.
|
||||
// Defined in main.cpp; call before entering the reader or after settings change.
|
||||
extern void ensureSdFontLoaded();
|
||||
|
||||
// Resolve the SD card font ID for the given family name.
|
||||
// Returns 0 if no SD font with that family name is currently loaded.
|
||||
// Free function (not stored as a callback in CrossPointSettings) so the linker
|
||||
// can resolve it directly without runtime indirection.
|
||||
int resolveSdCardFontId(const char* familyName);
|
||||
|
||||
// Trampolines used by the dynamic font-family SettingInfo. They walk
|
||||
// sdFontSystem's registry on each call to translate between
|
||||
// (built-in index | built-in count + SD index) and the appropriate
|
||||
// CrossPointSettings field (fontFamily vs sdFontFamilyName).
|
||||
// Signatures match SettingInfo::ValueGetterFn / ValueSetterFn so they can
|
||||
// be wired directly into a DynamicEnum SettingInfo without further indirection.
|
||||
uint8_t fontFamilyDynamicGetter(const void* ctx);
|
||||
void fontFamilyDynamicSetter(void* ctx, uint8_t value);
|
||||
|
||||
// Returns the total number of font-family options currently available
|
||||
// (BUILTIN_FONT_COUNT + number of discovered SD families). Used by the
|
||||
// settings UI / web layer to enrich enumLabels and to bound cycling.
|
||||
uint8_t fontFamilyOptionCount();
|
||||
|
||||
// Returns the localized label for option index `i`. For built-in indices
|
||||
// (< BUILTIN_FONT_COUNT) this returns the I18N string; for SD indices it
|
||||
// returns the family name from sdFontSystem.registry().
|
||||
#include <string>
|
||||
std::string fontFamilyOptionLabel(uint8_t i);
|
||||
@@ -0,0 +1,160 @@
|
||||
#include "SdCardFontSystem.h"
|
||||
|
||||
#include <GfxRenderer.h>
|
||||
#include <I18n.h>
|
||||
#include <Logging.h>
|
||||
|
||||
#include <climits>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
|
||||
#include "CrossPointSettings.h"
|
||||
#include "SdCardFontGlobals.h"
|
||||
|
||||
// Free-function resolver used by CrossPointSettings::getReaderFontId().
|
||||
// Resolved by the linker — no callback indirection stored in settings.
|
||||
int resolveSdCardFontId(const char* familyName) { return sdFontSystem.resolveFontId(familyName, 0); }
|
||||
|
||||
// --- Font-family dynamic SettingInfo trampolines ---
|
||||
//
|
||||
// The font-family SettingInfo lives in a namespace-static SettingsList that is
|
||||
// initialized at global-static phase, well before sdFontSystem.begin() runs.
|
||||
// We therefore cannot bake the SD family list into the SettingInfo at
|
||||
// construction; instead the SettingInfo holds these stateless trampolines that
|
||||
// consult sdFontSystem at every call. enumLabels is enriched lazily by the
|
||||
// consumers (SettingsActivity, CrossPointWebServer) before each iteration.
|
||||
|
||||
uint8_t fontFamilyDynamicGetter(const void* /*ctx*/) {
|
||||
if (SETTINGS.sdFontFamilyName[0] != '\0') {
|
||||
const auto& families = sdFontSystem.registry().getFamilies();
|
||||
for (size_t i = 0; i < families.size(); i++) {
|
||||
if (families[i].name == SETTINGS.sdFontFamilyName) {
|
||||
return static_cast<uint8_t>(CrossPointSettings::BUILTIN_FONT_COUNT + i);
|
||||
}
|
||||
}
|
||||
// SD family no longer present (card removed?); fall through to built-in.
|
||||
}
|
||||
return SETTINGS.fontFamily < CrossPointSettings::BUILTIN_FONT_COUNT ? SETTINGS.fontFamily
|
||||
: CrossPointSettings::BOOKERLY;
|
||||
}
|
||||
|
||||
void fontFamilyDynamicSetter(void* /*ctx*/, uint8_t value) {
|
||||
if (value < CrossPointSettings::BUILTIN_FONT_COUNT) {
|
||||
SETTINGS.fontFamily = value;
|
||||
SETTINGS.sdFontFamilyName[0] = '\0';
|
||||
return;
|
||||
}
|
||||
const auto& families = sdFontSystem.registry().getFamilies();
|
||||
uint8_t sdIdx = value - CrossPointSettings::BUILTIN_FONT_COUNT;
|
||||
if (sdIdx < families.size()) {
|
||||
strncpy(SETTINGS.sdFontFamilyName, families[sdIdx].name.c_str(), sizeof(SETTINGS.sdFontFamilyName) - 1);
|
||||
SETTINGS.sdFontFamilyName[sizeof(SETTINGS.sdFontFamilyName) - 1] = '\0';
|
||||
}
|
||||
}
|
||||
|
||||
uint8_t fontFamilyOptionCount() {
|
||||
return static_cast<uint8_t>(CrossPointSettings::BUILTIN_FONT_COUNT + sdFontSystem.registry().getFamilies().size());
|
||||
}
|
||||
|
||||
std::string fontFamilyOptionLabel(uint8_t i) {
|
||||
if (i < CrossPointSettings::BUILTIN_FONT_COUNT) {
|
||||
static const StrId BUILTIN_LABELS[] = {StrId::STR_BOOKERLY, StrId::STR_NOTO_SANS, StrId::STR_OPEN_DYSLEXIC};
|
||||
return I18N.get(BUILTIN_LABELS[i]);
|
||||
}
|
||||
const auto& families = sdFontSystem.registry().getFamilies();
|
||||
uint8_t sdIdx = i - CrossPointSettings::BUILTIN_FONT_COUNT;
|
||||
return sdIdx < families.size() ? families[sdIdx].name : std::string();
|
||||
}
|
||||
|
||||
// Map fontSize enum (SMALL=0, MEDIUM=1, LARGE=2, EXTRA_LARGE=3) to point sizes.
|
||||
static constexpr uint8_t FONT_SIZE_TO_PT[] = {12, 14, 16, 18};
|
||||
|
||||
static uint8_t targetPtSizeFromSettings() {
|
||||
uint8_t e = SETTINGS.fontSize;
|
||||
if (e >= sizeof(FONT_SIZE_TO_PT)) e = 1; // default to MEDIUM
|
||||
return FONT_SIZE_TO_PT[e];
|
||||
}
|
||||
|
||||
void SdCardFontSystem::begin(GfxRenderer& renderer) {
|
||||
registry_.discover();
|
||||
|
||||
// If user has a saved SD font selection, load it
|
||||
if (SETTINGS.sdFontFamilyName[0] != '\0') {
|
||||
const auto* family = registry_.findFamily(SETTINGS.sdFontFamilyName);
|
||||
if (family) {
|
||||
if (manager_.loadFamily(*family, renderer, targetPtSizeFromSettings())) {
|
||||
LOG_DBG("SDFS", "Loaded SD card font family: %s", SETTINGS.sdFontFamilyName);
|
||||
} else {
|
||||
LOG_ERR("SDFS", "Failed to load SD font family: %s (clearing)", SETTINGS.sdFontFamilyName);
|
||||
SETTINGS.sdFontFamilyName[0] = '\0';
|
||||
}
|
||||
} else {
|
||||
LOG_DBG("SDFS", "SD font family not found on card: %s (clearing)", SETTINGS.sdFontFamilyName);
|
||||
SETTINGS.sdFontFamilyName[0] = '\0';
|
||||
}
|
||||
}
|
||||
|
||||
LOG_DBG("SDFS", "SD font system ready (%d families discovered)", registry_.getFamilyCount());
|
||||
}
|
||||
|
||||
void SdCardFontSystem::ensureLoaded(GfxRenderer& renderer) {
|
||||
const char* wantedFamily = SETTINGS.sdFontFamilyName;
|
||||
const std::string& currentFamily = manager_.currentFamilyName();
|
||||
const uint8_t targetPt = targetPtSizeFromSettings();
|
||||
|
||||
if (wantedFamily[0] == '\0') {
|
||||
if (!currentFamily.empty()) {
|
||||
manager_.unloadAll(renderer);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Reload if family changed OR if the user-selected size changed and the
|
||||
// family has a closer file than what's currently loaded.
|
||||
bool familyMatches = (currentFamily == wantedFamily);
|
||||
if (familyMatches) {
|
||||
const auto* family = registry_.findFamily(wantedFamily);
|
||||
if (!family) {
|
||||
LOG_DBG("SDFS", "SD font family disappeared: %s (clearing)", wantedFamily);
|
||||
manager_.unloadAll(renderer);
|
||||
SETTINGS.sdFontFamilyName[0] = '\0';
|
||||
return;
|
||||
}
|
||||
uint8_t bestPt = 0;
|
||||
int bestDiff = INT32_MAX;
|
||||
for (const auto& f : family->files) {
|
||||
int diff = abs(static_cast<int>(f.pointSize) - static_cast<int>(targetPt));
|
||||
if (diff < bestDiff) {
|
||||
bestDiff = diff;
|
||||
bestPt = f.pointSize;
|
||||
}
|
||||
}
|
||||
if (bestPt == manager_.currentPointSize()) return; // already loaded with the right size
|
||||
LOG_DBG("SDFS", "Reloading %s: size %u -> %u (target %u)", wantedFamily, manager_.currentPointSize(), bestPt,
|
||||
targetPt);
|
||||
}
|
||||
|
||||
if (!currentFamily.empty()) {
|
||||
manager_.unloadAll(renderer);
|
||||
}
|
||||
|
||||
const auto* family = registry_.findFamily(wantedFamily);
|
||||
if (family) {
|
||||
if (manager_.loadFamily(*family, renderer, targetPt)) {
|
||||
LOG_DBG("SDFS", "Loaded SD font family: %s", wantedFamily);
|
||||
} else {
|
||||
LOG_ERR("SDFS", "Failed to load SD font family: %s (clearing)", wantedFamily);
|
||||
SETTINGS.sdFontFamilyName[0] = '\0';
|
||||
}
|
||||
} else {
|
||||
LOG_DBG("SDFS", "SD font family not found: %s (clearing)", wantedFamily);
|
||||
SETTINGS.sdFontFamilyName[0] = '\0';
|
||||
}
|
||||
}
|
||||
|
||||
int SdCardFontSystem::resolveFontId(const char* familyName, uint8_t /*fontSizeEnum*/) const {
|
||||
// The manager loads exactly one size (closest to SETTINGS.fontSize), so the
|
||||
// enum is implicit — always return the single loaded font ID for this family.
|
||||
// ensureLoaded() must have been called with the current settings before this.
|
||||
return manager_.getFontId(familyName);
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
#pragma once
|
||||
|
||||
#include <SdCardFontManager.h>
|
||||
#include <SdCardFontRegistry.h>
|
||||
|
||||
class GfxRenderer;
|
||||
|
||||
/// Facade that owns the SD card font registry, manager, and resolver logic.
|
||||
/// Hides implementation details behind a single begin() + ensureLoaded() API.
|
||||
class SdCardFontSystem {
|
||||
public:
|
||||
SdCardFontSystem() = default;
|
||||
SdCardFontSystem(const SdCardFontSystem&) = delete;
|
||||
SdCardFontSystem& operator=(const SdCardFontSystem&) = delete;
|
||||
/// Discover SD card fonts and load user's saved selection. Call once during setup.
|
||||
void begin(GfxRenderer& renderer);
|
||||
|
||||
/// Ensure the correct SD font family is loaded for the current settings.
|
||||
/// Call before entering the reader or after settings change.
|
||||
void ensureLoaded(GfxRenderer& renderer);
|
||||
|
||||
/// Resolve an SD card font ID from family name + fontSize enum.
|
||||
/// Returns 0 if not found. Used by CrossPointSettings::getReaderFontId().
|
||||
int resolveFontId(const char* familyName, uint8_t fontSizeEnum) const;
|
||||
|
||||
/// Access the registry (e.g. for settings UI to enumerate available fonts).
|
||||
const SdCardFontRegistry& registry() const { return registry_; }
|
||||
|
||||
private:
|
||||
SdCardFontRegistry registry_;
|
||||
SdCardFontManager manager_;
|
||||
};
|
||||
+8
-4
@@ -6,6 +6,7 @@
|
||||
|
||||
#include "CrossPointSettings.h"
|
||||
#include "KOReaderCredentialStore.h"
|
||||
#include "SdCardFontGlobals.h"
|
||||
#include "activities/settings/SettingInfo.h"
|
||||
|
||||
// Shared settings list used by both the device settings UI and the web settings API.
|
||||
@@ -86,10 +87,13 @@ inline const std::vector<SettingInfo> list = {
|
||||
SettingInfo::Enum(StrId::STR_ORIENTATION, &CrossPointSettings::orientation,
|
||||
{StrId::STR_PORTRAIT, StrId::STR_LANDSCAPE_CW, StrId::STR_INVERTED, StrId::STR_LANDSCAPE_CCW},
|
||||
"orientation", StrId::STR_CAT_READER),
|
||||
// Font
|
||||
SettingInfo::Enum(StrId::STR_FONT_FAMILY, &CrossPointSettings::fontFamily,
|
||||
{StrId::STR_BOOKERLY, StrId::STR_NOTO_SANS, StrId::STR_OPEN_DYSLEXIC}, "fontFamily",
|
||||
StrId::STR_CAT_READER)
|
||||
// Font — DynamicEnum so SD card font families can be appended at the consumer
|
||||
// side (SettingsActivity / CrossPointWebServer enrich enumLabels before
|
||||
// iterating). The built-in StrIds are kept as a fallback for code paths that
|
||||
// don't enrich enumLabels.
|
||||
SettingInfo::DynamicEnum(StrId::STR_FONT_FAMILY,
|
||||
{StrId::STR_BOOKERLY, StrId::STR_NOTO_SANS, StrId::STR_OPEN_DYSLEXIC},
|
||||
fontFamilyDynamicGetter, fontFamilyDynamicSetter, "fontFamily", StrId::STR_CAT_READER)
|
||||
.withSubcategory(StrId::STR_MENU_READER_FONT),
|
||||
SettingInfo::Enum(StrId::STR_FONT_SIZE, &CrossPointSettings::fontSize,
|
||||
{StrId::STR_SMALL, StrId::STR_MEDIUM, StrId::STR_LARGE, StrId::STR_X_LARGE}, "fontSize",
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
|
||||
#include "CrossPointState.h"
|
||||
#include "OpdsServerStore.h"
|
||||
#include "SdCardFontGlobals.h"
|
||||
#include "boot_sleep/BootActivity.h"
|
||||
#include "boot_sleep/SleepActivity.h"
|
||||
#include "browser/OpdsBookBrowserActivity.h"
|
||||
@@ -282,6 +283,7 @@ void ActivityManager::goToBrowser() {
|
||||
}
|
||||
|
||||
void ActivityManager::goToReader(std::string path) {
|
||||
ensureSdFontLoaded();
|
||||
replaceActivity(std::make_unique<ReaderActivity>(renderer, mappedInput, std::move(path)));
|
||||
}
|
||||
|
||||
@@ -301,6 +303,7 @@ void ActivityManager::goToKOReaderSync() {
|
||||
void ActivityManager::replaceWithReader(std::string path, ReturnHint hint) {
|
||||
returnHint = std::move(hint);
|
||||
hasReturnHint = true;
|
||||
ensureSdFontLoaded();
|
||||
replaceActivity(std::make_unique<ReaderActivity>(renderer, mappedInput, std::move(path)));
|
||||
}
|
||||
|
||||
|
||||
@@ -1073,48 +1073,25 @@ uint8_t EpubReaderActivity::getEffectiveImageRendering() const {
|
||||
}
|
||||
|
||||
int EpubReaderActivity::getEffectiveReaderFontId() const {
|
||||
const uint8_t fontFamily =
|
||||
(bookFontFamilyOverride >= 0) ? static_cast<uint8_t>(bookFontFamilyOverride) : SETTINGS.fontFamily;
|
||||
// Per-book font override: when set, force a specific BUILT-IN family even if
|
||||
// an SD card font is the global default. This makes the override predictable
|
||||
// ("override forces back to a known built-in") and avoids surprising users
|
||||
// who set the override before they had any SD fonts.
|
||||
const uint8_t fontSize = (bookFontSizeOverride >= 0) ? static_cast<uint8_t>(bookFontSizeOverride) : SETTINGS.fontSize;
|
||||
switch (fontFamily) {
|
||||
case CrossPointSettings::NOTOSANS:
|
||||
switch (fontSize) {
|
||||
case CrossPointSettings::SMALL:
|
||||
return NOTOSANS_12_FONT_ID;
|
||||
case CrossPointSettings::MEDIUM:
|
||||
default:
|
||||
return NOTOSANS_14_FONT_ID;
|
||||
case CrossPointSettings::LARGE:
|
||||
return NOTOSANS_16_FONT_ID;
|
||||
case CrossPointSettings::EXTRA_LARGE:
|
||||
return NOTOSANS_18_FONT_ID;
|
||||
}
|
||||
case CrossPointSettings::OPENDYSLEXIC:
|
||||
switch (fontSize) {
|
||||
case CrossPointSettings::SMALL:
|
||||
return OPENDYSLEXIC_8_FONT_ID;
|
||||
case CrossPointSettings::MEDIUM:
|
||||
default:
|
||||
return OPENDYSLEXIC_10_FONT_ID;
|
||||
case CrossPointSettings::LARGE:
|
||||
return OPENDYSLEXIC_12_FONT_ID;
|
||||
case CrossPointSettings::EXTRA_LARGE:
|
||||
return OPENDYSLEXIC_14_FONT_ID;
|
||||
}
|
||||
case CrossPointSettings::BOOKERLY:
|
||||
default:
|
||||
switch (fontSize) {
|
||||
case CrossPointSettings::SMALL:
|
||||
return BOOKERLY_12_FONT_ID;
|
||||
case CrossPointSettings::MEDIUM:
|
||||
default:
|
||||
return BOOKERLY_14_FONT_ID;
|
||||
case CrossPointSettings::LARGE:
|
||||
return BOOKERLY_16_FONT_ID;
|
||||
case CrossPointSettings::EXTRA_LARGE:
|
||||
return BOOKERLY_18_FONT_ID;
|
||||
}
|
||||
if (bookFontFamilyOverride >= 0) {
|
||||
return CrossPointSettings::getBuiltinReaderFontId(static_cast<uint8_t>(bookFontFamilyOverride), fontSize);
|
||||
}
|
||||
// No override: defer to global resolution (which honors SD card font selection).
|
||||
// We synthesize a temporary lookup using the override fontSize if it's set; otherwise
|
||||
// SETTINGS.getReaderFontId() is the canonical answer.
|
||||
if (bookFontSizeOverride >= 0) {
|
||||
if (SETTINGS.sdFontFamilyName[0] != '\0') {
|
||||
// SD font selected globally — size override doesn't change which family resolves.
|
||||
return SETTINGS.getReaderFontId();
|
||||
}
|
||||
return CrossPointSettings::getBuiltinReaderFontId(SETTINGS.fontFamily, fontSize);
|
||||
}
|
||||
return SETTINGS.getReaderFontId();
|
||||
}
|
||||
|
||||
bool EpubReaderActivity::stepPageState(const bool isForwardTurn) {
|
||||
|
||||
@@ -6,6 +6,8 @@
|
||||
#include <HalGPIO.h>
|
||||
#include <Logging.h>
|
||||
|
||||
#include <cstring>
|
||||
|
||||
#include "CrossPointSettings.h"
|
||||
#include "MappedInputManager.h"
|
||||
#include "SettingActionDispatch.h"
|
||||
@@ -66,14 +68,23 @@ void SettingsActivity::onEnter() {
|
||||
setting.nameId == StrId::STR_TIMEZONE)) {
|
||||
continue;
|
||||
}
|
||||
if (setting.category == StrId::STR_CAT_DISPLAY) {
|
||||
addTo(displaySettings, lastDisplaySub, setting);
|
||||
} else if (setting.category == StrId::STR_CAT_READER) {
|
||||
addTo(readerSettings, lastReaderSub, setting);
|
||||
} else if (setting.category == StrId::STR_CAT_CONTROLS) {
|
||||
addTo(controlsSettings, lastControlsSub, setting);
|
||||
} else if (setting.category == StrId::STR_CAT_SYSTEM) {
|
||||
addTo(systemSettings, lastSystemSub, setting);
|
||||
// Enrich the font-family entry with SD card families discovered at boot.
|
||||
// The list itself is a namespace-static; we only mutate our local copy here.
|
||||
SettingInfo enriched = setting;
|
||||
if (setting.key && std::strcmp(setting.key, "fontFamily") == 0) {
|
||||
const uint8_t n = fontFamilyOptionCount();
|
||||
enriched.enumLabels.clear();
|
||||
enriched.enumLabels.reserve(n);
|
||||
for (uint8_t i = 0; i < n; i++) enriched.enumLabels.push_back(fontFamilyOptionLabel(i));
|
||||
}
|
||||
if (enriched.category == StrId::STR_CAT_DISPLAY) {
|
||||
addTo(displaySettings, lastDisplaySub, enriched);
|
||||
} else if (enriched.category == StrId::STR_CAT_READER) {
|
||||
addTo(readerSettings, lastReaderSub, enriched);
|
||||
} else if (enriched.category == StrId::STR_CAT_CONTROLS) {
|
||||
addTo(controlsSettings, lastControlsSub, enriched);
|
||||
} else if (enriched.category == StrId::STR_CAT_SYSTEM) {
|
||||
addTo(systemSettings, lastSystemSub, enriched);
|
||||
}
|
||||
// Web-only categories (KOReader Sync, OPDS Browser) are skipped for device UI
|
||||
}
|
||||
|
||||
+12
-1
@@ -26,6 +26,7 @@
|
||||
#include "MappedInputManager.h"
|
||||
#include "OpdsServerStore.h"
|
||||
#include "RecentBooksStore.h"
|
||||
#include "SdCardFontSystem.h"
|
||||
#include "WeatherSettingsStore.h"
|
||||
#include "activities/Activity.h"
|
||||
#include "activities/ActivityManager.h"
|
||||
@@ -40,7 +41,8 @@ ButtonEventManager& globalButtonEvents() { return buttonEventManager; }
|
||||
GfxRenderer renderer(display);
|
||||
ActivityManager activityManager(renderer, mappedInputManager);
|
||||
FontDecompressor fontDecompressor;
|
||||
FontCacheManager fontCacheManager(renderer.getFontMap());
|
||||
SdCardFontSystem sdFontSystem;
|
||||
FontCacheManager fontCacheManager(renderer.getFontMap(), renderer.getSdCardFonts());
|
||||
|
||||
// Fonts
|
||||
EpdFont bookerly14RegularFont(&bookerly_14_regular);
|
||||
@@ -182,9 +184,18 @@ void setupDisplayAndFonts() {
|
||||
renderer.insertFont(UI_10_FONT_ID, ui10FontFamily);
|
||||
renderer.insertFont(UI_12_FONT_ID, ui12FontFamily);
|
||||
renderer.insertFont(SMALL_FONT_ID, smallFontFamily);
|
||||
|
||||
// Discover SD card fonts (under /.crosspoint/fonts/) and load the family
|
||||
// currently selected in settings (if any). Safe to call without an SD card.
|
||||
sdFontSystem.begin(renderer);
|
||||
|
||||
LOG_DBG("MAIN", "Fonts setup");
|
||||
}
|
||||
|
||||
// Defined here to satisfy SdCardFontGlobals.h's extern declaration. Keeps
|
||||
// activity-side callers out of SdCardFontSystem internals.
|
||||
void ensureSdFontLoaded() { sdFontSystem.ensureLoaded(renderer); }
|
||||
|
||||
void setup() {
|
||||
{
|
||||
esp_ota_img_states_t otaState;
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
#include <esp_task_wdt.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstring>
|
||||
|
||||
#include "CrossPointSettings.h"
|
||||
#include "OpdsServerStore.h"
|
||||
@@ -1224,8 +1225,21 @@ void CrossPointWebServer::handleGetSettings() const {
|
||||
bool seenFirst = false;
|
||||
JsonDocument doc;
|
||||
|
||||
for (const auto& s : settings) {
|
||||
if (!s.key) continue; // Skip ACTION-only entries
|
||||
for (const auto& sBase : settings) {
|
||||
if (!sBase.key) continue; // Skip ACTION-only entries
|
||||
|
||||
// Enrich the font-family entry with current SD card families.
|
||||
SettingInfo sLocal;
|
||||
const SettingInfo* sPtr = &sBase;
|
||||
if (std::strcmp(sBase.key, "fontFamily") == 0) {
|
||||
sLocal = sBase;
|
||||
const uint8_t n = fontFamilyOptionCount();
|
||||
sLocal.enumLabels.clear();
|
||||
sLocal.enumLabels.reserve(n);
|
||||
for (uint8_t i = 0; i < n; i++) sLocal.enumLabels.push_back(fontFamilyOptionLabel(i));
|
||||
sPtr = &sLocal;
|
||||
}
|
||||
const SettingInfo& s = *sPtr;
|
||||
|
||||
doc.clear();
|
||||
doc["key"] = s.key;
|
||||
@@ -1337,7 +1351,11 @@ void CrossPointWebServer::handlePostSettings() {
|
||||
}
|
||||
case SettingType::ENUM: {
|
||||
const int val = doc[s.key].as<int>();
|
||||
const auto count = static_cast<int>(s.enumLabels.empty() ? s.enumValues.size() : s.enumLabels.size());
|
||||
// For fontFamily the enumLabels in the static list are empty by design
|
||||
// (built lazily by handleGetSettings); use the dynamic option count instead.
|
||||
const int count = (std::strcmp(s.key, "fontFamily") == 0)
|
||||
? static_cast<int>(fontFamilyOptionCount())
|
||||
: static_cast<int>(s.enumLabels.empty() ? s.enumValues.size() : s.enumLabels.size());
|
||||
if (val >= 0 && val < count) {
|
||||
if (s.valuePtr) {
|
||||
SETTINGS.*(s.valuePtr) = static_cast<uint8_t>(val);
|
||||
|
||||
Reference in New Issue
Block a user