Add SdCardFontFamilyInfo::pickClosestSize(targetPtSize) and have the
manager and SdCardFontSystem drive size selection from a target point
size derived from the user's font-size enum (SMALL=12, MEDIUM=14,
LARGE=16, EXTRA_LARGE=18) rather than indexing the family's sorted size
list by enum ordinal.
The ordinal-slot approach mis-selects whenever a family doesn't ship the
canonical {12,14,16,18} set: a family with only [10,14,18] would map
SMALL/MEDIUM/LARGE/EXTRA_LARGE to 10/14/18/18 — fine for SMALL but
arbitrary for the rest. Closest-pt always picks the on-disk file nearest
to the user-intended point size, with a deterministic smaller-pt
tie-break.
No change for canonical-sized families.
## Summary
* **What is the goal of this PR?** (e.g., Implements the new feature for
file uploading.)
* **What changes are included?**
## Additional Context
* Add any other information that might be helpful for the reviewer
(e.g., performance implications, potential risks,
specific areas to focus on).
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? _**< YES | PARTIALLY | NO
>**_
248 lines
8.1 KiB
C++
248 lines
8.1 KiB
C++
#include "SdCardFontRegistry.h"
|
|
|
|
#include <HalStorage.h>
|
|
#include <Logging.h>
|
|
|
|
#include <algorithm>
|
|
#include <climits>
|
|
#include <cstdlib>
|
|
#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;
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
const SdCardFontFileInfo* SdCardFontFamilyInfo::pickClosestSize(uint8_t targetPtSize) const {
|
|
const SdCardFontFileInfo* selected = nullptr;
|
|
int bestDiff = INT_MAX;
|
|
for (const auto& f : files) {
|
|
int diff = std::abs(static_cast<int>(f.pointSize) - static_cast<int>(targetPtSize));
|
|
// Strict < ensures the first scan wins on ties; then tie-break by smaller
|
|
// pointSize to make the choice independent of filesystem enumeration order.
|
|
if (diff < bestDiff || (diff == bestDiff && selected && f.pointSize < selected->pointSize)) {
|
|
bestDiff = diff;
|
|
selected = &f;
|
|
}
|
|
}
|
|
return selected;
|
|
}
|
|
|
|
// --- 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) {
|
|
FsFile dir = Storage.open(dirPath);
|
|
if (!dir || !dir.isDirectory()) return;
|
|
|
|
char nameBuffer[128];
|
|
while (true) {
|
|
FsFile 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) {
|
|
FsFile 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) {
|
|
FsFile 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;
|
|
}
|