285 lines
9.1 KiB
C++
285 lines
9.1 KiB
C++
#include "SdCardFontRegistry.h"
|
|
|
|
#include <HalStorage.h>
|
|
#include <Logging.h>
|
|
|
|
#include <algorithm>
|
|
#include <cstring>
|
|
|
|
// --- 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<uint8_t> 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<uint8_t> SdCardFontFamilyInfo::availableSizes() const {
|
|
std::vector<uint8_t> 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: <name>_<size>.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<uint8_t>(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<SdCardFontFamilyInfo>& 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<int>(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<int>(families_.size()) > MAX_SD_FAMILIES) {
|
|
families_.resize(MAX_SD_FAMILIES);
|
|
}
|
|
|
|
LOG_DBG("SDREG", "Discovery complete: %d families", static_cast<int>(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<int>(families_.size()); i++) {
|
|
if (families_[i].name == name) return i;
|
|
}
|
|
return -1;
|
|
}
|