ExternalFont.cpp
1 #include "ExternalFont.h" 2 3 #include <HardwareSerial.h> 4 5 #include <algorithm> 6 #include <cstring> 7 #include <vector> 8 9 ExternalFont::~ExternalFont() { unload(); } 10 11 void ExternalFont::unload() { 12 if (_fontFile) { 13 _fontFile.close(); 14 } 15 _isLoaded = false; 16 _fontName[0] = '\0'; 17 _fontSize = 0; 18 _charWidth = 0; 19 _charHeight = 0; 20 _bytesPerRow = 0; 21 _bytesPerChar = 0; 22 _accessCounter = 0; 23 24 // Clear cache and hash table 25 for (int i = 0; i < CACHE_SIZE; i++) { 26 _cache[i].codepoint = 0xFFFFFFFF; 27 _cache[i].lastUsed = 0; 28 _cache[i].notFound = false; 29 _hashTable[i] = HASH_EMPTY; 30 } 31 } 32 33 bool ExternalFont::parseFilename(const char* filepath) { 34 // Extract filename from path 35 const char* filename = strrchr(filepath, '/'); 36 if (filename) { 37 filename++; // Skip '/' 38 } else { 39 filename = filepath; 40 } 41 42 // Parse format: FontName_size_WxH.bin 43 // Example: KingHwaOldSong_38_33x39.bin 44 45 char nameCopy[64]; 46 strncpy(nameCopy, filename, sizeof(nameCopy) - 1); 47 nameCopy[sizeof(nameCopy) - 1] = '\0'; 48 49 // Remove .bin extension 50 char* ext = strstr(nameCopy, ".bin"); 51 if (!ext) { 52 Serial.printf("[EXT_FONT] Invalid filename: no .bin extension\n"); 53 return false; 54 } 55 *ext = '\0'; 56 57 // Find _WxH part from the end 58 char* lastUnderscore = strrchr(nameCopy, '_'); 59 if (!lastUnderscore) { 60 Serial.printf("[EXT_FONT] Invalid filename format\n"); 61 return false; 62 } 63 64 // Parse WxH 65 int w, h; 66 if (sscanf(lastUnderscore + 1, "%dx%d", &w, &h) != 2) { 67 Serial.printf("[EXT_FONT] Failed to parse dimensions\n"); 68 return false; 69 } 70 _charWidth = (uint8_t)w; 71 _charHeight = (uint8_t)h; 72 73 // Validate dimensions 74 static constexpr uint8_t MAX_CHAR_DIM = 64; 75 if (_charWidth > MAX_CHAR_DIM || _charHeight > MAX_CHAR_DIM) { 76 Serial.printf("[EXT_FONT] Dimensions too large: %dx%d (max %d). Using default font.\n", _charWidth, _charHeight, 77 MAX_CHAR_DIM); 78 return false; 79 } 80 81 *lastUnderscore = '\0'; 82 83 // Find size 84 lastUnderscore = strrchr(nameCopy, '_'); 85 if (!lastUnderscore) { 86 Serial.printf("[EXT_FONT] Invalid filename format: no size\n"); 87 return false; 88 } 89 90 int size; 91 if (sscanf(lastUnderscore + 1, "%d", &size) != 1) { 92 Serial.printf("[EXT_FONT] Failed to parse size\n"); 93 return false; 94 } 95 _fontSize = (uint8_t)size; 96 *lastUnderscore = '\0'; 97 98 // Remaining part is font name 99 strncpy(_fontName, nameCopy, sizeof(_fontName) - 1); 100 _fontName[sizeof(_fontName) - 1] = '\0'; 101 102 // Calculate bytes per char 103 _bytesPerRow = (_charWidth + 7) / 8; 104 _bytesPerChar = _bytesPerRow * _charHeight; 105 106 if (_bytesPerChar > MAX_GLYPH_BYTES) { 107 Serial.printf("[EXT_FONT] Glyph too large: %d bytes (max %d)\n", _bytesPerChar, MAX_GLYPH_BYTES); 108 return false; 109 } 110 111 Serial.printf("[EXT_FONT] Parsed: name=%s, size=%d, %dx%d, %d bytes/char\n", _fontName, _fontSize, _charWidth, 112 _charHeight, _bytesPerChar); 113 114 return true; 115 } 116 117 bool ExternalFont::load(const char* filepath) { 118 unload(); 119 120 if (!parseFilename(filepath)) { 121 return false; 122 } 123 124 if (!SdMan.openFileForRead("EXT_FONT", filepath, _fontFile)) { 125 Serial.printf("[EXT_FONT] Failed to open: %s\n", filepath); 126 return false; 127 } 128 129 // Validate file size 130 static constexpr uint32_t MAX_FONT_FILE_SIZE = 32 * 1024 * 1024; // 32MB max 131 uint32_t fileSize = _fontFile.size(); 132 if (fileSize == 0 || fileSize > MAX_FONT_FILE_SIZE) { 133 Serial.printf("[EXT_FONT] Invalid file size: %u bytes (max 32MB). Using default font.\n", fileSize); 134 _fontFile.close(); 135 return false; 136 } 137 138 _isLoaded = true; 139 Serial.printf("[EXT_FONT] Loaded: %s\n", filepath); 140 return true; 141 } 142 143 int ExternalFont::findInCache(uint32_t codepoint) { 144 // O(1) hash table lookup with linear probing for collisions 145 int hash = hashCodepoint(codepoint); 146 for (int i = 0; i < CACHE_SIZE; i++) { 147 int idx = (hash + i) % CACHE_SIZE; 148 int16_t cacheIdx = _hashTable[idx]; 149 if (cacheIdx == HASH_EMPTY) { 150 // Empty slot (never used) - entry not in table 151 return -1; 152 } 153 if (cacheIdx == HASH_TOMBSTONE) { 154 // Deleted slot - continue probing 155 continue; 156 } 157 if (_cache[cacheIdx].codepoint == codepoint) { 158 return cacheIdx; 159 } 160 } 161 return -1; 162 } 163 164 int ExternalFont::getLruSlot() { 165 int lruIndex = 0; 166 uint32_t minUsed = _cache[0].lastUsed; 167 168 for (int i = 1; i < CACHE_SIZE; i++) { 169 // Prefer unused slots 170 if (_cache[i].codepoint == 0xFFFFFFFF) { 171 return i; 172 } 173 if (_cache[i].lastUsed < minUsed) { 174 minUsed = _cache[i].lastUsed; 175 lruIndex = i; 176 } 177 } 178 return lruIndex; 179 } 180 181 bool ExternalFont::readGlyphFromSD(uint32_t codepoint, uint8_t* buffer) { 182 if (!_fontFile) { 183 return false; 184 } 185 186 // Calculate offset 187 uint32_t offset = codepoint * _bytesPerChar; 188 189 // Seek and read 190 if (!_fontFile.seek(offset)) { 191 return false; 192 } 193 194 size_t bytesRead = _fontFile.read(buffer, _bytesPerChar); 195 if (bytesRead != _bytesPerChar) { 196 // May be end of file or other error, fill with zeros 197 memset(buffer, 0, _bytesPerChar); 198 } 199 200 return true; 201 } 202 203 const uint8_t* ExternalFont::getGlyph(uint32_t codepoint) { 204 if (!_isLoaded) { 205 return nullptr; 206 } 207 208 // First check cache (O(1) with hash table) 209 int cacheIndex = findInCache(codepoint); 210 if (cacheIndex >= 0) { 211 _cache[cacheIndex].lastUsed = ++_accessCounter; 212 // Return nullptr if this codepoint was previously marked as not found 213 if (_cache[cacheIndex].notFound) { 214 return nullptr; 215 } 216 return _cache[cacheIndex].bitmap; 217 } 218 219 // Cache miss, need to read from SD card 220 int slot = getLruSlot(); 221 222 // If replacing an existing entry, mark it as tombstone in hash table 223 if (_cache[slot].codepoint != 0xFFFFFFFF) { 224 int oldHash = hashCodepoint(_cache[slot].codepoint); 225 for (int i = 0; i < CACHE_SIZE; i++) { 226 int idx = (oldHash + i) % CACHE_SIZE; 227 if (_hashTable[idx] == slot) { 228 _hashTable[idx] = HASH_TOMBSTONE; 229 break; 230 } 231 } 232 } 233 234 // Read glyph from SD card 235 bool readSuccess = readGlyphFromSD(codepoint, _cache[slot].bitmap); 236 237 // Calculate metrics and check if glyph is empty 238 uint8_t minX = _charWidth; 239 uint8_t maxX = 0; 240 bool isEmpty = true; 241 242 if (readSuccess && _bytesPerChar > 0) { 243 for (int y = 0; y < _charHeight; y++) { 244 for (int x = 0; x < _charWidth; x++) { 245 int byteIndex = y * _bytesPerRow + (x / 8); 246 int bitIndex = 7 - (x % 8); 247 if ((_cache[slot].bitmap[byteIndex] >> bitIndex) & 1) { 248 isEmpty = false; 249 if (x < minX) minX = x; 250 if (x > maxX) maxX = x; 251 } 252 } 253 } 254 } 255 256 // Update cache entry 257 _cache[slot].codepoint = codepoint; 258 _cache[slot].lastUsed = ++_accessCounter; 259 260 // Check if this is a whitespace character (U+2000-U+200F: various spaces, U+3000: ideographic space) 261 bool isWhitespace = (codepoint >= 0x2000 && codepoint <= 0x200F) || codepoint == 0x3000; 262 263 // Mark as notFound only if read failed or (empty AND not whitespace AND non-ASCII) 264 // Whitespace characters are expected to be empty but should still be rendered 265 _cache[slot].notFound = !readSuccess || (isEmpty && !isWhitespace && codepoint > 0x7F); 266 267 // Store metrics 268 if (!isEmpty) { 269 _cache[slot].minX = minX; 270 // Variable width: content width + 2px padding 271 _cache[slot].advanceX = (maxX - minX + 1) + 2; 272 } else { 273 _cache[slot].minX = 0; 274 // Special handling for whitespace characters 275 if (isWhitespace) { 276 // em-space (U+2003) and similar should be full-width (same as CJK char) 277 // en-space (U+2002) should be half-width 278 // Other spaces use appropriate widths 279 if (codepoint == 0x2003) { 280 // em-space: full CJK character width 281 _cache[slot].advanceX = _charWidth; 282 } else if (codepoint == 0x2002) { 283 // en-space: half CJK character width 284 _cache[slot].advanceX = _charWidth / 2; 285 } else if (codepoint == 0x3000) { 286 // Ideographic space (CJK full-width space): full width 287 _cache[slot].advanceX = _charWidth; 288 } else { 289 // Other spaces: use standard space width 290 _cache[slot].advanceX = _charWidth / 3; 291 } 292 } else { 293 // Fallback for other empty glyphs 294 _cache[slot].advanceX = _charWidth / 3; 295 } 296 } 297 298 // Add to hash table (reuse tombstones or empty slots) 299 int hash = hashCodepoint(codepoint); 300 for (int i = 0; i < CACHE_SIZE; i++) { 301 int idx = (hash + i) % CACHE_SIZE; 302 if (_hashTable[idx] == HASH_EMPTY || _hashTable[idx] == HASH_TOMBSTONE) { 303 _hashTable[idx] = slot; 304 break; 305 } 306 } 307 308 if (_cache[slot].notFound) { 309 return nullptr; 310 } 311 312 return _cache[slot].bitmap; 313 } 314 315 bool ExternalFont::getGlyphMetrics(uint32_t codepoint, uint8_t* outMinX, uint8_t* outAdvanceX) { 316 int idx = findInCache(codepoint); 317 if (idx >= 0 && !_cache[idx].notFound) { 318 if (outMinX) *outMinX = _cache[idx].minX; 319 if (outAdvanceX) *outAdvanceX = _cache[idx].advanceX; 320 return true; 321 } 322 return false; 323 } 324 325 void ExternalFont::preloadGlyphs(const uint32_t* codepoints, size_t count) { 326 if (!_isLoaded || !codepoints || count == 0) { 327 return; 328 } 329 330 // Limit to cache size to avoid thrashing 331 const size_t maxLoad = std::min(count, static_cast<size_t>(CACHE_SIZE)); 332 333 // Create a sorted copy for sequential SD card access 334 // Sequential reads are much faster than random seeks 335 std::vector<uint32_t> sorted(codepoints, codepoints + maxLoad); 336 std::sort(sorted.begin(), sorted.end()); 337 338 // Remove duplicates 339 sorted.erase(std::unique(sorted.begin(), sorted.end()), sorted.end()); 340 341 Serial.printf("[EXT_FONT] Preloading %zu unique glyphs\n", sorted.size()); 342 const unsigned long startTime = millis(); 343 344 size_t loaded = 0; 345 size_t skipped = 0; 346 347 for (uint32_t cp : sorted) { 348 // Skip if already in cache 349 if (findInCache(cp) >= 0) { 350 skipped++; 351 continue; 352 } 353 354 // Load into cache (getGlyph handles all the cache management) 355 getGlyph(cp); 356 loaded++; 357 } 358 359 Serial.printf("[EXT_FONT] Preload done: %zu loaded, %zu already cached, took %lums\n", loaded, skipped, 360 millis() - startTime); 361 } 362 363 void ExternalFont::logCacheStats() const { 364 int used = 0; 365 for (int i = 0; i < CACHE_SIZE; i++) { 366 if (_cache[i].codepoint != 0xFFFFFFFF) used++; 367 } 368 Serial.printf("[EXT_FONT] Cache: %d/%d slots used (~%dKB)\n", used, CACHE_SIZE, (used * sizeof(CacheEntry)) / 1024); 369 }