mirror of
https://github.com/daveallie/crosspoint-reader.git
synced 2026-02-06 15:47:39 +03:00
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:
parent
21277e03eb
commit
68ce6db291
@ -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
566
lib/EpdFont/SdFont.cpp
Normal 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
184
lib/EpdFont/SdFont.h
Normal 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; }
|
||||
};
|
||||
296
lib/EpdFont/SdFontFamily.cpp
Normal file
296
lib/EpdFont/SdFontFamily.cpp
Normal 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
115
lib/EpdFont/SdFontFamily.h
Normal 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;
|
||||
};
|
||||
79
lib/EpdFont/SdFontFormat.h
Normal file
79
lib/EpdFont/SdFontFormat.h
Normal 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");
|
||||
@ -2,7 +2,40 @@
|
||||
|
||||
#include <Utf8.h>
|
||||
|
||||
void GfxRenderer::insertFont(const int fontId, EpdFontFamily font) { fontMap.insert({fontId, font}); }
|
||||
void GfxRenderer::insertFont(const int fontId, const EpdFontFamily* font) {
|
||||
fontMap[fontId] = std::unique_ptr<UnifiedFontFamily>(new UnifiedFontFamily(font));
|
||||
}
|
||||
|
||||
void GfxRenderer::insertSdFont(const int fontId, SdFontFamily* font) {
|
||||
fontMap[fontId] = std::unique_ptr<UnifiedFontFamily>(new UnifiedFontFamily(font));
|
||||
}
|
||||
|
||||
bool GfxRenderer::removeFont(const int fontId) {
|
||||
auto it = fontMap.find(fontId);
|
||||
if (it == fontMap.end()) {
|
||||
return false;
|
||||
}
|
||||
fontMap.erase(it);
|
||||
Serial.printf("[%lu] [GFX] Removed font %d\n", millis(), fontId);
|
||||
return true;
|
||||
}
|
||||
|
||||
int GfxRenderer::getEffectiveFontId(const int fontId) const {
|
||||
if (fontMap.find(fontId) != fontMap.end()) {
|
||||
return fontId;
|
||||
}
|
||||
// Custom font IDs are negative (hash-based), map to CUSTOM_FONT_ID slot (-999999)
|
||||
constexpr int CUSTOM_FONT_ID = -999999;
|
||||
if (fontId < 0 && fontMap.find(CUSTOM_FONT_ID) != fontMap.end()) {
|
||||
return CUSTOM_FONT_ID;
|
||||
}
|
||||
// Font not found, return fallback
|
||||
if (fallbackFontId != 0 && fontMap.find(fallbackFontId) != fontMap.end()) {
|
||||
return fallbackFontId;
|
||||
}
|
||||
// No fallback set or fallback not found, return original (will fail gracefully)
|
||||
return fontId;
|
||||
}
|
||||
|
||||
void GfxRenderer::rotateCoordinates(const int x, const int y, int* rotatedX, int* rotatedY) const {
|
||||
switch (orientation) {
|
||||
@ -66,26 +99,46 @@ void GfxRenderer::drawPixel(const int x, const int y, const bool state) const {
|
||||
}
|
||||
}
|
||||
|
||||
int GfxRenderer::getTextWidth(const int fontId, const char* text, const EpdFontFamily::Style style) const {
|
||||
if (fontMap.count(fontId) == 0) {
|
||||
Serial.printf("[%lu] [GFX] Font %d not found\n", millis(), fontId);
|
||||
int GfxRenderer::getTextWidth(const int fontId, const char* text, const EpdFontStyle style) const {
|
||||
const int effectiveId = getEffectiveFontId(fontId);
|
||||
auto it = fontMap.find(effectiveId);
|
||||
if (it == fontMap.end()) {
|
||||
Serial.printf("[%lu] [GFX] Font %d not found (no fallback)\n", millis(), fontId);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int w = 0, h = 0;
|
||||
fontMap.at(fontId).getTextDimensions(text, &w, &h, style);
|
||||
it->second->getTextDimensions(text, &w, &h, style);
|
||||
|
||||
// Add 1px per character for synthetic bold (bold requested but no bold font)
|
||||
const bool syntheticBold = (style == BOLD || style == BOLD_ITALIC) && !it->second->hasBold();
|
||||
if (syntheticBold && text != nullptr) {
|
||||
// Count UTF-8 characters
|
||||
const uint8_t* ptr = reinterpret_cast<const uint8_t*>(text);
|
||||
int charCount = 0;
|
||||
while (*ptr) {
|
||||
// Count UTF-8 start bytes (not continuation bytes 10xxxxxx)
|
||||
if ((*ptr & 0xC0) != 0x80) {
|
||||
charCount++;
|
||||
}
|
||||
ptr++;
|
||||
}
|
||||
w += charCount;
|
||||
}
|
||||
|
||||
return w;
|
||||
}
|
||||
|
||||
void GfxRenderer::drawCenteredText(const int fontId, const int y, const char* text, const bool black,
|
||||
const EpdFontFamily::Style style) const {
|
||||
const EpdFontStyle style) const {
|
||||
const int x = (getScreenWidth() - getTextWidth(fontId, text, style)) / 2;
|
||||
drawText(fontId, x, y, text, black, style);
|
||||
}
|
||||
|
||||
void GfxRenderer::drawText(const int fontId, const int x, const int y, const char* text, const bool black,
|
||||
const EpdFontFamily::Style style) const {
|
||||
const int yPos = y + getFontAscenderSize(fontId);
|
||||
const EpdFontStyle style) const {
|
||||
const int effectiveId = getEffectiveFontId(fontId);
|
||||
const int yPos = y + getFontAscenderSize(effectiveId);
|
||||
int xpos = x;
|
||||
|
||||
// cannot draw a NULL / empty string
|
||||
@ -93,11 +146,12 @@ void GfxRenderer::drawText(const int fontId, const int x, const int y, const cha
|
||||
return;
|
||||
}
|
||||
|
||||
if (fontMap.count(fontId) == 0) {
|
||||
Serial.printf("[%lu] [GFX] Font %d not found\n", millis(), fontId);
|
||||
auto it = fontMap.find(effectiveId);
|
||||
if (it == fontMap.end()) {
|
||||
Serial.printf("[%lu] [GFX] Font %d not found (no fallback)\n", millis(), fontId);
|
||||
return;
|
||||
}
|
||||
const auto font = fontMap.at(fontId);
|
||||
const auto& font = *(it->second);
|
||||
|
||||
// no printable characters
|
||||
if (!font.hasPrintableChars(text, style)) {
|
||||
@ -401,7 +455,7 @@ void GfxRenderer::displayBuffer(const EInkDisplay::RefreshMode refreshMode) cons
|
||||
}
|
||||
|
||||
std::string GfxRenderer::truncatedText(const int fontId, const char* text, const int maxWidth,
|
||||
const EpdFontFamily::Style style) const {
|
||||
const EpdFontStyle style) const {
|
||||
std::string item = text;
|
||||
int itemWidth = getTextWidth(fontId, item.c_str(), style);
|
||||
while (itemWidth > maxWidth && item.length() > 8) {
|
||||
@ -441,37 +495,41 @@ int GfxRenderer::getScreenHeight() const {
|
||||
}
|
||||
|
||||
int GfxRenderer::getSpaceWidth(const int fontId) const {
|
||||
if (fontMap.count(fontId) == 0) {
|
||||
Serial.printf("[%lu] [GFX] Font %d not found\n", millis(), fontId);
|
||||
const int effectiveId = getEffectiveFontId(fontId);
|
||||
auto it = fontMap.find(effectiveId);
|
||||
if (it == fontMap.end()) {
|
||||
Serial.printf("[%lu] [GFX] Font %d not found (no fallback)\n", millis(), fontId);
|
||||
return 0;
|
||||
}
|
||||
|
||||
return fontMap.at(fontId).getGlyph(' ', EpdFontFamily::REGULAR)->advanceX;
|
||||
const EpdGlyph* glyph = it->second->getGlyph(' ', REGULAR);
|
||||
return glyph ? glyph->advanceX : 0;
|
||||
}
|
||||
|
||||
int GfxRenderer::getFontAscenderSize(const int fontId) const {
|
||||
if (fontMap.count(fontId) == 0) {
|
||||
Serial.printf("[%lu] [GFX] Font %d not found\n", millis(), fontId);
|
||||
const int effectiveId = getEffectiveFontId(fontId);
|
||||
auto it = fontMap.find(effectiveId);
|
||||
if (it == fontMap.end()) {
|
||||
Serial.printf("[%lu] [GFX] Font %d not found (no fallback)\n", millis(), fontId);
|
||||
return 0;
|
||||
}
|
||||
|
||||
return fontMap.at(fontId).getData(EpdFontFamily::REGULAR)->ascender;
|
||||
return it->second->getAscender(REGULAR);
|
||||
}
|
||||
|
||||
int GfxRenderer::getLineHeight(const int fontId) const {
|
||||
if (fontMap.count(fontId) == 0) {
|
||||
Serial.printf("[%lu] [GFX] Font %d not found\n", millis(), fontId);
|
||||
const int effectiveId = getEffectiveFontId(fontId);
|
||||
auto it = fontMap.find(effectiveId);
|
||||
if (it == fontMap.end()) {
|
||||
Serial.printf("[%lu] [GFX] Font %d not found (no fallback)\n", millis(), fontId);
|
||||
return 0;
|
||||
}
|
||||
|
||||
return fontMap.at(fontId).getData(EpdFontFamily::REGULAR)->advanceY;
|
||||
return it->second->getAdvanceY(REGULAR);
|
||||
}
|
||||
|
||||
void GfxRenderer::drawButtonHints(const int fontId, const char* btn1, const char* btn2, const char* btn3,
|
||||
const char* btn4) {
|
||||
const Orientation orig_orientation = getOrientation();
|
||||
setOrientation(Orientation::Portrait);
|
||||
|
||||
const char* btn4) const {
|
||||
const int pageHeight = getScreenHeight();
|
||||
constexpr int buttonWidth = 106;
|
||||
constexpr int buttonHeight = 40;
|
||||
@ -484,15 +542,12 @@ void GfxRenderer::drawButtonHints(const int fontId, const char* btn1, const char
|
||||
// Only draw if the label is non-empty
|
||||
if (labels[i] != nullptr && labels[i][0] != '\0') {
|
||||
const int x = buttonPositions[i];
|
||||
fillRect(x, pageHeight - buttonY, buttonWidth, buttonHeight, false);
|
||||
drawRect(x, pageHeight - buttonY, buttonWidth, buttonHeight);
|
||||
const int textWidth = getTextWidth(fontId, labels[i]);
|
||||
const int textX = x + (buttonWidth - 1 - textWidth) / 2;
|
||||
drawText(fontId, textX, pageHeight - buttonY + textYOffset, labels[i]);
|
||||
}
|
||||
}
|
||||
|
||||
setOrientation(orig_orientation);
|
||||
}
|
||||
|
||||
void GfxRenderer::drawSideButtonHints(const int fontId, const char* topBtn, const char* bottomBtn) const {
|
||||
@ -551,7 +606,7 @@ int GfxRenderer::getTextHeight(const int fontId) const {
|
||||
Serial.printf("[%lu] [GFX] Font %d not found\n", millis(), fontId);
|
||||
return 0;
|
||||
}
|
||||
return fontMap.at(fontId).getData(EpdFontFamily::REGULAR)->ascender;
|
||||
return fontMap.at(fontId)->getAscender(EpdFontFamily::REGULAR);
|
||||
}
|
||||
|
||||
void GfxRenderer::drawTextRotated90CW(const int fontId, const int x, const int y, const char* text, const bool black,
|
||||
@ -565,10 +620,10 @@ void GfxRenderer::drawTextRotated90CW(const int fontId, const int x, const int y
|
||||
Serial.printf("[%lu] [GFX] Font %d not found\n", millis(), fontId);
|
||||
return;
|
||||
}
|
||||
const auto font = fontMap.at(fontId);
|
||||
const auto& font = fontMap.at(fontId);
|
||||
|
||||
// No printable characters
|
||||
if (!font.hasPrintableChars(text, style)) {
|
||||
if (!font->hasPrintableChars(text, style)) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -580,22 +635,22 @@ void GfxRenderer::drawTextRotated90CW(const int fontId, const int x, const int y
|
||||
|
||||
uint32_t cp;
|
||||
while ((cp = utf8NextCodepoint(reinterpret_cast<const uint8_t**>(&text)))) {
|
||||
const EpdGlyph* glyph = font.getGlyph(cp, style);
|
||||
const EpdGlyph* glyph = font->getGlyph(cp, style);
|
||||
if (!glyph) {
|
||||
glyph = font.getGlyph('?', style);
|
||||
glyph = font->getGlyph('?', style);
|
||||
}
|
||||
if (!glyph) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const int is2Bit = font.getData(style)->is2Bit;
|
||||
const uint32_t offset = glyph->dataOffset;
|
||||
const bool is2BitFont = font->is2Bit(style);
|
||||
const uint8_t width = glyph->width;
|
||||
const uint8_t height = glyph->height;
|
||||
const int left = glyph->left;
|
||||
const int top = glyph->top;
|
||||
const int ascender = font->getAscender(style);
|
||||
|
||||
const uint8_t* bitmap = &font.getData(style)->bitmap[offset];
|
||||
const uint8_t* bitmap = font->getGlyphBitmap(cp, style);
|
||||
|
||||
if (bitmap != nullptr) {
|
||||
for (int glyphY = 0; glyphY < height; glyphY++) {
|
||||
@ -605,10 +660,10 @@ void GfxRenderer::drawTextRotated90CW(const int fontId, const int x, const int y
|
||||
// 90° clockwise rotation transformation:
|
||||
// screenX = x + (ascender - top + glyphY)
|
||||
// screenY = yPos - (left + glyphX)
|
||||
const int screenX = x + (font.getData(style)->ascender - top + glyphY);
|
||||
const int screenX = x + (ascender - top + glyphY);
|
||||
const int screenY = yPos - left - glyphX;
|
||||
|
||||
if (is2Bit) {
|
||||
if (is2BitFont) {
|
||||
const uint8_t byte = bitmap[pixelPosition / 4];
|
||||
const uint8_t bit_index = (3 - pixelPosition % 4) * 2;
|
||||
const uint8_t bmpVal = 3 - (byte >> bit_index) & 0x3;
|
||||
@ -745,6 +800,38 @@ void GfxRenderer::restoreBwBuffer() {
|
||||
Serial.printf("[%lu] [GFX] Restored and freed BW buffer chunks\n", millis());
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy stored BW buffer to framebuffer without freeing the stored chunks.
|
||||
* Use this when you want to restore the buffer but keep it for later reuse.
|
||||
* Returns true if buffer was copied successfully.
|
||||
*/
|
||||
bool GfxRenderer::copyStoredBwBuffer() {
|
||||
// Check if all chunks are allocated
|
||||
for (const auto& bwBufferChunk : bwBufferChunks) {
|
||||
if (!bwBufferChunk) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
uint8_t* frameBuffer = einkDisplay.getFrameBuffer();
|
||||
if (!frameBuffer) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < BW_BUFFER_NUM_CHUNKS; i++) {
|
||||
const size_t offset = i * BW_BUFFER_CHUNK_SIZE;
|
||||
memcpy(frameBuffer + offset, bwBufferChunks[i], BW_BUFFER_CHUNK_SIZE);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Free the stored BW buffer chunks manually.
|
||||
* Use this when you no longer need the stored buffer.
|
||||
*/
|
||||
void GfxRenderer::freeStoredBwBuffer() { freeBwBufferChunks(); }
|
||||
|
||||
/**
|
||||
* Cleanup grayscale buffers using the current frame buffer.
|
||||
* Use this when BW buffer was re-rendered instead of stored/restored.
|
||||
@ -756,11 +843,11 @@ void GfxRenderer::cleanupGrayscaleWithFrameBuffer() const {
|
||||
}
|
||||
}
|
||||
|
||||
void GfxRenderer::renderChar(const EpdFontFamily& fontFamily, const uint32_t cp, int* x, const int* y,
|
||||
const bool pixelState, const EpdFontFamily::Style style) const {
|
||||
void GfxRenderer::renderChar(const UnifiedFontFamily& fontFamily, const uint32_t cp, int* x, const int* y,
|
||||
const bool pixelState, const EpdFontStyle style) const {
|
||||
const EpdGlyph* glyph = fontFamily.getGlyph(cp, style);
|
||||
if (!glyph) {
|
||||
// TODO: Replace with fallback glyph property?
|
||||
// Try fallback glyph
|
||||
glyph = fontFamily.getGlyph('?', style);
|
||||
}
|
||||
|
||||
@ -770,14 +857,20 @@ void GfxRenderer::renderChar(const EpdFontFamily& fontFamily, const uint32_t cp,
|
||||
return;
|
||||
}
|
||||
|
||||
const int is2Bit = fontFamily.getData(style)->is2Bit;
|
||||
const uint32_t offset = glyph->dataOffset;
|
||||
const bool is2Bit = fontFamily.is2Bit(style);
|
||||
const uint8_t width = glyph->width;
|
||||
const uint8_t height = glyph->height;
|
||||
const int left = glyph->left;
|
||||
|
||||
const uint8_t* bitmap = nullptr;
|
||||
bitmap = &fontFamily.getData(style)->bitmap[offset];
|
||||
// Check if we need synthetic bold (bold requested but no bold font available)
|
||||
const bool syntheticBold = (style == BOLD || style == BOLD_ITALIC) && !fontFamily.hasBold();
|
||||
|
||||
// Get bitmap data (works for both flash and SD fonts)
|
||||
const uint8_t* bitmap = fontFamily.getGlyphBitmap(cp, style);
|
||||
if (!bitmap) {
|
||||
// Try fallback
|
||||
bitmap = fontFamily.getGlyphBitmap('?', style);
|
||||
}
|
||||
|
||||
if (bitmap != nullptr) {
|
||||
for (int glyphY = 0; glyphY < height; glyphY++) {
|
||||
@ -797,13 +890,22 @@ void GfxRenderer::renderChar(const EpdFontFamily& fontFamily, const uint32_t cp,
|
||||
if (renderMode == BW && bmpVal < 3) {
|
||||
// Black (also paints over the grays in BW mode)
|
||||
drawPixel(screenX, screenY, pixelState);
|
||||
if (syntheticBold) {
|
||||
drawPixel(screenX + 1, screenY, pixelState); // Draw again 1px to the right
|
||||
}
|
||||
} else if (renderMode == GRAYSCALE_MSB && (bmpVal == 1 || bmpVal == 2)) {
|
||||
// Light gray (also mark the MSB if it's going to be a dark gray too)
|
||||
// We have to flag pixels in reverse for the gray buffers, as 0 leave alone, 1 update
|
||||
drawPixel(screenX, screenY, false);
|
||||
if (syntheticBold) {
|
||||
drawPixel(screenX + 1, screenY, false);
|
||||
}
|
||||
} else if (renderMode == GRAYSCALE_LSB && bmpVal == 1) {
|
||||
// Dark gray
|
||||
drawPixel(screenX, screenY, false);
|
||||
if (syntheticBold) {
|
||||
drawPixel(screenX + 1, screenY, false);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const uint8_t byte = bitmap[pixelPosition / 8];
|
||||
@ -811,13 +913,17 @@ void GfxRenderer::renderChar(const EpdFontFamily& fontFamily, const uint32_t cp,
|
||||
|
||||
if ((byte >> bit_index) & 1) {
|
||||
drawPixel(screenX, screenY, pixelState);
|
||||
if (syntheticBold) {
|
||||
drawPixel(screenX + 1, screenY, pixelState);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
*x += glyph->advanceX;
|
||||
// Advance by glyph width, adding 1px for synthetic bold
|
||||
*x += glyph->advanceX + (syntheticBold ? 1 : 0);
|
||||
}
|
||||
|
||||
void GfxRenderer::getOrientedViewableTRBL(int* outTop, int* outRight, int* outBottom, int* outLeft) const {
|
||||
|
||||
@ -1,9 +1,10 @@
|
||||
#pragma once
|
||||
|
||||
#include <EInkDisplay.h>
|
||||
#include <EpdFontFamily.h>
|
||||
#include <SdFontFamily.h>
|
||||
|
||||
#include <map>
|
||||
#include <memory>
|
||||
|
||||
#include "Bitmap.h"
|
||||
|
||||
@ -29,9 +30,10 @@ class GfxRenderer {
|
||||
RenderMode renderMode;
|
||||
Orientation orientation;
|
||||
uint8_t* bwBufferChunks[BW_BUFFER_NUM_CHUNKS] = {nullptr};
|
||||
std::map<int, EpdFontFamily> fontMap;
|
||||
void renderChar(const EpdFontFamily& fontFamily, uint32_t cp, int* x, const int* y, bool pixelState,
|
||||
EpdFontFamily::Style style) const;
|
||||
std::map<int, std::unique_ptr<UnifiedFontFamily>> fontMap;
|
||||
int fallbackFontId = 0; // Default fallback font ID (set after fonts are loaded)
|
||||
void renderChar(const UnifiedFontFamily& fontFamily, uint32_t cp, int* x, const int* y, bool pixelState,
|
||||
EpdFontStyle style) const;
|
||||
void freeBwBufferChunks();
|
||||
void rotateCoordinates(int x, int y, int* rotatedX, int* rotatedY) const;
|
||||
|
||||
@ -44,8 +46,18 @@ class GfxRenderer {
|
||||
static constexpr int VIEWABLE_MARGIN_BOTTOM = 3;
|
||||
static constexpr int VIEWABLE_MARGIN_LEFT = 3;
|
||||
|
||||
// Setup
|
||||
void insertFont(int fontId, EpdFontFamily font);
|
||||
// Setup - Flash fonts (EpdFontFamily) - stores pointer to global font
|
||||
void insertFont(int fontId, const EpdFontFamily* font);
|
||||
// Setup - SD card fonts (SdFontFamily) - takes ownership
|
||||
void insertSdFont(int fontId, SdFontFamily* font);
|
||||
// Set fallback font ID (used when requested font is not found)
|
||||
void setFallbackFont(int fontId) { fallbackFontId = fontId; }
|
||||
// Check if a font is registered
|
||||
bool hasFont(int fontId) const { return fontMap.find(fontId) != fontMap.end(); }
|
||||
// Remove a font from the registry (frees memory for SD fonts)
|
||||
bool removeFont(int fontId);
|
||||
// Get effective font ID (returns fallback if requested font not found)
|
||||
int getEffectiveFontId(int fontId) const;
|
||||
|
||||
// Orientation control (affects logical width/height and coordinate transforms)
|
||||
void setOrientation(const Orientation o) { orientation = o; }
|
||||
@ -72,19 +84,16 @@ class GfxRenderer {
|
||||
void fillPolygon(const int* xPoints, const int* yPoints, int numPoints, bool state = true) const;
|
||||
|
||||
// Text
|
||||
int getTextWidth(int fontId, const char* text, EpdFontFamily::Style style = EpdFontFamily::REGULAR) const;
|
||||
void drawCenteredText(int fontId, int y, const char* text, bool black = true,
|
||||
EpdFontFamily::Style style = EpdFontFamily::REGULAR) const;
|
||||
void drawText(int fontId, int x, int y, const char* text, bool black = true,
|
||||
EpdFontFamily::Style style = EpdFontFamily::REGULAR) const;
|
||||
int getTextWidth(int fontId, const char* text, EpdFontStyle style = REGULAR) const;
|
||||
void drawCenteredText(int fontId, int y, const char* text, bool black = true, EpdFontStyle style = REGULAR) const;
|
||||
void drawText(int fontId, int x, int y, const char* text, bool black = true, EpdFontStyle style = REGULAR) const;
|
||||
int getSpaceWidth(int fontId) const;
|
||||
int getFontAscenderSize(int fontId) const;
|
||||
int getLineHeight(int fontId) const;
|
||||
std::string truncatedText(int fontId, const char* text, int maxWidth,
|
||||
EpdFontFamily::Style style = EpdFontFamily::REGULAR) const;
|
||||
std::string truncatedText(int fontId, const char* text, int maxWidth, EpdFontStyle style = REGULAR) const;
|
||||
|
||||
// UI Components
|
||||
void drawButtonHints(int fontId, const char* btn1, const char* btn2, const char* btn3, const char* btn4);
|
||||
void drawButtonHints(int fontId, const char* btn1, const char* btn2, const char* btn3, const char* btn4) const;
|
||||
void drawSideButtonHints(int fontId, const char* topBtn, const char* bottomBtn) const;
|
||||
|
||||
private:
|
||||
@ -99,8 +108,10 @@ class GfxRenderer {
|
||||
void copyGrayscaleLsbBuffers() const;
|
||||
void copyGrayscaleMsbBuffers() const;
|
||||
void displayGrayBuffer() const;
|
||||
bool storeBwBuffer(); // Returns true if buffer was stored successfully
|
||||
void restoreBwBuffer(); // Restore and free the stored buffer
|
||||
bool storeBwBuffer(); // Returns true if buffer was stored successfully
|
||||
void restoreBwBuffer(); // Restore and free the stored buffer
|
||||
bool copyStoredBwBuffer(); // Copy stored buffer to framebuffer without freeing
|
||||
void freeStoredBwBuffer(); // Free the stored buffer manually
|
||||
void cleanupGrayscaleWithFrameBuffer() const;
|
||||
|
||||
// Low level functions
|
||||
|
||||
@ -14,7 +14,7 @@ CrossPointSettings CrossPointSettings::instance;
|
||||
namespace {
|
||||
constexpr uint8_t SETTINGS_FILE_VERSION = 1;
|
||||
// Increment this when adding new persisted settings fields
|
||||
constexpr uint8_t SETTINGS_COUNT = 18;
|
||||
constexpr uint8_t SETTINGS_COUNT = 19;
|
||||
constexpr char SETTINGS_FILE[] = "/.crosspoint/settings.bin";
|
||||
} // namespace
|
||||
|
||||
@ -48,6 +48,7 @@ bool CrossPointSettings::saveToFile() const {
|
||||
serialization::writePod(outputFile, textAntiAliasing);
|
||||
serialization::writePod(outputFile, hideBatteryPercentage);
|
||||
serialization::writePod(outputFile, longPressChapterSkip);
|
||||
serialization::writeString(outputFile, std::string(customFontPath));
|
||||
outputFile.close();
|
||||
|
||||
Serial.printf("[%lu] [CPS] Settings saved to file\n", millis());
|
||||
@ -116,6 +117,13 @@ bool CrossPointSettings::loadFromFile() {
|
||||
if (++settingsRead >= fileSettingsCount) break;
|
||||
serialization::readPod(inputFile, longPressChapterSkip);
|
||||
if (++settingsRead >= fileSettingsCount) break;
|
||||
{
|
||||
std::string fontPathStr;
|
||||
serialization::readString(inputFile, fontPathStr);
|
||||
strncpy(customFontPath, fontPathStr.c_str(), sizeof(customFontPath) - 1);
|
||||
customFontPath[sizeof(customFontPath) - 1] = '\0';
|
||||
}
|
||||
if (++settingsRead >= fileSettingsCount) break;
|
||||
} while (false);
|
||||
|
||||
inputFile.close();
|
||||
@ -192,6 +200,19 @@ int CrossPointSettings::getRefreshFrequency() const {
|
||||
}
|
||||
|
||||
int CrossPointSettings::getReaderFontId() const {
|
||||
// Return custom font ID if a custom font is configured
|
||||
if (hasCustomFont()) {
|
||||
// Generate unique negative ID based on font path hash
|
||||
// This ensures different custom fonts have different IDs for cache invalidation
|
||||
uint32_t hash = 5381;
|
||||
for (const char* p = customFontPath; *p; p++) {
|
||||
hash = ((hash << 5) + hash) + static_cast<uint8_t>(*p); // djb2 hash
|
||||
}
|
||||
// Return negative value to avoid collision with built-in font IDs
|
||||
return -static_cast<int>((hash & 0x7FFFFFFF) | 1);
|
||||
}
|
||||
|
||||
// Use built-in font based on fontFamily/fontSize
|
||||
switch (fontFamily) {
|
||||
case BOOKERLY:
|
||||
default:
|
||||
@ -232,3 +253,21 @@ int CrossPointSettings::getReaderFontId() const {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const char* CrossPointSettings::getCustomFontName() const {
|
||||
if (!hasCustomFont()) {
|
||||
return nullptr;
|
||||
}
|
||||
// Extract filename from path (e.g., "/.crosspoint/fonts/MyFont.epdfont" -> "MyFont")
|
||||
const char* lastSlash = strrchr(customFontPath, '/');
|
||||
const char* filename = lastSlash ? lastSlash + 1 : customFontPath;
|
||||
// Remove extension for display
|
||||
static char nameBuffer[32];
|
||||
strncpy(nameBuffer, filename, sizeof(nameBuffer) - 1);
|
||||
nameBuffer[sizeof(nameBuffer) - 1] = '\0';
|
||||
char* dot = strrchr(nameBuffer, '.');
|
||||
if (dot) {
|
||||
*dot = '\0';
|
||||
}
|
||||
return nameBuffer;
|
||||
}
|
||||
|
||||
@ -92,9 +92,16 @@ class CrossPointSettings {
|
||||
uint8_t hideBatteryPercentage = HIDE_NEVER;
|
||||
// Long-press chapter skip on side buttons
|
||||
uint8_t longPressChapterSkip = 1;
|
||||
// Custom reader font path (empty means use built-in font based on fontFamily/fontSize)
|
||||
char customFontPath[64] = "";
|
||||
|
||||
~CrossPointSettings() = default;
|
||||
|
||||
// Check if custom font is set
|
||||
bool hasCustomFont() const { return customFontPath[0] != '\0'; }
|
||||
// Get custom font name (extracted from path)
|
||||
const char* getCustomFontName() const;
|
||||
|
||||
// Get singleton instance
|
||||
static CrossPointSettings& getInstance() { return instance; }
|
||||
|
||||
|
||||
11
src/FontManager.h
Normal file
11
src/FontManager.h
Normal file
@ -0,0 +1,11 @@
|
||||
#pragma once
|
||||
|
||||
class GfxRenderer;
|
||||
|
||||
// Reload custom reader font - removes old font and loads new one
|
||||
// Call this when font settings change to apply immediately without reboot
|
||||
// Returns true if custom font was loaded successfully
|
||||
bool reloadCustomReaderFont();
|
||||
|
||||
// Get reference to global renderer (for font operations from other modules)
|
||||
GfxRenderer& getGlobalRenderer();
|
||||
335
src/activities/settings/FontSelectionActivity.cpp
Normal file
335
src/activities/settings/FontSelectionActivity.cpp
Normal file
@ -0,0 +1,335 @@
|
||||
#include "FontSelectionActivity.h"
|
||||
|
||||
#include <GfxRenderer.h>
|
||||
#include <HardwareSerial.h>
|
||||
#include <SDCardManager.h>
|
||||
|
||||
#include <cstring>
|
||||
|
||||
#include "CrossPointSettings.h"
|
||||
#include "FontManager.h"
|
||||
#include "MappedInputManager.h"
|
||||
#include "fontIds.h"
|
||||
|
||||
namespace {
|
||||
constexpr const char* DEFAULT_FONT_NAME = "Default";
|
||||
constexpr const char* CACHE_DIR = "/.crosspoint/cache";
|
||||
|
||||
// Recursively delete a directory and its contents
|
||||
void deleteDirectory(const char* path) {
|
||||
FsFile dir = SdMan.open(path);
|
||||
if (!dir || !dir.isDir()) {
|
||||
if (dir) dir.close();
|
||||
return;
|
||||
}
|
||||
|
||||
FsFile entry;
|
||||
while (entry.openNext(&dir, O_RDONLY)) {
|
||||
char entryName[64];
|
||||
entry.getName(entryName, sizeof(entryName));
|
||||
entry.close();
|
||||
|
||||
std::string fullPath = std::string(path) + "/" + entryName;
|
||||
FsFile check = SdMan.open(fullPath.c_str());
|
||||
if (check) {
|
||||
bool isDir = check.isDir();
|
||||
check.close();
|
||||
if (isDir) {
|
||||
deleteDirectory(fullPath.c_str());
|
||||
} else {
|
||||
SdMan.remove(fullPath.c_str());
|
||||
}
|
||||
}
|
||||
}
|
||||
dir.close();
|
||||
SdMan.rmdir(path);
|
||||
}
|
||||
|
||||
// Invalidate rendering caches for EPUB and TXT readers
|
||||
// Keeps progress.bin (reading position) but removes layout caches
|
||||
void invalidateReaderCaches() {
|
||||
Serial.printf("[%lu] [FNT] Invalidating reader rendering caches...\n", millis());
|
||||
|
||||
FsFile cacheDir = SdMan.open(CACHE_DIR);
|
||||
if (!cacheDir || !cacheDir.isDir()) {
|
||||
if (cacheDir) cacheDir.close();
|
||||
Serial.printf("[%lu] [FNT] No cache directory found\n", millis());
|
||||
return;
|
||||
}
|
||||
|
||||
int deletedCount = 0;
|
||||
FsFile bookCache;
|
||||
while (bookCache.openNext(&cacheDir, O_RDONLY)) {
|
||||
char bookCacheName[64];
|
||||
bookCache.getName(bookCacheName, sizeof(bookCacheName));
|
||||
bookCache.close();
|
||||
|
||||
std::string bookCachePath = std::string(CACHE_DIR) + "/" + bookCacheName;
|
||||
|
||||
// For EPUB: delete sections/ folder (keeps progress.bin)
|
||||
std::string sectionsPath = bookCachePath + "/sections";
|
||||
FsFile sectionsDir = SdMan.open(sectionsPath.c_str());
|
||||
if (sectionsDir && sectionsDir.isDir()) {
|
||||
sectionsDir.close();
|
||||
deleteDirectory(sectionsPath.c_str());
|
||||
Serial.printf("[%lu] [FNT] Deleted EPUB sections cache: %s\n", millis(), sectionsPath.c_str());
|
||||
deletedCount++;
|
||||
} else {
|
||||
if (sectionsDir) sectionsDir.close();
|
||||
}
|
||||
|
||||
// For TXT: delete index.bin (keeps progress.bin)
|
||||
std::string indexPath = bookCachePath + "/index.bin";
|
||||
if (SdMan.exists(indexPath.c_str())) {
|
||||
SdMan.remove(indexPath.c_str());
|
||||
Serial.printf("[%lu] [FNT] Deleted TXT index cache: %s\n", millis(), indexPath.c_str());
|
||||
deletedCount++;
|
||||
}
|
||||
}
|
||||
cacheDir.close();
|
||||
|
||||
Serial.printf("[%lu] [FNT] Invalidated %d cache entries\n", millis(), deletedCount);
|
||||
}
|
||||
} // namespace
|
||||
|
||||
void FontSelectionActivity::taskTrampoline(void* param) {
|
||||
auto* self = static_cast<FontSelectionActivity*>(param);
|
||||
self->displayTaskLoop();
|
||||
}
|
||||
|
||||
void FontSelectionActivity::loadFontList() {
|
||||
fontFiles.clear();
|
||||
fontNames.clear();
|
||||
|
||||
// First entry is always the default font (empty path means default)
|
||||
fontFiles.emplace_back("");
|
||||
fontNames.emplace_back(DEFAULT_FONT_NAME);
|
||||
|
||||
// Ensure fonts directory exists
|
||||
SdMan.mkdir("/.crosspoint");
|
||||
SdMan.mkdir(FONTS_DIR);
|
||||
|
||||
// Try to open the fonts folder
|
||||
FsFile dir = SdMan.open(FONTS_DIR);
|
||||
if (!dir) {
|
||||
Serial.printf("[%lu] [FNT] Font folder %s not found\n", millis(), FONTS_DIR);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!dir.isDir()) {
|
||||
Serial.printf("[%lu] [FNT] %s is not a directory\n", millis(), FONTS_DIR);
|
||||
dir.close();
|
||||
return;
|
||||
}
|
||||
|
||||
// List all .epdfont files
|
||||
FsFile file;
|
||||
while (file.openNext(&dir, O_RDONLY)) {
|
||||
if (!file.isDir()) {
|
||||
char filename[64];
|
||||
file.getName(filename, sizeof(filename));
|
||||
|
||||
// Check if file has .epdfont extension and skip macOS hidden files (._*)
|
||||
const size_t len = strlen(filename);
|
||||
if (len > 8 && strcasecmp(filename + len - 8, ".epdfont") == 0 && strncmp(filename, "._", 2) != 0) {
|
||||
// Build full path
|
||||
std::string fullPath = std::string(FONTS_DIR) + "/" + filename;
|
||||
fontFiles.push_back(fullPath);
|
||||
|
||||
// Extract name without extension for display
|
||||
std::string displayName(filename, len - 8);
|
||||
fontNames.push_back(displayName);
|
||||
|
||||
Serial.printf("[%lu] [FNT] Found font: %s\n", millis(), fullPath.c_str());
|
||||
}
|
||||
}
|
||||
file.close();
|
||||
}
|
||||
dir.close();
|
||||
|
||||
Serial.printf("[%lu] [FNT] Total fonts found: %zu (including default)\n", millis(), fontFiles.size());
|
||||
|
||||
// Find currently selected font index
|
||||
selectedIndex = 0; // Default
|
||||
if (SETTINGS.hasCustomFont()) {
|
||||
for (size_t i = 1; i < fontFiles.size(); i++) {
|
||||
if (fontFiles[i] == SETTINGS.customFontPath) {
|
||||
selectedIndex = static_cast<int>(i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void FontSelectionActivity::onEnter() {
|
||||
ActivityWithSubactivity::onEnter();
|
||||
|
||||
renderingMutex = xSemaphoreCreateMutex();
|
||||
|
||||
// Load font list from SD card
|
||||
loadFontList();
|
||||
|
||||
updateRequired = true;
|
||||
|
||||
xTaskCreate(&FontSelectionActivity::taskTrampoline, "FontSelectionTask",
|
||||
4096, // Stack size
|
||||
this, // Parameters
|
||||
1, // Priority
|
||||
&displayTaskHandle // Task handle
|
||||
);
|
||||
}
|
||||
|
||||
void FontSelectionActivity::onExit() {
|
||||
ActivityWithSubactivity::onExit();
|
||||
|
||||
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
||||
if (displayTaskHandle) {
|
||||
vTaskDelete(displayTaskHandle);
|
||||
displayTaskHandle = nullptr;
|
||||
}
|
||||
vSemaphoreDelete(renderingMutex);
|
||||
renderingMutex = nullptr;
|
||||
}
|
||||
|
||||
void FontSelectionActivity::loop() {
|
||||
if (subActivity) {
|
||||
subActivity->loop();
|
||||
return;
|
||||
}
|
||||
|
||||
if (mappedInput.wasPressed(MappedInputManager::Button::Back)) {
|
||||
onBack();
|
||||
return;
|
||||
}
|
||||
|
||||
if (mappedInput.wasPressed(MappedInputManager::Button::Confirm)) {
|
||||
handleSelection();
|
||||
return;
|
||||
}
|
||||
|
||||
const int itemCount = static_cast<int>(fontNames.size());
|
||||
if (mappedInput.wasPressed(MappedInputManager::Button::Up) ||
|
||||
mappedInput.wasPressed(MappedInputManager::Button::Left)) {
|
||||
selectedIndex = (selectedIndex + itemCount - 1) % itemCount;
|
||||
updateRequired = true;
|
||||
} else if (mappedInput.wasPressed(MappedInputManager::Button::Down) ||
|
||||
mappedInput.wasPressed(MappedInputManager::Button::Right)) {
|
||||
selectedIndex = (selectedIndex + 1) % itemCount;
|
||||
updateRequired = true;
|
||||
}
|
||||
}
|
||||
|
||||
void FontSelectionActivity::handleSelection() {
|
||||
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
||||
|
||||
// Show loading screen
|
||||
renderer.clearScreen();
|
||||
renderer.drawCenteredText(UI_10_FONT_ID, renderer.getScreenHeight() / 2 - 10, "Applying font...");
|
||||
renderer.displayBuffer();
|
||||
|
||||
// Update custom font path in settings
|
||||
if (selectedIndex == 0) {
|
||||
// Default font selected - clear custom font path
|
||||
SETTINGS.customFontPath[0] = '\0';
|
||||
} else {
|
||||
// Custom font selected
|
||||
strncpy(SETTINGS.customFontPath, fontFiles[selectedIndex].c_str(), sizeof(SETTINGS.customFontPath) - 1);
|
||||
SETTINGS.customFontPath[sizeof(SETTINGS.customFontPath) - 1] = '\0';
|
||||
}
|
||||
|
||||
SETTINGS.saveToFile();
|
||||
Serial.printf("[%lu] [FNT] Font selected: %s\n", millis(), selectedIndex == 0 ? "default" : SETTINGS.customFontPath);
|
||||
|
||||
// Reload custom font dynamically (no reboot needed)
|
||||
reloadCustomReaderFont();
|
||||
|
||||
// Invalidate EPUB/TXT caches since font changed
|
||||
invalidateReaderCaches();
|
||||
|
||||
xSemaphoreGive(renderingMutex);
|
||||
|
||||
// Return to settings
|
||||
onBack();
|
||||
}
|
||||
|
||||
void FontSelectionActivity::displayTaskLoop() {
|
||||
while (true) {
|
||||
if (updateRequired && !subActivity) {
|
||||
updateRequired = false;
|
||||
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
||||
render();
|
||||
xSemaphoreGive(renderingMutex);
|
||||
}
|
||||
vTaskDelay(10 / portTICK_PERIOD_MS);
|
||||
}
|
||||
}
|
||||
|
||||
void FontSelectionActivity::render() {
|
||||
renderer.clearScreen();
|
||||
|
||||
const auto pageWidth = renderer.getScreenWidth();
|
||||
const auto pageHeight = renderer.getScreenHeight();
|
||||
|
||||
// Draw header
|
||||
renderer.drawCenteredText(UI_12_FONT_ID, 15, "Custom Font", true, EpdFontFamily::BOLD);
|
||||
|
||||
// Calculate visible items (with scrolling if needed)
|
||||
constexpr int lineHeight = 30;
|
||||
constexpr int startY = 60;
|
||||
const int maxVisibleItems = (pageHeight - startY - 50) / lineHeight;
|
||||
const int itemCount = static_cast<int>(fontNames.size());
|
||||
|
||||
// Calculate scroll offset to keep selected item visible
|
||||
int scrollOffset = 0;
|
||||
if (itemCount > maxVisibleItems) {
|
||||
if (selectedIndex >= maxVisibleItems) {
|
||||
scrollOffset = selectedIndex - maxVisibleItems + 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Determine current selection (for checkmark comparison)
|
||||
int currentSelectedIndex = 0; // Default
|
||||
if (SETTINGS.hasCustomFont()) {
|
||||
for (size_t i = 1; i < fontFiles.size(); i++) {
|
||||
if (fontFiles[i] == SETTINGS.customFontPath) {
|
||||
currentSelectedIndex = static_cast<int>(i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Draw font list
|
||||
for (int i = 0; i < maxVisibleItems && (i + scrollOffset) < itemCount; i++) {
|
||||
const int itemIndex = i + scrollOffset;
|
||||
const int itemY = startY + i * lineHeight;
|
||||
const bool isHighlighted = (itemIndex == selectedIndex);
|
||||
const bool isCurrentFont = (itemIndex == currentSelectedIndex);
|
||||
|
||||
// Draw selection highlight
|
||||
if (isHighlighted) {
|
||||
renderer.fillRect(0, itemY - 2, pageWidth - 1, lineHeight);
|
||||
}
|
||||
|
||||
// Draw checkmark for currently active font (using asterisk - available in Pretendard)
|
||||
if (isCurrentFont) {
|
||||
renderer.drawText(UI_10_FONT_ID, 10, itemY, "*", !isHighlighted);
|
||||
}
|
||||
|
||||
// Draw font name
|
||||
renderer.drawText(UI_10_FONT_ID, 35, itemY, fontNames[itemIndex].c_str(), !isHighlighted);
|
||||
}
|
||||
|
||||
// Draw scroll indicators if needed
|
||||
if (scrollOffset > 0) {
|
||||
renderer.drawCenteredText(UI_10_FONT_ID, startY - 15, "...", true);
|
||||
}
|
||||
if (scrollOffset + maxVisibleItems < itemCount) {
|
||||
renderer.drawCenteredText(UI_10_FONT_ID, startY + maxVisibleItems * lineHeight, "...", true);
|
||||
}
|
||||
|
||||
// Draw help text
|
||||
const auto labels = mappedInput.mapLabels("« Back", "Select", "", "");
|
||||
renderer.drawButtonHints(UI_10_FONT_ID, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
|
||||
|
||||
renderer.displayBuffer();
|
||||
}
|
||||
43
src/activities/settings/FontSelectionActivity.h
Normal file
43
src/activities/settings/FontSelectionActivity.h
Normal file
@ -0,0 +1,43 @@
|
||||
#pragma once
|
||||
#include <freertos/FreeRTOS.h>
|
||||
#include <freertos/semphr.h>
|
||||
#include <freertos/task.h>
|
||||
|
||||
#include <functional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "activities/ActivityWithSubactivity.h"
|
||||
|
||||
/**
|
||||
* Activity for selecting a custom font from /.crosspoint/fonts folder.
|
||||
* Lists .bin font files and allows the user to select one.
|
||||
*/
|
||||
class FontSelectionActivity final : public ActivityWithSubactivity {
|
||||
public:
|
||||
explicit FontSelectionActivity(GfxRenderer& renderer, MappedInputManager& mappedInput,
|
||||
const std::function<void()>& onBack)
|
||||
: ActivityWithSubactivity("FontSelection", renderer, mappedInput), onBack(onBack) {}
|
||||
|
||||
void onEnter() override;
|
||||
void onExit() override;
|
||||
void loop() override;
|
||||
|
||||
private:
|
||||
TaskHandle_t displayTaskHandle = nullptr;
|
||||
SemaphoreHandle_t renderingMutex = nullptr;
|
||||
bool updateRequired = false;
|
||||
|
||||
int selectedIndex = 0;
|
||||
std::vector<std::string> fontFiles; // List of font file paths
|
||||
std::vector<std::string> fontNames; // Display names (without path and extension)
|
||||
const std::function<void()> onBack;
|
||||
|
||||
static void taskTrampoline(void* param);
|
||||
[[noreturn]] void displayTaskLoop();
|
||||
void render();
|
||||
void loadFontList();
|
||||
void handleSelection();
|
||||
|
||||
static constexpr const char* FONTS_DIR = "/.crosspoint/fonts";
|
||||
};
|
||||
@ -7,13 +7,14 @@
|
||||
|
||||
#include "CalibreSettingsActivity.h"
|
||||
#include "CrossPointSettings.h"
|
||||
#include "FontSelectionActivity.h"
|
||||
#include "MappedInputManager.h"
|
||||
#include "OtaUpdateActivity.h"
|
||||
#include "fontIds.h"
|
||||
|
||||
// Define the static settings list
|
||||
namespace {
|
||||
constexpr int settingsCount = 20;
|
||||
constexpr int settingsCount = 21;
|
||||
const SettingInfo settingsList[settingsCount] = {
|
||||
// Should match with SLEEP_SCREEN_MODE
|
||||
SettingInfo::Enum("Sleep Screen", &CrossPointSettings::sleepScreen, {"Dark", "Light", "Custom", "Cover", "None"}),
|
||||
@ -41,6 +42,7 @@ const SettingInfo settingsList[settingsCount] = {
|
||||
{"1 min", "5 min", "10 min", "15 min", "30 min"}),
|
||||
SettingInfo::Enum("Refresh Frequency", &CrossPointSettings::refreshFrequency,
|
||||
{"1 page", "5 pages", "10 pages", "15 pages", "30 pages"}),
|
||||
SettingInfo::Action("Custom Font"),
|
||||
SettingInfo::Action("Calibre Settings"),
|
||||
SettingInfo::Action("Check for updates")};
|
||||
} // namespace
|
||||
@ -139,7 +141,15 @@ void SettingsActivity::toggleCurrentSetting() {
|
||||
SETTINGS.*(setting.valuePtr) = currentValue + setting.valueRange.step;
|
||||
}
|
||||
} else if (setting.type == SettingType::ACTION) {
|
||||
if (strcmp(setting.name, "Calibre Settings") == 0) {
|
||||
if (strcmp(setting.name, "Custom Font") == 0) {
|
||||
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
||||
exitActivity();
|
||||
enterNewActivity(new FontSelectionActivity(renderer, mappedInput, [this] {
|
||||
exitActivity();
|
||||
updateRequired = true;
|
||||
}));
|
||||
xSemaphoreGive(renderingMutex);
|
||||
} else if (strcmp(setting.name, "Calibre Settings") == 0) {
|
||||
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
||||
exitActivity();
|
||||
enterNewActivity(new CalibreSettingsActivity(renderer, mappedInput, [this] {
|
||||
|
||||
@ -16,3 +16,6 @@
|
||||
#define UI_10_FONT_ID (-823541435)
|
||||
#define UI_12_FONT_ID (-126318184)
|
||||
#define SMALL_FONT_ID (-874196069)
|
||||
|
||||
// Custom font ID (used as slot ID in fontMap, actual ID is hash-based for cache invalidation)
|
||||
#define CUSTOM_FONT_ID (-999999)
|
||||
|
||||
105
src/main.cpp
105
src/main.cpp
@ -5,6 +5,7 @@
|
||||
#include <InputManager.h>
|
||||
#include <SDCardManager.h>
|
||||
#include <SPI.h>
|
||||
#include <SdFontFamily.h>
|
||||
#include <builtinFonts/all.h>
|
||||
|
||||
#include <cstring>
|
||||
@ -131,6 +132,67 @@ EpdFont ui12RegularFont(&ubuntu_12_regular);
|
||||
EpdFont ui12BoldFont(&ubuntu_12_bold);
|
||||
EpdFontFamily ui12FontFamily(&ui12RegularFont, &ui12BoldFont);
|
||||
|
||||
// Custom font loading from SD card
|
||||
constexpr char FONTS_DIR[] = "/.crosspoint/fonts";
|
||||
|
||||
// Load custom reader font from SD card if configured
|
||||
// Returns true if custom font was loaded successfully
|
||||
bool loadCustomReaderFont(GfxRenderer& gfxRenderer) {
|
||||
if (!SETTINGS.hasCustomFont()) {
|
||||
Serial.printf("[%lu] [FNT] No custom font configured\n", millis());
|
||||
return false;
|
||||
}
|
||||
|
||||
const char* fontPath = SETTINGS.customFontPath;
|
||||
Serial.printf("[%lu] [FNT] Loading custom font: %s\n", millis(), fontPath);
|
||||
|
||||
if (!SdMan.exists(fontPath)) {
|
||||
Serial.printf("[%lu] [FNT] Custom font file not found: %s\n", millis(), fontPath);
|
||||
// Clear invalid font path
|
||||
SETTINGS.customFontPath[0] = '\0';
|
||||
SETTINGS.saveToFile();
|
||||
return false;
|
||||
}
|
||||
|
||||
// Create SdFontFamily for the custom font
|
||||
SdFontFamily* font = new SdFontFamily(fontPath);
|
||||
if (font == nullptr) {
|
||||
Serial.printf("[%lu] [FNT] Failed to allocate memory for custom font\n", millis());
|
||||
return false;
|
||||
}
|
||||
|
||||
if (font->load()) {
|
||||
gfxRenderer.insertSdFont(CUSTOM_FONT_ID, font);
|
||||
Serial.printf("[%lu] [FNT] Custom reader font loaded successfully\n", millis());
|
||||
return true;
|
||||
}
|
||||
|
||||
Serial.printf("[%lu] [FNT] Failed to load custom font, clearing setting\n", millis());
|
||||
delete font;
|
||||
// Clear invalid font path so getReaderFontId() returns default font
|
||||
SETTINGS.customFontPath[0] = '\0';
|
||||
SETTINGS.saveToFile();
|
||||
return false;
|
||||
}
|
||||
|
||||
// Reload custom reader font - removes old font and loads new one
|
||||
// Call this when font settings change to apply immediately without reboot
|
||||
bool reloadCustomReaderFont() {
|
||||
Serial.printf("[%lu] [FNT] Reloading custom reader font...\n", millis());
|
||||
|
||||
// Remove existing custom font if any
|
||||
if (renderer.hasFont(CUSTOM_FONT_ID)) {
|
||||
renderer.removeFont(CUSTOM_FONT_ID);
|
||||
Serial.printf("[%lu] [FNT] Removed previous custom font\n", millis());
|
||||
}
|
||||
|
||||
// Load new custom font if configured
|
||||
return loadCustomReaderFont(renderer);
|
||||
}
|
||||
|
||||
// Get reference to global renderer (for font operations from other modules)
|
||||
GfxRenderer& getGlobalRenderer() { return renderer; }
|
||||
|
||||
// measurement of power button press duration calibration value
|
||||
unsigned long t1 = 0;
|
||||
unsigned long t2 = 0;
|
||||
@ -241,25 +303,34 @@ void onGoHome() {
|
||||
void setupDisplayAndFonts() {
|
||||
einkDisplay.begin();
|
||||
Serial.printf("[%lu] [ ] Display initialized\n", millis());
|
||||
renderer.insertFont(BOOKERLY_14_FONT_ID, bookerly14FontFamily);
|
||||
#ifndef OMIT_FONTS
|
||||
renderer.insertFont(BOOKERLY_12_FONT_ID, bookerly12FontFamily);
|
||||
renderer.insertFont(BOOKERLY_16_FONT_ID, bookerly16FontFamily);
|
||||
renderer.insertFont(BOOKERLY_18_FONT_ID, bookerly18FontFamily);
|
||||
|
||||
renderer.insertFont(NOTOSANS_12_FONT_ID, notosans12FontFamily);
|
||||
renderer.insertFont(NOTOSANS_14_FONT_ID, notosans14FontFamily);
|
||||
renderer.insertFont(NOTOSANS_16_FONT_ID, notosans16FontFamily);
|
||||
renderer.insertFont(NOTOSANS_18_FONT_ID, notosans18FontFamily);
|
||||
renderer.insertFont(OPENDYSLEXIC_8_FONT_ID, opendyslexic8FontFamily);
|
||||
renderer.insertFont(OPENDYSLEXIC_10_FONT_ID, opendyslexic10FontFamily);
|
||||
renderer.insertFont(OPENDYSLEXIC_12_FONT_ID, opendyslexic12FontFamily);
|
||||
renderer.insertFont(OPENDYSLEXIC_14_FONT_ID, opendyslexic14FontFamily);
|
||||
// Register built-in flash fonts
|
||||
renderer.insertFont(BOOKERLY_14_FONT_ID, &bookerly14FontFamily);
|
||||
#ifndef OMIT_FONTS
|
||||
renderer.insertFont(BOOKERLY_12_FONT_ID, &bookerly12FontFamily);
|
||||
renderer.insertFont(BOOKERLY_16_FONT_ID, &bookerly16FontFamily);
|
||||
renderer.insertFont(BOOKERLY_18_FONT_ID, &bookerly18FontFamily);
|
||||
|
||||
renderer.insertFont(NOTOSANS_12_FONT_ID, ¬osans12FontFamily);
|
||||
renderer.insertFont(NOTOSANS_14_FONT_ID, ¬osans14FontFamily);
|
||||
renderer.insertFont(NOTOSANS_16_FONT_ID, ¬osans16FontFamily);
|
||||
renderer.insertFont(NOTOSANS_18_FONT_ID, ¬osans18FontFamily);
|
||||
renderer.insertFont(OPENDYSLEXIC_8_FONT_ID, &opendyslexic8FontFamily);
|
||||
renderer.insertFont(OPENDYSLEXIC_10_FONT_ID, &opendyslexic10FontFamily);
|
||||
renderer.insertFont(OPENDYSLEXIC_12_FONT_ID, &opendyslexic12FontFamily);
|
||||
renderer.insertFont(OPENDYSLEXIC_14_FONT_ID, &opendyslexic14FontFamily);
|
||||
#endif // OMIT_FONTS
|
||||
renderer.insertFont(UI_10_FONT_ID, ui10FontFamily);
|
||||
renderer.insertFont(UI_12_FONT_ID, ui12FontFamily);
|
||||
renderer.insertFont(SMALL_FONT_ID, smallFontFamily);
|
||||
Serial.printf("[%lu] [ ] Fonts setup\n", millis());
|
||||
renderer.insertFont(UI_10_FONT_ID, &ui10FontFamily);
|
||||
renderer.insertFont(UI_12_FONT_ID, &ui12FontFamily);
|
||||
renderer.insertFont(SMALL_FONT_ID, &smallFontFamily);
|
||||
|
||||
// Set fallback font
|
||||
renderer.setFallbackFont(UI_10_FONT_ID);
|
||||
|
||||
// Try to load custom reader font from SD card
|
||||
loadCustomReaderFont(renderer);
|
||||
|
||||
Serial.printf("[%lu] [ ] Fonts setup complete\n", millis());
|
||||
}
|
||||
|
||||
void setup() {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user