#include "SdCardFontRegistry.h" #include #include #include #include // --- SdCardFontFamilyInfo helpers --- const SdCardFontFileInfo* SdCardFontFamilyInfo::findFile(uint8_t size, uint8_t style) const { for (const auto& f : files) { if (f.pointSize == size && f.style == style) return &f; } return nullptr; } const SdCardFontFileInfo* SdCardFontFamilyInfo::findClosestReaderSize(const uint8_t fontSizeEnum, const uint8_t style) const { if (files.empty()) return nullptr; // Collect sizes matching the requested style, sorted ascending. std::vector sizes; for (const auto& f : files) { if (f.style != style) continue; sizes.push_back(f.pointSize); } if (sizes.empty()) return nullptr; std::sort(sizes.begin(), sizes.end()); // When the family provides at least 4 sizes, use ordinal (index-based) // selection so custom-built font sets (e.g. 10/12/14/16) map SMALL to // the smallest file, not to a hardcoded 12pt target. if (sizes.size() >= 4) { uint8_t idx = fontSizeEnum; if (idx >= sizes.size()) idx = sizes.size() - 1; return findFile(sizes[idx], style); } // Fewer sizes than enum slots (e.g. CJK packs with only 2-3 sizes): // fall back to closest-match against the built-in reader targets. uint8_t target = 14; switch (fontSizeEnum) { case 0: target = 12; break; case 2: target = 16; break; case 3: target = 18; break; case 1: default: target = 14; break; } const SdCardFontFileInfo* best = nullptr; uint8_t bestDelta = 255; for (const auto& f : files) { if (f.style != style) continue; const uint8_t delta = f.pointSize > target ? f.pointSize - target : target - f.pointSize; if (!best || delta < bestDelta || (delta == bestDelta && f.pointSize < best->pointSize)) { best = &f; bestDelta = delta; } } return best; } bool SdCardFontFamilyInfo::hasSize(uint8_t size) const { for (const auto& f : files) { if (f.pointSize == size) return true; } return false; } std::vector SdCardFontFamilyInfo::availableSizes() const { std::vector sizes; for (const auto& f : files) { bool found = false; for (uint8_t s : sizes) { if (s == f.pointSize) { found = true; break; } } if (!found) sizes.push_back(f.pointSize); } std::sort(sizes.begin(), sizes.end()); return sizes; } // --- SdCardFontRegistry --- bool SdCardFontRegistry::parseFilename(const char* filename, uint8_t& size, uint8_t& style) { // V4 naming: _.cpfont (e.g. Bookerly-SD_14.cpfont) // Use an ends-with check rather than strstr() so that in-progress downloads // like "Foo_14.cpfont.tmp" or backups like "Foo_14.cpfont~" aren't accepted. static constexpr char kExt[] = ".cpfont"; static constexpr size_t kExtLen = sizeof(kExt) - 1; const size_t nameLen = strlen(filename); if (nameLen <= kExtLen) return false; if (strcmp(filename + nameLen - kExtLen, kExt) != 0) return false; const char* ext = filename + nameLen - kExtLen; size_t baseLen = ext - filename; if (baseLen == 0 || baseLen > 127) return false; char base[128]; memcpy(base, filename, baseLen); base[baseLen] = '\0'; char* lastUnderscore = strrchr(base, '_'); if (!lastUnderscore || lastUnderscore == base) return false; const char* sizeStr = lastUnderscore + 1; char* endPtr; long sizeVal = strtol(sizeStr, &endPtr, 10); if (endPtr == sizeStr || *endPtr != '\0' || sizeVal < 1 || sizeVal > 255) return false; size = static_cast(sizeVal); // V4 .cpfont files bundle every style (regular/bold/italic/bold-italic) into // one file, so style is always 0 at the registry level. The per-style // bitstream is selected later by SdCardFont::getEpdFont(style). The `style` // field in SdCardFontFileInfo is reserved for future formats that split // styles across files; scanDirectory() defends against accidental // (pointSize, style) collisions in that scenario. style = 0; return true; } void SdCardFontRegistry::scanDirectory(const char* dirPath, SdCardFontFamilyInfo& family) { HalFile dir = Storage.open(dirPath); if (!dir || !dir.isDirectory()) return; char nameBuffer[128]; while (true) { HalFile entry = dir.openNextFile(); if (!entry) break; if (entry.isDirectory()) { entry.close(); continue; } entry.getName(nameBuffer, sizeof(nameBuffer)); entry.close(); // Skip macOS resource fork files (._*) and other hidden files if (nameBuffer[0] == '.' || nameBuffer[0] == '_') continue; uint8_t size, style; if (!parseFilename(nameBuffer, size, style)) continue; // Reject duplicate (pointSize, style) entries in the same family. With // v4's bundle-everything design parseFilename always returns style=0, so // two files at the same size in the same family would silently shadow // each other in findFile(). Skip the duplicate and warn. bool duplicate = false; for (const auto& existing : family.files) { if (existing.pointSize == size && existing.style == style) { duplicate = true; break; } } if (duplicate) { LOG_ERR("SDREG", "Duplicate font %s in %s — skipping", nameBuffer, dirPath); continue; } SdCardFontFileInfo info; info.path = std::string(dirPath) + "/" + nameBuffer; info.pointSize = size; info.style = style; family.files.push_back(std::move(info)); } } // Scan a single root (e.g. "/.fonts") and append its families to `out`. // Skips families whose names already exist in `out` (de-duplicates between // the hidden and visible roots — first scan wins). void SdCardFontRegistry::scanRoot(const char* rootPath, std::vector& out) { HalFile root = Storage.open(rootPath); if (!root) { LOG_DBG("SDREG", "Fonts directory not found: %s", rootPath); return; } if (!root.isDirectory()) { LOG_ERR("SDREG", "Fonts path is not a directory: %s", rootPath); return; } char nameBuffer[128]; while (true) { HalFile entry = root.openNextFile(); if (!entry) break; if (entry.isDirectory()) { entry.getName(nameBuffer, sizeof(nameBuffer)); entry.close(); // Skip hidden/system directories inside the root (macOS ._*, .Trashes, etc.) if (nameBuffer[0] == '.' || nameBuffer[0] == '_') continue; // De-dup by family name across roots. bool exists = false; for (const auto& fam : out) { if (fam.name == nameBuffer) { exists = true; break; } } if (exists) continue; SdCardFontFamilyInfo family; family.name = nameBuffer; std::string subDirPath = std::string(rootPath) + "/" + nameBuffer; SdCardFontRegistry::scanDirectory(subDirPath.c_str(), family); if (!family.files.empty()) { out.push_back(std::move(family)); LOG_DBG("SDREG", "Found family: %s (%d files) in %s", out.back().name.c_str(), static_cast(out.back().files.size()), rootPath); } } else { entry.close(); } } } bool SdCardFontRegistry::discover() { families_.clear(); families_.reserve(MAX_SD_FAMILIES); // Hidden root is scanned first so it wins on name collisions, matching the // sleep-folder pattern (/.sleep preferred over /sleep). scanRoot(FONTS_DIR_HIDDEN, families_); scanRoot(FONTS_DIR_VISIBLE, families_); // Sort families alphabetically std::sort(families_.begin(), families_.end(), [](const SdCardFontFamilyInfo& a, const SdCardFontFamilyInfo& b) { return a.name < b.name; }); // Cap at MAX_SD_FAMILIES if (static_cast(families_.size()) > MAX_SD_FAMILIES) { families_.resize(MAX_SD_FAMILIES); } LOG_DBG("SDREG", "Discovery complete: %d families", static_cast(families_.size())); return !families_.empty(); } const char* SdCardFontRegistry::findFamilyRoot(const char* familyName) { if (!familyName || !*familyName) return nullptr; char path[160]; snprintf(path, sizeof(path), "%s/%s", FONTS_DIR_HIDDEN, familyName); if (Storage.exists(path)) return FONTS_DIR_HIDDEN; snprintf(path, sizeof(path), "%s/%s", FONTS_DIR_VISIBLE, familyName); if (Storage.exists(path)) return FONTS_DIR_VISIBLE; return nullptr; } const char* SdCardFontRegistry::defaultWriteRoot() { // If exactly one of the roots already exists, keep using it. Otherwise // (neither exists, or both exist) prefer the hidden root for new installs. bool hiddenExists = Storage.exists(FONTS_DIR_HIDDEN); bool visibleExists = Storage.exists(FONTS_DIR_VISIBLE); if (hiddenExists) return FONTS_DIR_HIDDEN; if (visibleExists) return FONTS_DIR_VISIBLE; return FONTS_DIR_HIDDEN; } const SdCardFontFamilyInfo* SdCardFontRegistry::findFamily(const std::string& name) const { for (const auto& f : families_) { if (f.name == name) return &f; } return nullptr; } int SdCardFontRegistry::getFamilyIndex(const std::string& name) const { for (int i = 0; i < static_cast(families_.size()); i++) { if (families_[i].name == name) return i; } return -1; }