/ lib / ExternalFont / ExternalFont.cpp
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  }