Merge branch 'master' into release/1.3.0
This commit is contained in:
@@ -28,25 +28,21 @@ int SdCardFontManager::computeFontId(uint32_t contentHash, const char* familyNam
|
||||
return id != 0 ? id : 1; // 0 is reserved as "not found" sentinel
|
||||
}
|
||||
|
||||
bool SdCardFontManager::loadFamily(const SdCardFontFamilyInfo& family, GfxRenderer& renderer, uint8_t fontSizeEnum) {
|
||||
bool SdCardFontManager::loadFamily(const SdCardFontFamilyInfo& family, GfxRenderer& renderer, uint8_t targetPtSize) {
|
||||
// Unload any previously loaded family first
|
||||
if (!loadedFamilyName_.empty()) {
|
||||
unloadAll(renderer);
|
||||
}
|
||||
|
||||
// Select by ordinal position: sort available sizes, then map the font size
|
||||
// enum (SMALL=0 .. EXTRA_LARGE=3) to the corresponding slot. When the
|
||||
// family has fewer sizes than 4, clamp to the last available size.
|
||||
auto sizes = family.availableSizes();
|
||||
if (sizes.empty()) {
|
||||
// Pick the single file whose size is closest to targetPtSize. Loading
|
||||
// only one size bounds resident memory (intervals + kern/ligature tables
|
||||
// per style) to one file's worth, vs. N_sizes × per-file overhead.
|
||||
const SdCardFontFileInfo* selected = family.pickClosestSize(targetPtSize);
|
||||
if (!selected) {
|
||||
LOG_ERR("SDMGR", "Family %s has no files to load", family.name.c_str());
|
||||
return false;
|
||||
}
|
||||
|
||||
uint8_t idx = fontSizeEnum;
|
||||
if (idx >= sizes.size()) idx = sizes.size() - 1;
|
||||
const SdCardFontFileInfo* selected = family.findFile(sizes[idx]);
|
||||
|
||||
auto* font = new (std::nothrow) SdCardFont();
|
||||
if (!font) {
|
||||
LOG_ERR("SDMGR", "Failed to allocate SdCardFont for %s", selected->path.c_str());
|
||||
@@ -70,8 +66,8 @@ bool SdCardFontManager::loadFamily(const SdCardFontFamilyInfo& family, GfxRender
|
||||
renderer.registerSdCardFont(fontId, font);
|
||||
loaded_.push_back({font, fontId, selected->pointSize});
|
||||
|
||||
LOG_DBG("SDMGR", "Loaded %s size=%u id=%d styles=%u (sizeEnum=%u)", selected->path.c_str(), selected->pointSize,
|
||||
fontId, font->styleCount(), fontSizeEnum);
|
||||
LOG_DBG("SDMGR", "Loaded %s size=%u id=%d styles=%u (target=%u)", selected->path.c_str(), selected->pointSize, fontId,
|
||||
font->styleCount(), targetPtSize);
|
||||
|
||||
EpdFontFamily fontFamily(font->getEpdFont(0), font->getEpdFont(1), font->getEpdFont(2), font->getEpdFont(3));
|
||||
renderer.insertFont(fontId, fontFamily);
|
||||
|
||||
@@ -15,12 +15,16 @@ class SdCardFontManager {
|
||||
SdCardFontManager(const SdCardFontManager&) = delete;
|
||||
SdCardFontManager& operator=(const SdCardFontManager&) = delete;
|
||||
|
||||
// Load the font file matching fontSizeEnum (SMALL=0 .. EXTRA_LARGE=3) by
|
||||
// ordinal position in the family's sorted size list. Only one .cpfont file
|
||||
// is loaded; other sizes remain on disk. This keeps resident interval +
|
||||
// kern/ligature tables to one size's worth of memory.
|
||||
// Load the single size whose pointSize is closest to targetPtSize. Only one
|
||||
// .cpfont file is loaded; other sizes remain on disk. This keeps resident
|
||||
// interval + kern/ligature tables to one size's worth of memory.
|
||||
//
|
||||
// Closest-pt selection is robust against families that don't ship the
|
||||
// canonical {12,14,16,18}: a family with only [10,14,18] still resolves
|
||||
// any reasonable target, where ordinal slot-mapping by SMALL..EXTRA_LARGE
|
||||
// would mis-select.
|
||||
// Returns true on success.
|
||||
bool loadFamily(const SdCardFontFamilyInfo& family, GfxRenderer& renderer, uint8_t fontSizeEnum);
|
||||
bool loadFamily(const SdCardFontFamilyInfo& family, GfxRenderer& renderer, uint8_t targetPtSize);
|
||||
|
||||
// Unload everything, unregister from renderer.
|
||||
void unloadAll(GfxRenderer& renderer);
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
#include <Logging.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <climits>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
|
||||
// --- SdCardFontFamilyInfo helpers ---
|
||||
@@ -38,6 +40,21 @@ std::vector<uint8_t> SdCardFontFamilyInfo::availableSizes() const {
|
||||
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) {
|
||||
|
||||
@@ -20,6 +20,15 @@ struct SdCardFontFamilyInfo {
|
||||
const SdCardFontFileInfo* findFile(uint8_t size, uint8_t style = 0) const;
|
||||
bool hasSize(uint8_t size) const;
|
||||
std::vector<uint8_t> availableSizes() const;
|
||||
|
||||
// Pick the file whose pointSize is closest to targetPtSize. On ties (equal
|
||||
// distance) prefers the smaller pointSize so behaviour is deterministic
|
||||
// across SD card layouts. Returns nullptr when files is empty.
|
||||
//
|
||||
// Robust against families that don't ship the canonical {12,14,16,18} set:
|
||||
// a family with only [10,14,18] resolves target 12 → 10 (or 14 on tie),
|
||||
// target 16 → 14 or 18, etc., instead of mis-indexing by ordinal slot.
|
||||
const SdCardFontFileInfo* pickClosestSize(uint8_t targetPtSize) const;
|
||||
};
|
||||
|
||||
class SdCardFontRegistry {
|
||||
|
||||
@@ -85,15 +85,29 @@ def extract_static_instance(source_path: Path, axes: dict, family_name: str, sty
|
||||
tmp_fd, tmp_name = tempfile.mkstemp(suffix=".ttf", dir=cached.parent)
|
||||
os.close(tmp_fd)
|
||||
tmp_path = Path(tmp_name)
|
||||
font = TTFont(str(source_path))
|
||||
# Keep separate handles for the source variable font and the static
|
||||
# instance: instantiateVariableFont with default inplace=False returns a
|
||||
# *new* TTFont, so rebinding `font` would otherwise strand the source's
|
||||
# file handle open until GC runs.
|
||||
#
|
||||
# updateFontNames=True — rewrite the name table so the saved font
|
||||
# reports its weight/style accurately rather
|
||||
# than retaining the variable-font names.
|
||||
# optimize=False — skip the gvar interpolation optimisation;
|
||||
# fully pinning every axis drops gvar anyway,
|
||||
# so the work would be wasted.
|
||||
source_font = TTFont(str(source_path))
|
||||
try:
|
||||
instantiateVariableFont(font, axes)
|
||||
font.save(str(tmp_path))
|
||||
font = instantiateVariableFont(source_font, axes, updateFontNames=True, optimize=False)
|
||||
try:
|
||||
font.save(str(tmp_path))
|
||||
finally:
|
||||
font.close()
|
||||
except Exception:
|
||||
tmp_path.unlink(missing_ok=True)
|
||||
raise
|
||||
finally:
|
||||
font.close()
|
||||
source_font.close()
|
||||
tmp_path.replace(cached)
|
||||
|
||||
return cached
|
||||
|
||||
@@ -8,6 +8,12 @@ import argparse
|
||||
from collections import namedtuple
|
||||
from fontTools.ttLib import TTFont
|
||||
|
||||
# Force UTF-8 stdout so that `python fontconvert.py … > foo.h` on Windows
|
||||
# (default cp1252) doesn't emit UTF-16 LE / replacement chars in the generated
|
||||
# header. Wrapped in a hasattr guard so it's a no-op on older Pythons.
|
||||
if hasattr(sys.stdout, 'reconfigure'):
|
||||
sys.stdout.reconfigure(encoding='utf-8')
|
||||
|
||||
# Originally from https://github.com/vroland/epdiy
|
||||
|
||||
parser = argparse.ArgumentParser(description="Generate a header file from a font to be used with epdiy.")
|
||||
@@ -792,6 +798,15 @@ if compress:
|
||||
# are grouped together for efficient LRU caching on the embedded target.
|
||||
# Since glyphs are in codepoint order, glyphs in the same Unicode block
|
||||
# are contiguous in the array and form natural groups.
|
||||
#
|
||||
# On top of script boundaries, a hard size cap (GROUP_MAX_UNCOMPRESSED_BYTES)
|
||||
# is applied: if adding the next glyph would push the uncompressed group
|
||||
# size over the cap, the group is closed and a new one started with the
|
||||
# same script ID. This bounds the embedded decompressor's transient
|
||||
# malloc regardless of font density (CJK, Vietnamese, user-supplied
|
||||
# fonts with large Unicode blocks). Without it, a single dense script
|
||||
# group can balloon past what fits in a transient page-decompress
|
||||
# allocation on the device.
|
||||
SCRIPT_GROUP_RANGES = [
|
||||
(0x0000, 0x007F), # ASCII
|
||||
(0x0080, 0x00FF), # Latin-1 Supplement
|
||||
@@ -809,6 +824,11 @@ if compress:
|
||||
(0xFFFD, 0xFFFD), # Replacement Character
|
||||
]
|
||||
|
||||
# 64 KB cap: large enough to hold any single built-in script group with
|
||||
# headroom, small enough to be a comfortable transient malloc on the
|
||||
# ESP32-C3.
|
||||
GROUP_MAX_UNCOMPRESSED_BYTES = 65536
|
||||
|
||||
def get_script_group(code_point):
|
||||
for i, (start, end) in enumerate(SCRIPT_GROUP_RANGES):
|
||||
if start <= code_point <= end:
|
||||
@@ -819,17 +839,34 @@ if compress:
|
||||
current_group_id = None
|
||||
group_start = 0
|
||||
group_count = 0
|
||||
group_uncompressed = 0
|
||||
|
||||
for i, (props, packed) in enumerate(all_glyphs):
|
||||
for i, (props, _) in enumerate(all_glyphs):
|
||||
sg = get_script_group(props.code_point)
|
||||
if sg != current_group_id:
|
||||
# Use the byte-aligned size (4-pixel-aligned row stride) rather than
|
||||
# the packed length, since the decompressor consumes byte-aligned
|
||||
# buffers. Empty glyphs contribute zero.
|
||||
glyph_aligned_size = (((props.width + 3) // 4) * props.height
|
||||
if props.width > 0 and props.height > 0 else 0)
|
||||
if glyph_aligned_size > GROUP_MAX_UNCOMPRESSED_BYTES:
|
||||
raise ValueError(
|
||||
f"Glyph {i} (code point U+{props.code_point:04X}) byte-aligned size "
|
||||
f"{glyph_aligned_size} exceeds GROUP_MAX_UNCOMPRESSED_BYTES="
|
||||
f"{GROUP_MAX_UNCOMPRESSED_BYTES}. Consider: (1) increasing GROUP_MAX_UNCOMPRESSED_BYTES, "
|
||||
f"(2) reducing font size, or (3) excluding this codepoint."
|
||||
)
|
||||
size_overflow = group_uncompressed + glyph_aligned_size > GROUP_MAX_UNCOMPRESSED_BYTES
|
||||
|
||||
if sg != current_group_id or size_overflow:
|
||||
if group_count > 0:
|
||||
groups.append((group_start, group_count))
|
||||
current_group_id = sg
|
||||
group_start = i
|
||||
group_count = 1
|
||||
group_uncompressed = glyph_aligned_size
|
||||
else:
|
||||
group_count += 1
|
||||
group_uncompressed += glyph_aligned_size
|
||||
|
||||
if group_count > 0:
|
||||
groups.append((group_start, group_count))
|
||||
|
||||
@@ -26,6 +26,7 @@ import freetype
|
||||
import struct
|
||||
import sys
|
||||
import os
|
||||
import re
|
||||
import math
|
||||
import argparse
|
||||
from collections import namedtuple
|
||||
@@ -75,17 +76,39 @@ INTERVAL_PRESETS = {
|
||||
(0xFB00, 0xFB06)],
|
||||
}
|
||||
|
||||
# Regex for parsing unnamed hex range intervals: (0xSTART-0xEND)
|
||||
_HEX_RANGE_PATTERN = re.compile(r'^\(0x([0-9a-fA-F]+)-0x([0-9a-fA-F]+)\)$')
|
||||
|
||||
def parse_hex_range(s: str) -> tuple[int, int] | None:
|
||||
match = _HEX_RANGE_PATTERN.fullmatch(s)
|
||||
if not match:
|
||||
return None
|
||||
|
||||
start_hex, end_hex = match.groups()
|
||||
start, end = int(start_hex, 16), int(end_hex, 16)
|
||||
|
||||
# Validating Unicode range bounds.
|
||||
if start > end or end > 0x10FFFF:
|
||||
return None
|
||||
return start, end
|
||||
|
||||
|
||||
def resolve_intervals(preset_str):
|
||||
"""Resolve comma-separated preset names into a merged, sorted, deduplicated interval list."""
|
||||
all_intervals = []
|
||||
for name in preset_str.split(","):
|
||||
name = name.strip().lower()
|
||||
if name not in INTERVAL_PRESETS:
|
||||
unnamed_interval = parse_hex_range(name)
|
||||
if name not in INTERVAL_PRESETS and unnamed_interval is None:
|
||||
print(f"Error: unknown interval preset '{name}'", file=sys.stderr)
|
||||
print(f"Available presets: {', '.join(sorted(INTERVAL_PRESETS.keys()))}", file=sys.stderr)
|
||||
print("You can also specify unnamed hex ranges like (0x2100-0x214F)", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
all_intervals.extend(INTERVAL_PRESETS[name])
|
||||
|
||||
if unnamed_interval is not None:
|
||||
all_intervals.append(unnamed_interval)
|
||||
else:
|
||||
all_intervals.extend(INTERVAL_PRESETS[name])
|
||||
|
||||
# Always add replacement character
|
||||
all_intervals.append((0xFFFD, 0xFFFD))
|
||||
@@ -266,11 +289,29 @@ def extract_kerning_fonttools(font_path, codepoints, ppem):
|
||||
lookup = gpos.LookupList.Lookup[li]
|
||||
for st in lookup.SubTable:
|
||||
actual = st
|
||||
# Unwrap Extension (lookup type 9) wrappers
|
||||
# Unwrap Extension (lookup type 9) wrappers. After unwrapping,
|
||||
# `lookup.LookupType` is still 9, so we must look at the
|
||||
# *effective* type carried on the extension subtable to know
|
||||
# whether `actual` is a PairPos table.
|
||||
if lookup.LookupType == 9 and hasattr(st, 'ExtSubTable'):
|
||||
actual = st.ExtSubTable
|
||||
effective_type = getattr(st, 'ExtensionLookupType', lookup.LookupType)
|
||||
if hasattr(actual, 'Format'):
|
||||
_extract_pairpos_subtable(actual, glyph_to_cp, raw_kern)
|
||||
# _extract_pairpos_subtable assumes a Type-2 (PairPos)
|
||||
# subtable. Other lookup types reachable through the kern
|
||||
# feature (cursive attachment, mark-to-mark, contextual,
|
||||
# etc.) have a different shape and crash inside the
|
||||
# extractor. Skip them with a debug note rather than
|
||||
# aborting the whole build. Modern fonts often ship kern
|
||||
# via Extension-wrapped PairPos, so checking the effective
|
||||
# type instead of the outer type is what makes those
|
||||
# lookups actually reach the extractor.
|
||||
if effective_type == 2:
|
||||
_extract_pairpos_subtable(actual, glyph_to_cp, raw_kern)
|
||||
else:
|
||||
print(f" Debug: skipping unsupported GPOS kern lookupType="
|
||||
f"{effective_type} (outer={lookup.LookupType}, Format={actual.Format})",
|
||||
file=sys.stderr)
|
||||
|
||||
font.close()
|
||||
|
||||
@@ -425,11 +466,20 @@ def extract_ligatures_fonttools(font_path, codepoints):
|
||||
font.close()
|
||||
|
||||
# Filter: only keep ligatures where all input and output codepoints are
|
||||
# in our generated glyph set
|
||||
# in our generated glyph set, and all codepoints fit in 16 bits.
|
||||
#
|
||||
# The on-disk format packs each component as a uint16 (the 3+ chained
|
||||
# path packs `intermediate_cp << 16 | last_cp`, where `intermediate_cp`
|
||||
# is the lig_cp of the prefix). Dropping any seq with an SMP cp here —
|
||||
# plus any lig_cp > 0xFFFF — means every cp that reaches `packed = … <<
|
||||
# 16 | …` below is already 16-bit safe, including the chained path
|
||||
# (intermediate_cp = filtered[prefix] is filtered too).
|
||||
codepoints_set = set(codepoints)
|
||||
filtered = {}
|
||||
for seq, lig_cp in raw_ligatures.items():
|
||||
if lig_cp not in codepoints_set:
|
||||
if lig_cp not in codepoints_set or lig_cp > 0xFFFF:
|
||||
continue
|
||||
if any(cp > 0xFFFF for cp in seq):
|
||||
continue
|
||||
if all(cp in codepoints_set for cp in seq):
|
||||
filtered[seq] = lig_cp
|
||||
@@ -468,6 +518,12 @@ def rasterize_font_style(fontfile, size, intervals, style_id=0, force_autohint=F
|
||||
style_label = style_names.get(style_id, str(style_id))
|
||||
|
||||
face = freetype.Face(fontfile)
|
||||
# Set font size at 150 DPI (matching fontconvert.py) BEFORE any glyph load.
|
||||
# load_glyph() with FT_LOAD_RENDER renders at the active size, so calling
|
||||
# it before set_char_size() would waste work at the default size and risk
|
||||
# Invalid_Size_Handle on some fonts.
|
||||
face.set_char_size(size << 6, size << 6, 150, 150)
|
||||
|
||||
load_flags = freetype.FT_LOAD_RENDER
|
||||
if force_autohint:
|
||||
load_flags |= freetype.FT_LOAD_FORCE_AUTOHINT
|
||||
@@ -497,9 +553,6 @@ def rasterize_font_style(fontfile, size, intervals, style_id=0, force_autohint=F
|
||||
total_glyphs = sum(end - start + 1 for start, end in intervals)
|
||||
print(f" [{style_label}] Validated: {len(intervals)} intervals, {total_glyphs} glyphs", file=sys.stderr)
|
||||
|
||||
# Set font size at 150 DPI (matching fontconvert.py)
|
||||
face.set_char_size(size << 6, size << 6, 150, 150)
|
||||
|
||||
# Rasterize all glyphs
|
||||
total_bitmap_size = 0
|
||||
all_glyphs = []
|
||||
@@ -514,18 +567,28 @@ def rasterize_font_style(fontfile, size, intervals, style_id=0, force_autohint=F
|
||||
|
||||
bitmap = f.glyph.bitmap
|
||||
|
||||
# Build 4-bit greyscale bitmap (same logic as fontconvert.py)
|
||||
# Build 4-bit greyscale bitmap (same logic as fontconvert.py).
|
||||
#
|
||||
# FreeType returns the buffer with bitmap.pitch as the row stride
|
||||
# in bytes, which can be negative when the bitmap is stored
|
||||
# bottom-up. Iterating bitmap.buffer linearly assumes
|
||||
# pitch == width and a top-down layout — that holds in the common
|
||||
# case but breaks on padded or flipped bitmaps and corrupts the
|
||||
# output. Walk by (row, col) using the real pitch instead.
|
||||
pixels4g = []
|
||||
px = 0
|
||||
for i, v in enumerate(bitmap.buffer):
|
||||
x = i % bitmap.width
|
||||
if x % 2 == 0:
|
||||
px = (v >> 4)
|
||||
else:
|
||||
px = px | (v & 0xF0)
|
||||
pixels4g.append(px)
|
||||
px = 0
|
||||
if x == bitmap.width - 1 and bitmap.width % 2 > 0:
|
||||
abs_pitch = abs(bitmap.pitch)
|
||||
for y in range(bitmap.rows):
|
||||
row_offset = y * abs_pitch if bitmap.pitch >= 0 else (bitmap.rows - 1 - y) * abs_pitch
|
||||
for x in range(bitmap.width):
|
||||
v = bitmap.buffer[row_offset + x]
|
||||
if x % 2 == 0:
|
||||
px = (v >> 4)
|
||||
else:
|
||||
px = px | (v & 0xF0)
|
||||
pixels4g.append(px)
|
||||
px = 0
|
||||
if bitmap.width % 2 > 0:
|
||||
pixels4g.append(px)
|
||||
px = 0
|
||||
|
||||
@@ -550,7 +613,12 @@ def rasterize_font_style(fontfile, size, intervals, style_id=0, force_autohint=F
|
||||
pixels2b.append(px)
|
||||
px = 0
|
||||
if (bitmap.width * bitmap.rows) % 4 != 0:
|
||||
px = px << (4 - (bitmap.width * bitmap.rows) % 4) * 2
|
||||
# Outer parens are for clarity: in Python `*` binds tighter
|
||||
# than `<<`, so the original `px << (4 - … % 4) * 2` already
|
||||
# evaluates as `px << ((4 - … % 4) * 2)`. Match the explicit
|
||||
# bracketing here so the shift width is obvious at a glance,
|
||||
# mirroring the inner-loop style in fontconvert.py.
|
||||
px = px << ((4 - (bitmap.width * bitmap.rows) % 4) * 2)
|
||||
pixels2b.append(px)
|
||||
|
||||
packed = bytes(pixels2b)
|
||||
@@ -582,6 +650,10 @@ def rasterize_font_style(fontfile, size, intervals, style_id=0, force_autohint=F
|
||||
all_cps = set(g.code_point for g, _ in all_glyphs)
|
||||
|
||||
kern_map = extract_kerning_fonttools(fontfile, all_cps, ppem)
|
||||
# SMP codepoints (> U+FFFF) cannot be stored in the uint16 kern codepoint
|
||||
# field; drop them before class derivation to avoid a downstream
|
||||
# struct.error when packing the binary kern tables.
|
||||
kern_map = {(lcp, rcp): v for (lcp, rcp), v in kern_map.items() if lcp <= 0xFFFF and rcp <= 0xFFFF}
|
||||
print(f" [{style_label}] Kerning: {len(kern_map)} pairs extracted", file=sys.stderr)
|
||||
|
||||
(kern_left_classes, kern_right_classes, kern_matrix,
|
||||
@@ -593,6 +665,9 @@ def rasterize_font_style(fontfile, size, intervals, style_id=0, force_autohint=F
|
||||
print(f" [{style_label}] Kerning classes: {kern_left_class_count} left, {kern_right_class_count} right, "
|
||||
f"{matrix_size + entries_size} bytes", file=sys.stderr)
|
||||
|
||||
# SMP codepoints in ligature inputs / outputs are filtered inside
|
||||
# extract_ligatures_fonttools (see the codepoints_set filter), so every
|
||||
# entry returned here is already 16-bit safe.
|
||||
ligature_pairs = extract_ligatures_fonttools(fontfile, all_cps)
|
||||
if len(ligature_pairs) > 255:
|
||||
print(f" [{style_label}] WARNING: {len(ligature_pairs)} ligature pairs exceeds uint8_t max (255), truncating",
|
||||
@@ -895,4 +970,4 @@ def main():
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
main()
|
||||
|
||||
@@ -108,9 +108,9 @@ families:
|
||||
intervals: latin-ext,cyrillic
|
||||
sizes: [12, 14, 16, 18]
|
||||
styles:
|
||||
regular: {url: "https://raw.githubusercontent.com/google/fonts/main/ofl/bitter/Bitter%5Bwght%5D.ttf", variable: {wght: 400}}
|
||||
regular: {url: "https://raw.githubusercontent.com/google/fonts/main/ofl/bitter/Bitter%5Bwght%5D.ttf", variable: {wght: 500}}
|
||||
bold: {url: "https://raw.githubusercontent.com/google/fonts/main/ofl/bitter/Bitter%5Bwght%5D.ttf", variable: {wght: 700}}
|
||||
italic: {url: "https://raw.githubusercontent.com/google/fonts/main/ofl/bitter/Bitter-Italic%5Bwght%5D.ttf", variable: {wght: 400}}
|
||||
italic: {url: "https://raw.githubusercontent.com/google/fonts/main/ofl/bitter/Bitter-Italic%5Bwght%5D.ttf", variable: {wght: 500}}
|
||||
bolditalic: {url: "https://raw.githubusercontent.com/google/fonts/main/ofl/bitter/Bitter-Italic%5Bwght%5D.ttf", variable: {wght: 700}}
|
||||
|
||||
# ── Sans-serif ─────────────────────────────────────────────────────────
|
||||
|
||||
@@ -38,7 +38,7 @@ STR_JOIN_NETWORK: "Dołącz do sieci"
|
||||
STR_CREATE_HOTSPOT: "Stwórz Hotspot"
|
||||
STR_JOIN_DESC: "Podłącz do istniejącej sieci WiFi"
|
||||
STR_HOTSPOT_DESC: "Stwórz sieć WiFi do której podłączyć mogą się inni"
|
||||
STR_STARTING_HOTSPOT: "Startowanie Hotspot'a..."
|
||||
STR_STARTING_HOTSPOT: "Startowanie Hotspota..."
|
||||
STR_HOTSPOT_MODE: "Tryb Hotspot"
|
||||
STR_CONNECT_WIFI_HINT: "Podłącz swoje urządzenie do tej sieci WiFi"
|
||||
STR_OPEN_URL_HINT: "Otwórz ten URL w przeglądarce"
|
||||
@@ -107,7 +107,7 @@ STR_KOREADER_AUTH: "KOReader Auth"
|
||||
STR_SYNC_READY: "KOReader sync gotowy"
|
||||
STR_AUTH_FAILED: "Błąd uwierzytelniania"
|
||||
STR_DONE: "Zrobione"
|
||||
STR_CLEAR_CACHE_WARNING_1: "To wyczyści całą pamięc podręczną książek."
|
||||
STR_CLEAR_CACHE_WARNING_1: "To wyczyści całą pamięć podręczną książek."
|
||||
STR_CLEAR_CACHE_WARNING_2: "Cały postęp czytania zostanie stracony!"
|
||||
STR_CLEAR_CACHE_WARNING_3: "Książki trzeba będzie ponownie indeksować"
|
||||
STR_CLEAR_CACHE_WARNING_4: "po ponownym otwarciu."
|
||||
@@ -130,6 +130,7 @@ STR_ALWAYS: "Zawsze"
|
||||
STR_IGNORE: "Ignoruj"
|
||||
STR_SLEEP: "Uśpienie"
|
||||
STR_PAGE_TURN: "Nast. str."
|
||||
STR_FORCE_REFRESH: "Odśwież ekran"
|
||||
STR_PORTRAIT: "Pionowo"
|
||||
STR_LANDSCAPE_CW: "Poziomo P"
|
||||
STR_INVERTED: "Odwrócony"
|
||||
@@ -170,6 +171,7 @@ STR_NO_UPDATE: "Brak aktualizacji"
|
||||
STR_UPDATE_FAILED: "Aktualizacja nieudana"
|
||||
STR_UPDATE_COMPLETE: "Aktualizacja zakończona"
|
||||
STR_POWER_ON_HINT: "Przyciśnij i przytrzymaj przycisk zasilania aby włączyć ponownie"
|
||||
STR_RESTARTING_HINT: "Restartowanie... Jeśli urządzenie się nie zrestartuje, przytrzymaj przycisk zasilania przez kilka sekund."
|
||||
STR_NO_ENTRIES: "Brak wpisów"
|
||||
STR_DOWNLOADING: "Pobieranie..."
|
||||
STR_DOWNLOAD_FAILED: "Błąd pobierania"
|
||||
@@ -178,6 +180,8 @@ STR_UNNAMED: "Nienazwany"
|
||||
STR_NO_SERVER_URL: "Brak skonfigurowanego serwera URL"
|
||||
STR_FETCH_FEED_FAILED: "Nie udało się pobrać kanału"
|
||||
STR_PARSE_FEED_FAILED: "Nie udało się przeanalizować kanału"
|
||||
STR_NEXT_PAGE: "Następna strona »"
|
||||
STR_PREV_PAGE: "« Poprzednia strona"
|
||||
STR_NETWORK_PREFIX: "Sieć: "
|
||||
STR_IP_ADDRESS_PREFIX: "Adres IP: "
|
||||
STR_ERROR_GENERAL_FAILURE: "Błąd: Ogólny"
|
||||
@@ -225,13 +229,18 @@ STR_EXAMPLE_BOOK: "Tytuł książki"
|
||||
STR_PREVIEW: "Podgląd"
|
||||
STR_TITLE: "Tytuł"
|
||||
STR_BATTERY: "Bateria"
|
||||
STR_XTC_STATUS_BAR: "Pasek statusu XTC"
|
||||
STR_BOTTOM: "Dół"
|
||||
STR_TOP: "Góra"
|
||||
STR_UI_THEME: "Skórka UI"
|
||||
STR_THEME_CLASSIC: "Classic"
|
||||
STR_THEME_LYRA: "Lyra"
|
||||
STR_THEME_ROUNDEDRAFF: "RoundedRaff"
|
||||
STR_THEME_LYRA_EXTENDED: "Lyra Extended"
|
||||
STR_SUNLIGHT_FADING_FIX: "Przeciwdziałanie blaknięciu od słońca"
|
||||
STR_REMAP_FRONT_BUTTONS: "Skonfiguruj przednie przyciski"
|
||||
STR_OPDS_BROWSER: "OPDS Browser"
|
||||
STR_SEARCH: "Szukaj"
|
||||
STR_COVER_CUSTOM: "Okładka + Własne"
|
||||
STR_MENU_RECENT_BOOKS: "Ostatnio czytane"
|
||||
STR_NO_RECENT_BOOKS: "Brak ostatnio czytanych"
|
||||
@@ -288,10 +297,64 @@ STR_UPLOAD: "Wyślij"
|
||||
STR_BOOK_S_STYLE: "Styl książki"
|
||||
STR_EMBEDDED_STYLE: "Style wbudowane w EPUB"
|
||||
STR_OPDS_SERVER_URL: "URL serwera OPDS"
|
||||
STR_SET_SLEEP_COVER: "Ustaw okładkę"
|
||||
STR_FOOTNOTES: "Przypisy"
|
||||
STR_NO_FOOTNOTES: "Brak przypisów na tej stronie"
|
||||
STR_LINK: "[link]"
|
||||
STR_SCREENSHOT_BUTTON: "Zrób zrzut ekranu"
|
||||
STR_ADD_SERVER: "Dodaj serwer"
|
||||
STR_SERVER_NAME: "Nazwa serwera"
|
||||
STR_NO_SERVERS: "Brak skonfigurowanych serwerów OPDS"
|
||||
STR_DELETE_SERVER: "Usuń serwer"
|
||||
STR_DELETE_CONFIRM: "Usunąć ten serwer?"
|
||||
STR_OPDS_SERVERS: "Serwery OPDS"
|
||||
STR_AUTO_TURN_ENABLED: "Auto-kartkowanie: "
|
||||
STR_AUTO_TURN_PAGES_PER_MIN: "Auto-kartkowanie (str./min)"
|
||||
STR_DOWNLOAD_FONTS: "Pobierz czcionki"
|
||||
STR_FONT_DOWNLOAD: "Pobierz czcionkę"
|
||||
STR_LOADING_FONT_LIST: "Ładowanie listy czcionek..."
|
||||
STR_NO_FONTS_AVAILABLE: "Brak dostępnych czcionek"
|
||||
STR_FONT_INSTALLED: "Czcionka zainstalowana!"
|
||||
STR_FONT_INSTALL_FAILED: "Instalacja czcionki nieudana"
|
||||
STR_INSTALLED: "Zainstalowany"
|
||||
STR_CONFIRM_DOWNLOAD_PROMPT: "Pobrać?"
|
||||
STR_SD_CARD_FULL: "Za mało pamięci na karcie SD"
|
||||
STR_FILES_LABEL: "Pliki: "
|
||||
STR_SIZE_LABEL: "Rozmiar: "
|
||||
STR_REDOWNLOAD: "Re-download"
|
||||
STR_DOWNLOAD_ALL: "Pobierz / Uaktualnij wszystkie"
|
||||
STR_ALL_FONTS_INSTALLED: "Wszystkie czcionki zainstalowane!"
|
||||
STR_UPDATE_AVAILABLE: "Uaktualnij"
|
||||
STR_CRASH_TITLE: "Awaria systemu"
|
||||
STR_CRASH_DESCRIPTION: "Szczegółowy raport zapisany do crash_report.txt. Prosimy o dołączenie tego pliku do zgłoszenia błędu."
|
||||
STR_CRASH_REASON: "Powód awarii:"
|
||||
STR_CRASH_NO_REASON: "(Nie odnotowano powodu)"
|
||||
STR_TILT_PAGE_TURN: "Kartkowanie przechyłem"
|
||||
STR_KB_HINT_MOVE_CURSOR: "Naciśnij LEWO lub PRAWO aby poruszyć kursor"
|
||||
STR_KB_HINT_RETURN_CURSOR: "Naciśnij LEWO aby powrócić do pozycji kursora"
|
||||
STR_KB_HINT_HIDE_PASSWORD: "Przytrzymaj PRAWO potem naciśnij [***] aby ukryć hasło"
|
||||
STR_KB_HINT_SHOW_PASSWORD: "Przytrzymaj PRAWO potem naciśnij [abc] aby pokazać hasło"
|
||||
STR_KB_HINT_TOGGLE_HIDE_PASSWORD: "Naciśnij [***] aby ukryć hasło"
|
||||
STR_KB_HINT_TOGGLE_SHOW_PASSWORD: "Naciśnij [abc] aby pokazać hasło"
|
||||
STR_KB_HINT_EDIT_ENTRY: "Przytrzymaj GÓRA aby edytować wpis"
|
||||
STR_KB_TIPS: "Porady:"
|
||||
STR_KB_HINT_RETURN_KEYBOARD: "Naciśnij DÓŁ aby powrócić do klawiatury"
|
||||
STR_KB_HINT_EXIT_URL_MODE: "Naciśnij ABC aby wyjść z trybu URL"
|
||||
STR_KB_HINT_CLEAR_TEXT: "Przytrzymaj 'Backspace' aby wyczyścić cały tekst"
|
||||
STR_KB_HINT_SECONDARY_CHAR: "Przytrz. 'Wybierz' dla znaku drugorzędnego"
|
||||
STR_KB_HINT_UPPER_SECONDARY: "Przytrz. 'Wybierz' dla W. LITERY lub znaku drugorzędnego"
|
||||
STR_KB_HINT_LOWER_SECONDARY: "Przytrz. 'Wybierz' dla m. litery lub znaku drugorzędnego"
|
||||
STR_KB_HINT_URL_SNIPPETS: "Naciśnij URL dla adresów"
|
||||
STR_SD_FIRMWARE_UPDATE: "Uaktualnienie oprogramowania z karty SD"
|
||||
STR_SELECT_FIRMWARE_FILE: "Wybierz plik z oprogramowaniem (.bin)"
|
||||
STR_NO_BIN_FILES: "Nie znaleziono plików .bin"
|
||||
STR_VALIDATING_FIRMWARE: "Sprawdzanie oprogramowania..."
|
||||
STR_INVALID_FIRMWARE: "Nieprawidłowy plik oprogramowania"
|
||||
STR_FIRMWARE_TOO_LARGE: "Oprogramowanie za duże na partycję"
|
||||
STR_FIRMWARE_TOO_SMALL: "Plik oprogramowania za mały"
|
||||
STR_FIRMWARE_UPDATE_PROMPT: "Uaktualnić oprogramowanie?"
|
||||
STR_FIRMWARE_FILE_OPEN_FAILED: "Nie można otworzyć pliku"
|
||||
STR_FIRMWARE_WRITE_FAILED: "Zapis oprogramowania nieudany"
|
||||
STR_FIRMWARE_UPDATE_DO_NOT_POWER_OFF: "Nie wyłączać!"
|
||||
STR_RECOVERY_MODE: "Tryb przywracania"
|
||||
STR_RECOVERY_MODE_HINT: "Umieść firmware.bin w głównym katalogu karty SD i wybierz go"
|
||||
|
||||
@@ -229,6 +229,9 @@ STR_EXAMPLE_BOOK: "Boktitel"
|
||||
STR_PREVIEW: "Förhandsgranskning"
|
||||
STR_TITLE: "Titel"
|
||||
STR_BATTERY: "Batteri"
|
||||
STR_XTC_STATUS_BAR: "XTC-statusfält"
|
||||
STR_BOTTOM: "Botten"
|
||||
STR_TOP: "Överst"
|
||||
STR_UI_THEME: "Användargränssnittstema"
|
||||
STR_THEME_CLASSIC: "Klassisk"
|
||||
STR_THEME_LYRA: "Lyra"
|
||||
@@ -294,6 +297,7 @@ STR_UPLOAD: "Uppladdning"
|
||||
STR_BOOK_S_STYLE: "Bokstil"
|
||||
STR_EMBEDDED_STYLE: "Inbäddad stil"
|
||||
STR_OPDS_SERVER_URL: "OPDS-serveradress"
|
||||
STR_SET_SLEEP_COVER: "Ställ in omslag"
|
||||
STR_FOOTNOTES: "Fotnoter"
|
||||
STR_NO_FOOTNOTES: "Inga fotnoter på den här sidan"
|
||||
STR_LINK: "[länk]"
|
||||
@@ -306,6 +310,21 @@ STR_DELETE_CONFIRM: "Vill du ta bort den här servern?"
|
||||
STR_OPDS_SERVERS: "OPDS-servrar"
|
||||
STR_AUTO_TURN_ENABLED: "Automatisk vändning aktiverad: "
|
||||
STR_AUTO_TURN_PAGES_PER_MIN: "Automatisk vändning (sidor per minut)"
|
||||
STR_DOWNLOAD_FONTS: "Ladda ner teckensnitt"
|
||||
STR_FONT_DOWNLOAD: "Nedladdning av teckensnitt"
|
||||
STR_LOADING_FONT_LIST: "Laddar teckensnittslista..."
|
||||
STR_NO_FONTS_AVAILABLE: "Inga teckensnitt tillgängliga"
|
||||
STR_FONT_INSTALLED: "Teckensnitt installerat!"
|
||||
STR_FONT_INSTALL_FAILED: "Installationen av teckensnitt misslyckades"
|
||||
STR_INSTALLED: "Installerad"
|
||||
STR_CONFIRM_DOWNLOAD_PROMPT: "Nedladdning?"
|
||||
STR_SD_CARD_FULL: "Otillräckligt utrymme på SD-kortet"
|
||||
STR_FILES_LABEL: "Filer: "
|
||||
STR_SIZE_LABEL: "Storlek: "
|
||||
STR_REDOWNLOAD: "Ladda ner igen"
|
||||
STR_DOWNLOAD_ALL: "Ladda ner / Uppdatera alla"
|
||||
STR_ALL_FONTS_INSTALLED: "Alla teckensnitt installerade!"
|
||||
STR_UPDATE_AVAILABLE: "Uppdatering"
|
||||
STR_CRASH_TITLE: "Systemkrasch"
|
||||
STR_CRASH_DESCRIPTION: "En detaljerad rapport sparades till crash_report.txt. Vänligen inkludera den här filen i din felrapport."
|
||||
STR_CRASH_REASON: "Orsak till kraschen:"
|
||||
|
||||
@@ -21,6 +21,7 @@ import json
|
||||
import os
|
||||
import struct
|
||||
import sys
|
||||
import zlib
|
||||
from pathlib import Path
|
||||
|
||||
# Import canonical version constants from the shared file in lib/EpdFont/scripts/
|
||||
@@ -115,6 +116,15 @@ def parse_filename(filename: str) -> tuple[str, str] | None:
|
||||
return family, size_str
|
||||
|
||||
|
||||
def compute_crc32(filepath: Path) -> int:
|
||||
"""Compute CRC32 of a file, matching esp_rom_crc32_le(0xFFFFFFFF, ...) ^ 0xFFFFFFFF."""
|
||||
crc = 0
|
||||
with open(filepath, "rb") as f:
|
||||
for chunk in iter(lambda: f.read(65536), b""):
|
||||
crc = zlib.crc32(chunk, crc)
|
||||
return crc & 0xFFFFFFFF
|
||||
|
||||
|
||||
def scan_cpfont_files(input_dir: Path) -> dict[str, list[Path]]:
|
||||
"""Scan input directory for .cpfont files, grouped by family name.
|
||||
|
||||
@@ -164,6 +174,7 @@ def build_manifest(
|
||||
{
|
||||
"name": filepath.name,
|
||||
"size": filepath.stat().st_size,
|
||||
"crc32": compute_crc32(filepath),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
+17
-14
@@ -5,10 +5,15 @@
|
||||
|
||||
#include "CrossPointSettings.h"
|
||||
|
||||
static uint8_t fontSizeEnumFromSettings() {
|
||||
// Map fontSize enum (SMALL=0, MEDIUM=1, LARGE=2, EXTRA_LARGE=3) to the point
|
||||
// sizes shipped with the built-in fonts. Used to drive closest-pt selection
|
||||
// in the SD card font registry (see SdCardFontFamilyInfo::pickClosestSize).
|
||||
static constexpr uint8_t FONT_SIZE_TO_PT[CrossPointSettings::FONT_SIZE_COUNT] = {12, 14, 16, 18};
|
||||
|
||||
static uint8_t targetPtSizeFromSettings() {
|
||||
uint8_t e = SETTINGS.fontSize;
|
||||
if (e >= CrossPointSettings::FONT_SIZE_COUNT) e = 1; // default to MEDIUM
|
||||
return e;
|
||||
return FONT_SIZE_TO_PT[e];
|
||||
}
|
||||
|
||||
void SdCardFontSystem::begin(GfxRenderer& renderer) {
|
||||
@@ -25,7 +30,7 @@ void SdCardFontSystem::begin(GfxRenderer& renderer) {
|
||||
if (SETTINGS.sdFontFamilyName[0] != '\0') {
|
||||
const auto* family = registry_.findFamily(SETTINGS.sdFontFamilyName);
|
||||
if (family) {
|
||||
if (manager_.loadFamily(*family, renderer, fontSizeEnumFromSettings())) {
|
||||
if (manager_.loadFamily(*family, renderer, targetPtSizeFromSettings())) {
|
||||
LOG_DBG("SDFS", "Loaded SD card font family: %s", SETTINGS.sdFontFamilyName);
|
||||
} else {
|
||||
LOG_ERR("SDFS", "Failed to load SD font family: %s (clearing)", SETTINGS.sdFontFamilyName);
|
||||
@@ -53,7 +58,7 @@ void SdCardFontSystem::ensureLoaded(GfxRenderer& renderer) {
|
||||
|
||||
const char* wantedFamily = SETTINGS.sdFontFamilyName;
|
||||
const std::string& currentFamily = manager_.currentFamilyName();
|
||||
const uint8_t sizeEnum = fontSizeEnumFromSettings();
|
||||
const uint8_t targetPt = targetPtSizeFromSettings();
|
||||
|
||||
if (wantedFamily[0] == '\0') {
|
||||
if (!currentFamily.empty()) {
|
||||
@@ -62,8 +67,8 @@ void SdCardFontSystem::ensureLoaded(GfxRenderer& renderer) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Reload if family changed OR if the user-selected size maps to a
|
||||
// different file than what's currently loaded OR if the registry was
|
||||
// Reload if family changed OR if the user-selected size now resolves to a
|
||||
// different on-disk file than what's currently loaded OR if the registry was
|
||||
// just rediscovered (file may have been replaced on disk).
|
||||
bool familyMatches = (currentFamily == wantedFamily);
|
||||
if (familyMatches) {
|
||||
@@ -74,13 +79,11 @@ void SdCardFontSystem::ensureLoaded(GfxRenderer& renderer) {
|
||||
SETTINGS.sdFontFamilyName[0] = '\0';
|
||||
return;
|
||||
}
|
||||
auto sizes = family->availableSizes();
|
||||
uint8_t idx = sizeEnum;
|
||||
if (idx >= sizes.size()) idx = sizes.size() - 1;
|
||||
uint8_t wantedPt = sizes.empty() ? 0 : sizes[idx];
|
||||
if (!registryWasDirty && wantedPt == manager_.currentPointSize()) return;
|
||||
LOG_DBG("SDFS", "Reloading %s: size %u -> %u (enum %u)%s", wantedFamily, manager_.currentPointSize(), wantedPt,
|
||||
sizeEnum, registryWasDirty ? " [registry dirty]" : "");
|
||||
const auto* best = family->pickClosestSize(targetPt);
|
||||
const uint8_t bestPt = best ? best->pointSize : 0;
|
||||
if (!registryWasDirty && bestPt == manager_.currentPointSize()) return;
|
||||
LOG_DBG("SDFS", "Reloading %s: size %u -> %u (target %u)%s", wantedFamily, manager_.currentPointSize(), bestPt,
|
||||
targetPt, registryWasDirty ? " [registry dirty]" : "");
|
||||
}
|
||||
|
||||
if (!currentFamily.empty()) {
|
||||
@@ -89,7 +92,7 @@ void SdCardFontSystem::ensureLoaded(GfxRenderer& renderer) {
|
||||
|
||||
const auto* family = registry_.findFamily(wantedFamily);
|
||||
if (family) {
|
||||
if (manager_.loadFamily(*family, renderer, sizeEnum)) {
|
||||
if (manager_.loadFamily(*family, renderer, targetPt)) {
|
||||
LOG_DBG("SDFS", "Loaded SD font family: %s", wantedFamily);
|
||||
} else {
|
||||
LOG_ERR("SDFS", "Failed to load SD font family: %s (clearing)", wantedFamily);
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
#include <I18n.h>
|
||||
#include <Logging.h>
|
||||
#include <WiFi.h>
|
||||
#include <esp_rom_crc.h>
|
||||
|
||||
#include "MappedInputManager.h"
|
||||
#include "SdCardFontGlobals.h"
|
||||
@@ -123,6 +124,14 @@ bool FontDownloadActivity::fetchAndParseManifest() {
|
||||
ManifestFile file;
|
||||
file.name = fileObj["name"] | "";
|
||||
file.size = fileObj["size"] | 0;
|
||||
|
||||
if (!fileObj["crc32"].is<uint32_t>()) {
|
||||
LOG_ERR("FONT", "Malformed manifest file entry: missing or invalid crc32 for %s", file.name.c_str());
|
||||
errorMessage_ = "Invalid font manifest";
|
||||
return false;
|
||||
}
|
||||
file.crc32 = fileObj["crc32"].as<uint32_t>();
|
||||
|
||||
family.totalSize += file.size;
|
||||
family.files.push_back(std::move(file));
|
||||
}
|
||||
@@ -181,6 +190,24 @@ size_t FontDownloadActivity::totalUninstalledSize() const {
|
||||
return total;
|
||||
}
|
||||
|
||||
// Standard CRC32 matching zlib/Python zlib.crc32().
|
||||
bool FontDownloadActivity::computeFileCrc32(const char* path, uint32_t& outCrc) {
|
||||
FsFile f;
|
||||
if (!Storage.openFileForRead("FONT", path, f)) {
|
||||
return false;
|
||||
}
|
||||
constexpr size_t BUF_SIZE = 128;
|
||||
uint8_t buf[BUF_SIZE];
|
||||
uint32_t crc = 0;
|
||||
while (f.available()) {
|
||||
const int n = f.read(buf, BUF_SIZE);
|
||||
if (n <= 0) break;
|
||||
crc = esp_rom_crc32_le(crc, buf, static_cast<uint32_t>(n));
|
||||
}
|
||||
outCrc = crc;
|
||||
return true;
|
||||
}
|
||||
|
||||
void FontDownloadActivity::downloadFamily(ManifestFamily& family) {
|
||||
{
|
||||
RenderLock lock(*this);
|
||||
@@ -233,6 +260,29 @@ void FontDownloadActivity::downloadFamily(ManifestFamily& family) {
|
||||
return;
|
||||
}
|
||||
|
||||
uint32_t actualCrc = 0;
|
||||
if (!computeFileCrc32(destPath, actualCrc)) {
|
||||
LOG_ERR("FONT", "Failed to open file for CRC check: %s", destPath);
|
||||
fontInstaller_.deleteFamily(family.name.c_str());
|
||||
family.installed = false;
|
||||
family.hasUpdate = false;
|
||||
RenderLock lock(*this);
|
||||
state_ = ERROR;
|
||||
errorMessage_ = "Failed to compute checksum: " + file.name;
|
||||
return;
|
||||
}
|
||||
if (actualCrc != file.crc32) {
|
||||
LOG_ERR("FONT", "CRC32 mismatch for %s: got %08x expected %08x", file.name.c_str(), actualCrc, file.crc32);
|
||||
fontInstaller_.deleteFamily(family.name.c_str());
|
||||
family.installed = false;
|
||||
family.hasUpdate = false;
|
||||
RenderLock lock(*this);
|
||||
state_ = ERROR;
|
||||
errorMessage_ = "Checksum mismatch: " + file.name;
|
||||
return;
|
||||
}
|
||||
LOG_DBG("FONT", "Downloaded %s (size=%zu crc32=%08x)", file.name.c_str(), file.size, actualCrc);
|
||||
|
||||
if (!fontInstaller_.validateCpfontFile(destPath)) {
|
||||
LOG_ERR("FONT", "Invalid .cpfont: %s", destPath);
|
||||
fontInstaller_.deleteFamily(family.name.c_str());
|
||||
|
||||
@@ -50,6 +50,7 @@ class FontDownloadActivity : public Activity {
|
||||
struct ManifestFile {
|
||||
std::string name;
|
||||
size_t size = 0;
|
||||
uint32_t crc32 = 0;
|
||||
};
|
||||
|
||||
struct ManifestFamily {
|
||||
@@ -83,6 +84,7 @@ class FontDownloadActivity : public Activity {
|
||||
bool fetchAndParseManifest();
|
||||
void downloadFamily(ManifestFamily& family);
|
||||
void downloadAll();
|
||||
static bool computeFileCrc32(const char* path, uint32_t& outCrc);
|
||||
bool isDownloadAllSelected() const { return selectedIndex_ == 0 && !families_.empty(); }
|
||||
int familyIndexFromList(int listIndex) const { return listIndex - 1; }
|
||||
int listItemCount() const { return families_.empty() ? 0 : static_cast<int>(families_.size()) + 1; }
|
||||
|
||||
@@ -122,7 +122,7 @@ void RoundedRaffTheme::drawTabBar(const GfxRenderer& renderer, Rect rect, const
|
||||
}
|
||||
|
||||
// Full-width divider between tabs and setting rows.
|
||||
renderer.drawLine(rect.x, rect.y + rect.height - 1, rect.x + rect.width, rect.y + rect.height - 1, true);
|
||||
renderer.drawLine(rect.x, rect.y + rect.height - 1, rect.x + rect.width - 1, rect.y + rect.height - 1, true);
|
||||
}
|
||||
|
||||
void RoundedRaffTheme::drawRecentBookCover(GfxRenderer& renderer, Rect rect, const std::vector<RecentBook>& recentBooks,
|
||||
@@ -243,6 +243,77 @@ void RoundedRaffTheme::drawButtonMenu(GfxRenderer& renderer, Rect rect, int butt
|
||||
drawScrollBar(renderer, rect, buttonCount, pageStartIndex, pageItems);
|
||||
}
|
||||
|
||||
void RoundedRaffTheme::drawTextField(const GfxRenderer& renderer, Rect rect, const int textWidth, bool cursorMode,
|
||||
int contentStartX, int contentWidth) const {
|
||||
const auto& metrics = UITheme::getInstance().getMetrics();
|
||||
const int lineHeight = renderer.getLineHeight(UI_12_FONT_ID);
|
||||
const int lineY = rect.y + rect.height + lineHeight + metrics.verticalSpacing;
|
||||
const int thickness = cursorMode ? 3 : 2;
|
||||
|
||||
if (contentWidth > 0) {
|
||||
renderer.drawLine(rect.x + contentStartX, lineY, rect.x + contentStartX + contentWidth - 1, lineY, thickness, true);
|
||||
return;
|
||||
}
|
||||
|
||||
constexpr int hPadding = 8;
|
||||
const int lineW = textWidth + hPadding * 2;
|
||||
const int lineStart = rect.x + (rect.width - lineW) / 2;
|
||||
renderer.drawLine(lineStart, lineY, lineStart + lineW - 1, lineY, thickness, true);
|
||||
}
|
||||
|
||||
void RoundedRaffTheme::drawKeyboardKey(const GfxRenderer& renderer, Rect rect, const char* label, const bool isSelected,
|
||||
const char* secondaryLabel, const KeyboardKeyType keyType,
|
||||
const bool inactiveSelection) const {
|
||||
constexpr int keyRadius = 10;
|
||||
const bool disabled = keyType == KeyboardKeyType::Disabled;
|
||||
const bool invert = isSelected && !inactiveSelection;
|
||||
|
||||
if (isSelected) {
|
||||
const Color fillColor = (inactiveSelection || disabled) ? Color::LightGray : Color::Black;
|
||||
renderer.fillRoundedRect(rect.x, rect.y, rect.width, rect.height, keyRadius, fillColor);
|
||||
} else {
|
||||
if (disabled) {
|
||||
renderer.fillRoundedRect(rect.x, rect.y, rect.width, rect.height, keyRadius, Color::LightGray);
|
||||
} else {
|
||||
renderer.fillRoundedRect(rect.x, rect.y, rect.width, rect.height, keyRadius, Color::White);
|
||||
}
|
||||
renderer.drawRoundedRect(rect.x, rect.y, rect.width, rect.height, 1, keyRadius, true);
|
||||
}
|
||||
|
||||
if (keyType == KeyboardKeyType::Space) {
|
||||
const int lineHalfWidth = rect.width * 3 / 10;
|
||||
const int centerX = rect.x + rect.width / 2;
|
||||
const int lineY = rect.y + rect.height / 2 + 3;
|
||||
renderer.drawLine(centerX - lineHalfWidth, lineY, centerX + lineHalfWidth, lineY, 3, !invert);
|
||||
return;
|
||||
}
|
||||
|
||||
if (keyType == KeyboardKeyType::Del) {
|
||||
const int centerX = rect.x + rect.width / 2;
|
||||
const int centerY = rect.y + rect.height / 2;
|
||||
const int arrowLen = rect.width / 4;
|
||||
const int arrowHead = std::max(1, arrowLen / 2);
|
||||
renderer.drawLine(centerX - arrowLen / 2, centerY, centerX + arrowLen / 2, centerY, 3, !invert);
|
||||
renderer.drawLine(centerX - arrowLen / 2, centerY, centerX - arrowLen / 2 + arrowHead, centerY - arrowHead, 3,
|
||||
!invert);
|
||||
renderer.drawLine(centerX - arrowLen / 2, centerY, centerX - arrowLen / 2 + arrowHead, centerY + arrowHead, 3,
|
||||
!invert);
|
||||
return;
|
||||
}
|
||||
|
||||
if (label != nullptr && label[0] != '\0') {
|
||||
const int itemWidth = renderer.getTextWidth(UI_12_FONT_ID, label);
|
||||
const int textX = rect.x + (rect.width - itemWidth) / 2;
|
||||
const int textY = rect.y + (rect.height - renderer.getLineHeight(UI_12_FONT_ID)) / 2;
|
||||
renderer.drawText(UI_12_FONT_ID, textX, textY, label, !invert);
|
||||
}
|
||||
|
||||
if (secondaryLabel != nullptr && secondaryLabel[0] != '\0') {
|
||||
const int secWidth = renderer.getTextWidth(SMALL_FONT_ID, secondaryLabel);
|
||||
renderer.drawText(SMALL_FONT_ID, rect.x + rect.width - secWidth - 3, rect.y + 1, secondaryLabel, !invert);
|
||||
}
|
||||
}
|
||||
|
||||
void RoundedRaffTheme::drawList(const GfxRenderer& renderer, Rect rect, int itemCount, int selectedIndex,
|
||||
const std::function<std::string(int index)>& rowTitle,
|
||||
const std::function<std::string(int index)>& rowSubtitle,
|
||||
|
||||
@@ -36,8 +36,14 @@ constexpr ThemeMetrics values = {.batteryWidth = 15,
|
||||
.keyboardKeyWidth = 22,
|
||||
.keyboardKeyHeight = 30,
|
||||
.keyboardKeySpacing = 10,
|
||||
.keyboardBottomAligned = false,
|
||||
.keyboardCenteredText = false};
|
||||
.keyboardBottomKeyHeight = 30,
|
||||
.keyboardBottomKeySpacing = 5,
|
||||
.keyboardBottomAligned = true,
|
||||
.keyboardCenteredText = false,
|
||||
.keyboardVerticalOffset = 0,
|
||||
.keyboardTextFieldWidthPercent = 85,
|
||||
.keyboardWidthPercent = 90,
|
||||
.keyboardKeyCornerRadius = 0};
|
||||
}
|
||||
|
||||
class RoundedRaffTheme : public BaseTheme {
|
||||
@@ -52,6 +58,11 @@ class RoundedRaffTheme : public BaseTheme {
|
||||
void drawButtonMenu(GfxRenderer& renderer, Rect rect, int buttonCount, int selectedIndex,
|
||||
const std::function<std::string(int index)>& buttonLabel,
|
||||
const std::function<UIIcon(int index)>& rowIcon) const override;
|
||||
void drawTextField(const GfxRenderer& renderer, Rect rect, int textWidth, bool cursorMode = false,
|
||||
int contentStartX = 0, int contentWidth = 0) const override;
|
||||
void drawKeyboardKey(const GfxRenderer& renderer, Rect rect, const char* label, bool isSelected,
|
||||
const char* secondaryLabel = nullptr, KeyboardKeyType keyType = KeyboardKeyType::Normal,
|
||||
bool inactiveSelection = false) const override;
|
||||
void drawList(const GfxRenderer& renderer, Rect rect, int itemCount, int selectedIndex,
|
||||
const std::function<std::string(int index)>& rowTitle,
|
||||
const std::function<std::string(int index)>& rowSubtitle = nullptr,
|
||||
|
||||
Reference in New Issue
Block a user