feat: Add custom font selection from SD card

Allow users to select custom fonts (.epdfont files) from the
/.crosspoint/fonts/ directory on the SD card for EPUB/TXT reading.

Features:
- New FontSelectionActivity for browsing and selecting fonts
- SdFont and SdFontFamily classes for loading fonts from SD card
- Dynamic font reloading without device reboot
- Reader cache invalidation when font changes
- Hash-based font ID generation for proper cache management

The custom fonts use the .epdfont binary format which supports:
- 2-bit antialiasing for smooth text rendering
- Efficient on-demand glyph loading with LRU cache
- Memory-optimized design for ESP32-C3 constraints
This commit is contained in:
Eunchurn Park
2026-01-18 18:46:23 +09:00
parent 21277e03eb
commit 68ce6db291
16 changed files with 1968 additions and 82 deletions

View File

@@ -14,6 +14,9 @@ class EpdFontFamily {
const EpdFontData* getData(Style style = REGULAR) const;
const EpdGlyph* getGlyph(uint32_t cp, Style style = REGULAR) const;
// Check if bold variant is available (for synthetic bold decision)
bool hasBold() const { return bold != nullptr; }
private:
const EpdFont* regular;
const EpdFont* bold;
@@ -22,3 +25,10 @@ class EpdFontFamily {
const EpdFont* getFont(Style style) const;
};
// Global typedef for use outside class scope (needed by SdFontFamily and GfxRenderer)
using EpdFontStyle = EpdFontFamily::Style;
constexpr EpdFontStyle REGULAR = EpdFontFamily::REGULAR;
constexpr EpdFontStyle BOLD = EpdFontFamily::BOLD;
constexpr EpdFontStyle ITALIC = EpdFontFamily::ITALIC;
constexpr EpdFontStyle BOLD_ITALIC = EpdFontFamily::BOLD_ITALIC;

566
lib/EpdFont/SdFont.cpp Normal file
View File

@@ -0,0 +1,566 @@
#include "SdFont.h"
#include <Arduino.h>
#include <HardwareSerial.h>
#include <SDCardManager.h>
#include <Utf8.h>
#include <algorithm>
#include <cstring>
#include <new>
// ============================================================================
// GlyphBitmapCache Implementation
// ============================================================================
GlyphBitmapCache::GlyphBitmapCache(size_t maxSize) : maxCacheSize(maxSize), currentSize(0) {}
GlyphBitmapCache::~GlyphBitmapCache() { clear(); }
void GlyphBitmapCache::evictOldest() {
while (currentSize > maxCacheSize && !cacheList.empty()) {
auto& oldest = cacheList.back();
currentSize -= oldest.size;
cacheMap.erase(oldest.codepoint);
free(oldest.bitmap);
cacheList.pop_back();
}
}
const uint8_t* GlyphBitmapCache::get(uint32_t codepoint) {
auto it = cacheMap.find(codepoint);
if (it == cacheMap.end()) {
return nullptr;
}
// Move to front (most recently used)
if (it->second != cacheList.begin()) {
cacheList.splice(cacheList.begin(), cacheList, it->second);
}
return it->second->bitmap;
}
const uint8_t* GlyphBitmapCache::put(uint32_t codepoint, const uint8_t* data, uint32_t size) {
// Check if already cached
auto it = cacheMap.find(codepoint);
if (it != cacheMap.end()) {
// Move to front
if (it->second != cacheList.begin()) {
cacheList.splice(cacheList.begin(), cacheList, it->second);
}
return it->second->bitmap;
}
// Allocate and copy bitmap data
uint8_t* bitmapCopy = static_cast<uint8_t*>(malloc(size));
if (!bitmapCopy) {
Serial.printf("[%lu] [SdFont] Failed to allocate %u bytes for glyph cache\n", millis(), size);
return nullptr;
}
memcpy(bitmapCopy, data, size);
// Add to cache
CacheEntry entry = {codepoint, bitmapCopy, size};
cacheList.push_front(entry);
cacheMap[codepoint] = cacheList.begin();
currentSize += size;
// Evict if over limit
evictOldest();
return bitmapCopy;
}
void GlyphBitmapCache::clear() {
for (auto& entry : cacheList) {
free(entry.bitmap);
}
cacheList.clear();
cacheMap.clear();
currentSize = 0;
}
// ============================================================================
// GlyphMetadataCache Implementation (simple fixed-size circular buffer)
// ============================================================================
const EpdGlyph* GlyphMetadataCache::get(uint32_t codepoint) {
// Linear search through cache (simple but effective for small cache)
for (size_t i = 0; i < MAX_ENTRIES; i++) {
if (entries[i].valid && entries[i].codepoint == codepoint) {
return &entries[i].glyph;
}
}
return nullptr;
}
const EpdGlyph* GlyphMetadataCache::put(uint32_t codepoint, const EpdGlyph& glyph) {
// Check if already cached
for (size_t i = 0; i < MAX_ENTRIES; i++) {
if (entries[i].valid && entries[i].codepoint == codepoint) {
return &entries[i].glyph;
}
}
// Add to next slot (circular overwrite)
entries[nextSlot].codepoint = codepoint;
entries[nextSlot].glyph = glyph;
entries[nextSlot].valid = true;
const EpdGlyph* result = &entries[nextSlot].glyph;
nextSlot = (nextSlot + 1) % MAX_ENTRIES;
return result;
}
void GlyphMetadataCache::clear() {
for (size_t i = 0; i < MAX_ENTRIES; i++) {
entries[i].valid = false;
}
nextSlot = 0;
}
// ============================================================================
// SdFontData Implementation
// ============================================================================
// Static members
GlyphBitmapCache* SdFontData::sharedCache = nullptr;
int SdFontData::cacheRefCount = 0;
SdFontData::SdFontData(const char* path) : filePath(path), loaded(false), intervals(nullptr) {
memset(&header, 0, sizeof(header));
// Initialize shared cache on first SdFontData creation
// Use larger cache (64KB) to improve performance with Korean fonts
if (sharedCache == nullptr) {
sharedCache = new GlyphBitmapCache(32768); // 32KB cache (conserve memory for XTC)
}
cacheRefCount++;
}
SdFontData::~SdFontData() {
if (fontFile) {
fontFile.close();
}
delete[] intervals;
// Cleanup shared cache when last SdFontData is destroyed
cacheRefCount--;
if (cacheRefCount == 0 && sharedCache != nullptr) {
delete sharedCache;
sharedCache = nullptr;
}
}
SdFontData::SdFontData(SdFontData&& other) noexcept
: filePath(std::move(other.filePath)), loaded(other.loaded), header(other.header), intervals(other.intervals) {
other.intervals = nullptr;
other.loaded = false;
cacheRefCount++; // New instance references the cache
}
SdFontData& SdFontData::operator=(SdFontData&& other) noexcept {
if (this != &other) {
// Clean up current resources
if (fontFile) {
fontFile.close();
}
delete[] intervals;
// Move from other
filePath = std::move(other.filePath);
loaded = other.loaded;
header = other.header;
intervals = other.intervals;
other.intervals = nullptr;
other.loaded = false;
}
return *this;
}
// Maximum reasonable values for validation
// CJK fonts (Korean + Chinese + Japanese) can have 120K+ glyphs
// Glyphs are loaded on-demand from SD, so high count doesn't affect memory
static constexpr uint32_t MAX_INTERVAL_COUNT = 10000;
static constexpr uint32_t MAX_GLYPH_COUNT = 150000;
static constexpr size_t MIN_FREE_HEAP_AFTER_LOAD = 16384; // 16KB minimum heap after loading
bool SdFontData::load() {
if (loaded) {
return true;
}
// Check available heap before attempting to load
size_t freeHeap = ESP.getFreeHeap();
if (freeHeap < MIN_FREE_HEAP_AFTER_LOAD) {
Serial.printf("[%lu] [SdFont] Insufficient heap: %u bytes (need %u)\n", millis(), freeHeap,
MIN_FREE_HEAP_AFTER_LOAD);
return false;
}
// Open font file
if (!SdMan.openFileForRead("SdFont", filePath.c_str(), fontFile)) {
Serial.printf("[%lu] [SdFont] Failed to open font file: %s\n", millis(), filePath.c_str());
return false;
}
// Read and validate header
if (fontFile.read(&header, sizeof(EpdFontHeader)) != sizeof(EpdFontHeader)) {
Serial.printf("[%lu] [SdFont] Failed to read header from: %s\n", millis(), filePath.c_str());
fontFile.close();
return false;
}
// Validate magic number
if (header.magic != EPDFONT_MAGIC) {
Serial.printf("[%lu] [SdFont] Invalid magic: 0x%08X (expected 0x%08X)\n", millis(), header.magic, EPDFONT_MAGIC);
fontFile.close();
return false;
}
// Validate version
if (header.version != EPDFONT_VERSION) {
Serial.printf("[%lu] [SdFont] Bad version: %u (expected %u)\n", millis(), header.version, EPDFONT_VERSION);
fontFile.close();
return false;
}
// Validate header values to prevent memory issues
if (header.intervalCount > MAX_INTERVAL_COUNT) {
Serial.printf("[%lu] [SdFont] Too many intervals: %u (max %u)\n", millis(), header.intervalCount,
MAX_INTERVAL_COUNT);
fontFile.close();
return false;
}
if (header.glyphCount > MAX_GLYPH_COUNT) {
Serial.printf("[%lu] [SdFont] Too many glyphs: %u (max %u)\n", millis(), header.glyphCount, MAX_GLYPH_COUNT);
fontFile.close();
return false;
}
// Calculate required memory - only intervals are loaded into RAM
// Glyphs are loaded on-demand from SD card to save memory
size_t intervalsMemory = header.intervalCount * sizeof(EpdFontInterval);
if (intervalsMemory > freeHeap - MIN_FREE_HEAP_AFTER_LOAD) {
Serial.printf("[%lu] [SdFont] Not enough memory for intervals: need %u, have %u\n", millis(), intervalsMemory,
freeHeap);
fontFile.close();
return false;
}
Serial.printf("[%lu] [SdFont] Loading %s: %u intervals, %u glyphs (on-demand)\n", millis(), filePath.c_str(),
header.intervalCount, header.glyphCount);
// Allocate intervals array
intervals = new (std::nothrow) EpdFontInterval[header.intervalCount];
if (intervals == nullptr) {
Serial.printf("[%lu] [SdFont] Failed to allocate intervals (%u bytes)\n", millis(), intervalsMemory);
fontFile.close();
return false;
}
// Read intervals - data should be contiguous after header, but verify offset
// Expected offset for intervals is 32 (right after header)
if (header.intervalsOffset != sizeof(EpdFontHeader)) {
// Need to seek - file layout is non-standard
if (!fontFile.seekSet(header.intervalsOffset)) {
Serial.printf("[%lu] [SdFont] Failed to seek to intervals at %u\n", millis(), header.intervalsOffset);
fontFile.close();
delete[] intervals;
intervals = nullptr;
return false;
}
}
// Otherwise, we're already positioned right after header - read directly
if (fontFile.read(intervals, intervalsMemory) != static_cast<int>(intervalsMemory)) {
Serial.printf("[%lu] [SdFont] Failed to read intervals\n", millis());
fontFile.close();
delete[] intervals;
intervals = nullptr;
return false;
}
// Close the file after loading intervals - we'll reopen when reading glyphs/bitmaps
fontFile.close();
loaded = true;
Serial.printf("[%lu] [SdFont] Loaded: %s (advanceY=%u, intervals=%uKB)\n", millis(), filePath.c_str(),
header.advanceY, intervalsMemory / 1024);
return true;
}
bool SdFontData::ensureFileOpen() const {
if (fontFile && fontFile.isOpen()) {
return true;
}
return SdMan.openFileForRead("SdFont", filePath.c_str(), fontFile);
}
bool SdFontData::loadGlyphFromSD(int glyphIndex, EpdGlyph* outGlyph) const {
if (!loaded || glyphIndex < 0 || glyphIndex >= static_cast<int>(header.glyphCount)) {
return false;
}
// Keep file open for better performance
if (!ensureFileOpen()) {
return false;
}
// Calculate position in file
uint32_t glyphFileOffset = header.glyphsOffset + (glyphIndex * sizeof(EpdFontGlyph));
if (!fontFile.seekSet(glyphFileOffset)) {
return false;
}
// Read the glyph from file format
EpdFontGlyph fileGlyph;
if (fontFile.read(&fileGlyph, sizeof(EpdFontGlyph)) != sizeof(EpdFontGlyph)) {
return false;
}
// Convert from file format to runtime format
outGlyph->width = fileGlyph.width;
outGlyph->height = fileGlyph.height;
outGlyph->advanceX = fileGlyph.advanceX;
outGlyph->left = fileGlyph.left;
outGlyph->top = fileGlyph.top;
outGlyph->dataLength = static_cast<uint16_t>(fileGlyph.dataLength);
outGlyph->dataOffset = fileGlyph.dataOffset;
return true;
}
int SdFontData::findGlyphIndex(uint32_t codepoint) const {
if (!loaded || intervals == nullptr) {
return -1;
}
// Binary search for the interval containing this codepoint
int left = 0;
int right = static_cast<int>(header.intervalCount) - 1;
while (left <= right) {
int mid = left + (right - left) / 2;
const EpdFontInterval* interval = &intervals[mid];
if (codepoint < interval->first) {
right = mid - 1;
} else if (codepoint > interval->last) {
left = mid + 1;
} else {
// Found: codepoint is within this interval
return static_cast<int>(interval->offset + (codepoint - interval->first));
}
}
return -1; // Not found
}
const EpdGlyph* SdFontData::getGlyph(uint32_t codepoint) const {
if (!loaded) {
return nullptr;
}
// Check cache first
const EpdGlyph* cached = glyphCache.get(codepoint);
if (cached != nullptr) {
return cached;
}
// Find glyph index using binary search on intervals
int index = findGlyphIndex(codepoint);
if (index < 0 || index >= static_cast<int>(header.glyphCount)) {
return nullptr;
}
// Load glyph from SD card
EpdGlyph glyph;
if (!loadGlyphFromSD(index, &glyph)) {
return nullptr;
}
// Store in cache and return pointer to cached copy
return glyphCache.put(codepoint, glyph);
}
const uint8_t* SdFontData::getGlyphBitmap(uint32_t codepoint) const {
if (!loaded || sharedCache == nullptr) {
return nullptr;
}
// Check cache first
const uint8_t* cached = sharedCache->get(codepoint);
if (cached != nullptr) {
return cached;
}
// Find glyph index
int glyphIndex = findGlyphIndex(codepoint);
if (glyphIndex < 0 || glyphIndex >= static_cast<int>(header.glyphCount)) {
return nullptr;
}
// Ensure file is open (keeps file handle open for performance)
if (!ensureFileOpen()) {
return nullptr;
}
// Read glyph metadata first (we need dataLength and dataOffset)
uint32_t glyphFileOffset = header.glyphsOffset + (glyphIndex * sizeof(EpdFontGlyph));
if (!fontFile.seekSet(glyphFileOffset)) {
return nullptr;
}
EpdFontGlyph fileGlyph;
if (fontFile.read(&fileGlyph, sizeof(EpdFontGlyph)) != sizeof(EpdFontGlyph)) {
return nullptr;
}
if (fileGlyph.dataLength == 0) {
return nullptr;
}
// Seek to bitmap data
if (!fontFile.seekSet(header.bitmapOffset + fileGlyph.dataOffset)) {
return nullptr;
}
// Allocate temporary buffer for reading
uint8_t* tempBuffer = static_cast<uint8_t*>(malloc(fileGlyph.dataLength));
if (!tempBuffer) {
return nullptr;
}
if (fontFile.read(tempBuffer, fileGlyph.dataLength) != static_cast<int>(fileGlyph.dataLength)) {
free(tempBuffer);
return nullptr;
}
// File stays open for next glyph read (performance optimization)
// Store in cache
const uint8_t* result = sharedCache->put(codepoint, tempBuffer, fileGlyph.dataLength);
free(tempBuffer);
return result;
}
void SdFontData::setCacheSize(size_t maxBytes) {
if (sharedCache != nullptr) {
delete sharedCache;
}
sharedCache = new GlyphBitmapCache(maxBytes);
}
void SdFontData::clearCache() {
if (sharedCache != nullptr) {
sharedCache->clear();
}
}
size_t SdFontData::getCacheUsedSize() {
if (sharedCache != nullptr) {
return sharedCache->getUsedSize();
}
return 0;
}
// ============================================================================
// SdFont Implementation
// ============================================================================
SdFont::SdFont(SdFontData* fontData, bool takeOwnership) : data(fontData), ownsData(takeOwnership) {}
SdFont::SdFont(const char* filePath) : data(new SdFontData(filePath)), ownsData(true) {}
SdFont::~SdFont() {
if (ownsData) {
delete data;
}
}
SdFont::SdFont(SdFont&& other) noexcept : data(other.data), ownsData(other.ownsData) {
other.data = nullptr;
other.ownsData = false;
}
SdFont& SdFont::operator=(SdFont&& other) noexcept {
if (this != &other) {
if (ownsData) {
delete data;
}
data = other.data;
ownsData = other.ownsData;
other.data = nullptr;
other.ownsData = false;
}
return *this;
}
bool SdFont::load() {
if (data == nullptr) {
return false;
}
return data->load();
}
void SdFont::getTextDimensions(const char* string, int* w, int* h) const {
*w = 0;
*h = 0;
if (data == nullptr || !data->isLoaded() || string == nullptr || *string == '\0') {
return;
}
int minX = 0, minY = 0, maxX = 0, maxY = 0;
int cursorX = 0;
const int cursorY = 0;
uint32_t cp;
while ((cp = utf8NextCodepoint(reinterpret_cast<const uint8_t**>(&string)))) {
const EpdGlyph* glyph = data->getGlyph(cp);
if (!glyph) {
glyph = data->getGlyph('?');
}
if (!glyph) {
continue;
}
minX = std::min(minX, cursorX + glyph->left);
maxX = std::max(maxX, cursorX + glyph->left + glyph->width);
minY = std::min(minY, cursorY + glyph->top - glyph->height);
maxY = std::max(maxY, cursorY + glyph->top);
cursorX += glyph->advanceX;
}
*w = maxX - minX;
*h = maxY - minY;
}
bool SdFont::hasPrintableChars(const char* string) const {
int w = 0, h = 0;
getTextDimensions(string, &w, &h);
return w > 0 || h > 0;
}
const EpdGlyph* SdFont::getGlyph(uint32_t cp) const {
if (data == nullptr) {
return nullptr;
}
return data->getGlyph(cp);
}
const uint8_t* SdFont::getGlyphBitmap(uint32_t cp) const {
if (data == nullptr) {
return nullptr;
}
return data->getGlyphBitmap(cp);
}

184
lib/EpdFont/SdFont.h Normal file
View File

@@ -0,0 +1,184 @@
#pragma once
#include <SdFat.h>
#include <cstdint>
#include <list>
#include <string>
#include <unordered_map>
#include "EpdFontData.h"
#include "SdFontFormat.h"
/**
* LRU Cache for glyph bitmap data loaded from SD card.
* Automatically evicts least recently used entries when memory limit is reached.
*/
class GlyphBitmapCache {
public:
struct CacheEntry {
uint32_t codepoint;
uint8_t* bitmap;
uint32_t size;
};
private:
size_t maxCacheSize;
size_t currentSize;
std::list<CacheEntry> cacheList; // Most recent at front
std::unordered_map<uint32_t, std::list<CacheEntry>::iterator> cacheMap;
void evictOldest();
public:
explicit GlyphBitmapCache(size_t maxSize = 32768); // Default 32KB cache
~GlyphBitmapCache();
// Returns cached bitmap or nullptr if not cached
const uint8_t* get(uint32_t codepoint);
// Stores bitmap in cache, returns pointer to cached data
const uint8_t* put(uint32_t codepoint, const uint8_t* data, uint32_t size);
void clear();
size_t getUsedSize() const { return currentSize; }
size_t getMaxSize() const { return maxCacheSize; }
};
/**
* SD Card font data structure.
* Mimics EpdFontData interface but loads data on-demand from SD card.
*/
/**
* Simple fixed-size cache for glyph metadata (EpdGlyph) loaded on-demand.
* Uses a simple circular buffer to avoid STL container overhead on ESP32.
*/
class GlyphMetadataCache {
public:
static constexpr size_t MAX_ENTRIES = 128; // Balanced for Korean text while conserving memory
struct CacheEntry {
uint32_t codepoint;
EpdGlyph glyph;
bool valid;
};
private:
CacheEntry entries[MAX_ENTRIES];
size_t nextSlot;
public:
GlyphMetadataCache() : nextSlot(0) {
for (size_t i = 0; i < MAX_ENTRIES; i++) {
entries[i].valid = false;
}
}
const EpdGlyph* get(uint32_t codepoint);
const EpdGlyph* put(uint32_t codepoint, const EpdGlyph& glyph);
void clear();
};
class SdFontData {
private:
std::string filePath;
bool loaded;
// Font metadata (loaded once, kept in RAM)
EpdFontHeader header;
EpdFontInterval* intervals; // Dynamically allocated (~40KB for Korean)
// Note: glyphs are NOT preloaded - loaded on-demand to save memory
// Glyph metadata cache (per-font, small LRU cache)
mutable GlyphMetadataCache glyphCache;
// Bitmap cache (shared across all SdFontData instances)
static GlyphBitmapCache* sharedCache;
static int cacheRefCount;
// File handle for reading (opened on demand)
mutable FsFile fontFile;
// Binary search for glyph index
int findGlyphIndex(uint32_t codepoint) const;
// Load a single glyph from SD card by index
bool loadGlyphFromSD(int glyphIndex, EpdGlyph* outGlyph) const;
// Ensure font file is open (keeps handle open for performance)
bool ensureFileOpen() const;
public:
explicit SdFontData(const char* path);
~SdFontData();
// Disable copy to prevent resource issues
SdFontData(const SdFontData&) = delete;
SdFontData& operator=(const SdFontData&) = delete;
// Move constructor and assignment
SdFontData(SdFontData&& other) noexcept;
SdFontData& operator=(SdFontData&& other) noexcept;
// Load font header and metadata from SD card
bool load();
bool isLoaded() const { return loaded; }
// EpdFontData-compatible getters
uint8_t getAdvanceY() const { return header.advanceY; }
int8_t getAscender() const { return header.ascender; }
int8_t getDescender() const { return header.descender; }
bool is2Bit() const { return header.is2Bit != 0; }
uint32_t getIntervalCount() const { return header.intervalCount; }
uint32_t getGlyphCount() const { return header.glyphCount; }
// Get glyph by codepoint (loads bitmap on demand)
const EpdGlyph* getGlyph(uint32_t codepoint) const;
// Get bitmap for a glyph (loads from SD if not cached)
const uint8_t* getGlyphBitmap(uint32_t codepoint) const;
// Static cache management
static void setCacheSize(size_t maxBytes);
static void clearCache();
static size_t getCacheUsedSize();
};
/**
* SD Card font class - similar interface to EpdFont but loads from SD card.
*/
class SdFont {
private:
SdFontData* data;
bool ownsData;
public:
explicit SdFont(SdFontData* fontData, bool takeOwnership = false);
explicit SdFont(const char* filePath);
~SdFont();
// Disable copy
SdFont(const SdFont&) = delete;
SdFont& operator=(const SdFont&) = delete;
// Move semantics
SdFont(SdFont&& other) noexcept;
SdFont& operator=(SdFont&& other) noexcept;
bool load();
bool isLoaded() const { return data && data->isLoaded(); }
// EpdFont-compatible interface
void getTextDimensions(const char* string, int* w, int* h) const;
bool hasPrintableChars(const char* string) const;
const EpdGlyph* getGlyph(uint32_t cp) const;
const uint8_t* getGlyphBitmap(uint32_t cp) const;
// Metadata accessors
uint8_t getAdvanceY() const { return data ? data->getAdvanceY() : 0; }
int8_t getAscender() const { return data ? data->getAscender() : 0; }
int8_t getDescender() const { return data ? data->getDescender() : 0; }
bool is2Bit() const { return data ? data->is2Bit() : false; }
SdFontData* getData() const { return data; }
};

View File

@@ -0,0 +1,296 @@
#include "SdFontFamily.h"
#include <HardwareSerial.h>
// ============================================================================
// SdFontFamily Implementation
// ============================================================================
SdFontFamily::SdFontFamily(const char* regularPath, const char* boldPath, const char* italicPath,
const char* boldItalicPath)
: regular(nullptr), bold(nullptr), italic(nullptr), boldItalic(nullptr), ownsPointers(true) {
if (regularPath) {
regular = new SdFont(regularPath);
}
if (boldPath) {
bold = new SdFont(boldPath);
}
if (italicPath) {
italic = new SdFont(italicPath);
}
if (boldItalicPath) {
boldItalic = new SdFont(boldItalicPath);
}
}
SdFontFamily::~SdFontFamily() {
if (ownsPointers) {
delete regular;
delete bold;
delete italic;
delete boldItalic;
}
}
SdFontFamily::SdFontFamily(SdFontFamily&& other) noexcept
: regular(other.regular),
bold(other.bold),
italic(other.italic),
boldItalic(other.boldItalic),
ownsPointers(other.ownsPointers) {
other.regular = nullptr;
other.bold = nullptr;
other.italic = nullptr;
other.boldItalic = nullptr;
other.ownsPointers = false;
}
SdFontFamily& SdFontFamily::operator=(SdFontFamily&& other) noexcept {
if (this != &other) {
if (ownsPointers) {
delete regular;
delete bold;
delete italic;
delete boldItalic;
}
regular = other.regular;
bold = other.bold;
italic = other.italic;
boldItalic = other.boldItalic;
ownsPointers = other.ownsPointers;
other.regular = nullptr;
other.bold = nullptr;
other.italic = nullptr;
other.boldItalic = nullptr;
other.ownsPointers = false;
}
return *this;
}
bool SdFontFamily::load() {
bool success = true;
if (regular && !regular->load()) {
Serial.printf("[%lu] [SdFontFamily] Failed to load regular font\n", millis());
success = false;
}
if (bold && !bold->load()) {
Serial.printf("[%lu] [SdFontFamily] Failed to load bold font\n", millis());
// Bold is optional, don't fail completely
}
if (italic && !italic->load()) {
Serial.printf("[%lu] [SdFontFamily] Failed to load italic font\n", millis());
// Italic is optional
}
if (boldItalic && !boldItalic->load()) {
Serial.printf("[%lu] [SdFontFamily] Failed to load bold-italic font\n", millis());
// Bold-italic is optional
}
return success;
}
bool SdFontFamily::isLoaded() const { return regular && regular->isLoaded(); }
SdFont* SdFontFamily::getFont(EpdFontStyle style) const {
if (style == BOLD && bold && bold->isLoaded()) {
return bold;
}
if (style == ITALIC && italic && italic->isLoaded()) {
return italic;
}
if (style == BOLD_ITALIC) {
if (boldItalic && boldItalic->isLoaded()) {
return boldItalic;
}
if (bold && bold->isLoaded()) {
return bold;
}
if (italic && italic->isLoaded()) {
return italic;
}
}
return regular;
}
void SdFontFamily::getTextDimensions(const char* string, int* w, int* h, EpdFontStyle style) const {
SdFont* font = getFont(style);
if (font) {
font->getTextDimensions(string, w, h);
} else {
*w = 0;
*h = 0;
}
}
bool SdFontFamily::hasPrintableChars(const char* string, EpdFontStyle style) const {
SdFont* font = getFont(style);
return font ? font->hasPrintableChars(string) : false;
}
const EpdGlyph* SdFontFamily::getGlyph(uint32_t cp, EpdFontStyle style) const {
SdFont* font = getFont(style);
return font ? font->getGlyph(cp) : nullptr;
}
const uint8_t* SdFontFamily::getGlyphBitmap(uint32_t cp, EpdFontStyle style) const {
SdFont* font = getFont(style);
return font ? font->getGlyphBitmap(cp) : nullptr;
}
uint8_t SdFontFamily::getAdvanceY(EpdFontStyle style) const {
SdFont* font = getFont(style);
return font ? font->getAdvanceY() : 0;
}
int8_t SdFontFamily::getAscender(EpdFontStyle style) const {
SdFont* font = getFont(style);
return font ? font->getAscender() : 0;
}
int8_t SdFontFamily::getDescender(EpdFontStyle style) const {
SdFont* font = getFont(style);
return font ? font->getDescender() : 0;
}
bool SdFontFamily::is2Bit(EpdFontStyle style) const {
SdFont* font = getFont(style);
return font ? font->is2Bit() : false;
}
// ============================================================================
// UnifiedFontFamily Implementation
// ============================================================================
UnifiedFontFamily::UnifiedFontFamily(const EpdFontFamily* font) : type(Type::FLASH), flashFont(font), sdFont(nullptr) {}
UnifiedFontFamily::UnifiedFontFamily(SdFontFamily* font) : type(Type::SD), flashFont(nullptr), sdFont(font) {}
UnifiedFontFamily::~UnifiedFontFamily() {
// flashFont is not owned (points to global), don't delete
delete sdFont;
}
UnifiedFontFamily::UnifiedFontFamily(UnifiedFontFamily&& other) noexcept
: type(other.type), flashFont(other.flashFont), sdFont(other.sdFont) {
other.flashFont = nullptr;
other.sdFont = nullptr;
}
UnifiedFontFamily& UnifiedFontFamily::operator=(UnifiedFontFamily&& other) noexcept {
if (this != &other) {
// flashFont is not owned (points to global), don't delete
delete sdFont;
type = other.type;
flashFont = other.flashFont;
sdFont = other.sdFont;
other.flashFont = nullptr;
other.sdFont = nullptr;
}
return *this;
}
void UnifiedFontFamily::getTextDimensions(const char* string, int* w, int* h, EpdFontStyle style) const {
if (type == Type::FLASH && flashFont) {
flashFont->getTextDimensions(string, w, h, style);
} else if (sdFont) {
sdFont->getTextDimensions(string, w, h, style);
} else {
*w = 0;
*h = 0;
}
}
bool UnifiedFontFamily::hasPrintableChars(const char* string, EpdFontStyle style) const {
if (type == Type::FLASH && flashFont) {
return flashFont->hasPrintableChars(string, style);
} else if (sdFont) {
return sdFont->hasPrintableChars(string, style);
}
return false;
}
const EpdGlyph* UnifiedFontFamily::getGlyph(uint32_t cp, EpdFontStyle style) const {
if (type == Type::FLASH && flashFont) {
return flashFont->getGlyph(cp, style);
} else if (sdFont) {
return sdFont->getGlyph(cp, style);
}
return nullptr;
}
const uint8_t* UnifiedFontFamily::getGlyphBitmap(uint32_t cp, EpdFontStyle style) const {
if (type == Type::FLASH && flashFont) {
// For flash fonts, get bitmap from the data structure
const EpdFontData* data = flashFont->getData(style);
const EpdGlyph* glyph = flashFont->getGlyph(cp, style);
if (data && glyph) {
return &data->bitmap[glyph->dataOffset];
}
return nullptr;
} else if (sdFont) {
return sdFont->getGlyphBitmap(cp, style);
}
return nullptr;
}
uint8_t UnifiedFontFamily::getAdvanceY(EpdFontStyle style) const {
if (type == Type::FLASH && flashFont) {
const EpdFontData* data = flashFont->getData(style);
return data ? data->advanceY : 0;
} else if (sdFont) {
return sdFont->getAdvanceY(style);
}
return 0;
}
int8_t UnifiedFontFamily::getAscender(EpdFontStyle style) const {
if (type == Type::FLASH && flashFont) {
const EpdFontData* data = flashFont->getData(style);
return data ? data->ascender : 0;
} else if (sdFont) {
return sdFont->getAscender(style);
}
return 0;
}
int8_t UnifiedFontFamily::getDescender(EpdFontStyle style) const {
if (type == Type::FLASH && flashFont) {
const EpdFontData* data = flashFont->getData(style);
return data ? data->descender : 0;
} else if (sdFont) {
return sdFont->getDescender(style);
}
return 0;
}
bool UnifiedFontFamily::is2Bit(EpdFontStyle style) const {
if (type == Type::FLASH && flashFont) {
const EpdFontData* data = flashFont->getData(style);
return data ? data->is2Bit : false;
} else if (sdFont) {
return sdFont->is2Bit(style);
}
return false;
}
const EpdFontData* UnifiedFontFamily::getFlashData(EpdFontStyle style) const {
if (type == Type::FLASH && flashFont) {
return flashFont->getData(style);
}
return nullptr;
}
bool UnifiedFontFamily::hasBold() const {
if (type == Type::FLASH && flashFont) {
return flashFont->hasBold();
} else if (sdFont) {
return sdFont->hasBold();
}
return false;
}

115
lib/EpdFont/SdFontFamily.h Normal file
View File

@@ -0,0 +1,115 @@
#pragma once
#include "EpdFontFamily.h"
#include "SdFont.h"
/**
* SD Card font family - similar interface to EpdFontFamily but uses SdFont.
* Supports regular, bold, italic, and bold-italic variants.
*/
class SdFontFamily {
private:
SdFont* regular;
SdFont* bold;
SdFont* italic;
SdFont* boldItalic;
bool ownsPointers;
SdFont* getFont(EpdFontStyle style) const;
public:
// Constructor with raw pointers (does not take ownership)
explicit SdFontFamily(SdFont* regular, SdFont* bold = nullptr, SdFont* italic = nullptr, SdFont* boldItalic = nullptr)
: regular(regular), bold(bold), italic(italic), boldItalic(boldItalic), ownsPointers(false) {}
// Constructor with file paths (creates and owns SdFont objects)
explicit SdFontFamily(const char* regularPath, const char* boldPath = nullptr, const char* italicPath = nullptr,
const char* boldItalicPath = nullptr);
~SdFontFamily();
// Disable copy
SdFontFamily(const SdFontFamily&) = delete;
SdFontFamily& operator=(const SdFontFamily&) = delete;
// Enable move
SdFontFamily(SdFontFamily&& other) noexcept;
SdFontFamily& operator=(SdFontFamily&& other) noexcept;
// Load all fonts in the family
bool load();
bool isLoaded() const;
// EpdFontFamily-compatible interface
void getTextDimensions(const char* string, int* w, int* h, EpdFontStyle style = REGULAR) const;
bool hasPrintableChars(const char* string, EpdFontStyle style = REGULAR) const;
// Get glyph (metadata only, no bitmap)
const EpdGlyph* getGlyph(uint32_t cp, EpdFontStyle style = REGULAR) const;
// Get glyph bitmap data (loaded on demand from SD)
const uint8_t* getGlyphBitmap(uint32_t cp, EpdFontStyle style = REGULAR) const;
// Font metadata
uint8_t getAdvanceY(EpdFontStyle style = REGULAR) const;
int8_t getAscender(EpdFontStyle style = REGULAR) const;
int8_t getDescender(EpdFontStyle style = REGULAR) const;
bool is2Bit(EpdFontStyle style = REGULAR) const;
// Check if bold variant is available
bool hasBold() const { return bold != nullptr; }
};
/**
* Unified font family that can hold either EpdFontFamily (flash) or SdFontFamily (SD card).
* This allows GfxRenderer to work with both types transparently.
*/
class UnifiedFontFamily {
public:
enum class Type { FLASH, SD };
private:
Type type;
const EpdFontFamily* flashFont; // Non-owning pointer for flash fonts (they're global)
SdFontFamily* sdFont; // Owned pointer for SD fonts
public:
// Construct from flash font (EpdFontFamily) - stores pointer, does not copy
explicit UnifiedFontFamily(const EpdFontFamily* font);
// Construct from SD font family (takes ownership)
explicit UnifiedFontFamily(SdFontFamily* font);
~UnifiedFontFamily();
// Disable copy
UnifiedFontFamily(const UnifiedFontFamily&) = delete;
UnifiedFontFamily& operator=(const UnifiedFontFamily&) = delete;
// Enable move
UnifiedFontFamily(UnifiedFontFamily&& other) noexcept;
UnifiedFontFamily& operator=(UnifiedFontFamily&& other) noexcept;
Type getType() const { return type; }
bool isSdFont() const { return type == Type::SD; }
// Unified interface
void getTextDimensions(const char* string, int* w, int* h, EpdFontStyle style = REGULAR) const;
bool hasPrintableChars(const char* string, EpdFontStyle style = REGULAR) const;
const EpdGlyph* getGlyph(uint32_t cp, EpdFontStyle style = REGULAR) const;
// For SD fonts: get bitmap data (for flash fonts, use getData()->bitmap[offset])
const uint8_t* getGlyphBitmap(uint32_t cp, EpdFontStyle style = REGULAR) const;
// Metadata (common interface)
uint8_t getAdvanceY(EpdFontStyle style = REGULAR) const;
int8_t getAscender(EpdFontStyle style = REGULAR) const;
int8_t getDescender(EpdFontStyle style = REGULAR) const;
bool is2Bit(EpdFontStyle style = REGULAR) const;
// Flash font specific (returns nullptr for SD fonts)
const EpdFontData* getFlashData(EpdFontStyle style = REGULAR) const;
// Check if bold variant is available (for synthetic bold decision)
bool hasBold() const;
};

View File

@@ -0,0 +1,79 @@
/**
* .epdfont Binary Font Format Specification
*
* This format is designed for on-demand loading from SD card
* with minimal RAM usage on embedded devices.
*
* File Layout:
* ┌─────────────────────────────────────────────────────┐
* │ Header (32 bytes) │
* ├─────────────────────────────────────────────────────┤
* │ Intervals[] (intervalCount × 12 bytes) │
* ├─────────────────────────────────────────────────────┤
* │ Glyphs[] (glyphCount × 16 bytes) │
* ├─────────────────────────────────────────────────────┤
* │ Bitmap data (variable size) │
* └─────────────────────────────────────────────────────┘
*/
#pragma once
#include <cstdint>
// Magic number: "EPDF" in little-endian
#define EPDFONT_MAGIC 0x46445045
// Current format version
#define EPDFONT_VERSION 1
#pragma pack(push, 1)
/**
* File header - 32 bytes
*/
struct EpdFontHeader {
uint32_t magic; // 0x46445045 ("EPDF")
uint16_t version; // Format version (1)
uint8_t is2Bit; // 1 = 2-bit grayscale, 0 = 1-bit
uint8_t reserved1; // Reserved for alignment
uint8_t advanceY; // Line height
int8_t ascender; // Max height above baseline
int8_t descender; // Max depth below baseline (negative)
uint8_t reserved2; // Reserved for alignment
uint32_t intervalCount; // Number of unicode intervals
uint32_t glyphCount; // Total number of glyphs
uint32_t intervalsOffset; // File offset to intervals array
uint32_t glyphsOffset; // File offset to glyphs array
uint32_t bitmapOffset; // File offset to bitmap data
};
/**
* Unicode interval - 12 bytes
* Same as EpdUnicodeInterval but with explicit packing
*/
struct EpdFontInterval {
uint32_t first; // First unicode code point
uint32_t last; // Last unicode code point
uint32_t offset; // Index into glyph array
};
/**
* Glyph data - 16 bytes
* Same as EpdGlyph but with explicit packing
*/
struct EpdFontGlyph {
uint8_t width; // Bitmap width in pixels
uint8_t height; // Bitmap height in pixels
uint8_t advanceX; // Horizontal advance
uint8_t reserved; // Reserved for alignment
int16_t left; // X offset from cursor
int16_t top; // Y offset from cursor
uint32_t dataLength; // Bitmap data size in bytes
uint32_t dataOffset; // Offset into bitmap section
};
#pragma pack(pop)
// Sanity checks for struct sizes
static_assert(sizeof(EpdFontHeader) == 32, "EpdFontHeader must be 32 bytes");
static_assert(sizeof(EpdFontInterval) == 12, "EpdFontInterval must be 12 bytes");
static_assert(sizeof(EpdFontGlyph) == 16, "EpdFontGlyph must be 16 bytes");