diff --git a/.github/workflows/release-fonts.yml b/.github/workflows/release-fonts.yml
index 0fcf9874..c95c0927 100644
--- a/.github/workflows/release-fonts.yml
+++ b/.github/workflows/release-fonts.yml
@@ -40,7 +40,7 @@ jobs:
echo "metadata=$(python3 -c 'from cpfont_version import FONTS_MANIFEST_VERSION; print(FONTS_MANIFEST_VERSION)')" >> "$GITHUB_OUTPUT"
- name: Build SD card fonts
- run: python3 lib/EpdFont/scripts/build-sd-fonts.py --clean
+ run: python3 lib/EpdFont/scripts/build-sd-fonts.py --clean --verbose -j 1
- name: Flatten output for release assets
run: |
@@ -83,7 +83,7 @@ jobs:
--title "$TITLE" \
--notes "Pre-built \`.cpfont\` font files for CrossPoint Reader.
- Download individual files or use **Settings > System > Download Fonts** on the device.
+ Download individual files or use **Settings > System > Manage Fonts** on the device.
See [SD Card Fonts documentation](https://github.com/${{ github.repository }}/blob/main/docs/sd-card-fonts.md) for details."
@@ -103,4 +103,4 @@ jobs:
This is revision **${{ steps.tags.outputs.revision }}** — see [\`${{ steps.tags.outputs.versioned }}\`](https://github.com/${{ env.FONTS_REPO }}/releases/tag/${{ steps.tags.outputs.versioned }}) for the immutable copy.
- Download individual files or use **Settings > System > Download Fonts** on the device."
+ Download individual files or use **Settings > System > Manage fonts** on the device."
diff --git a/README.md b/README.md
index c8a0f3de..c870a156 100644
--- a/README.md
+++ b/README.md
@@ -45,7 +45,7 @@ This project is **not affiliated with Xteink**; it's built as a community projec
Multi-language support: Read EPUBs in various languages, including English, Spanish, French, German, Italian, Portuguese, Russian, Ukrainian, Polish, Swedish, Norwegian, [and more](./USER_GUIDE.md#supported-languages).
See [the user guide](./USER_GUIDE.md) for instructions on operating CrossPoint, including the
-[KOReader Sync quick setup](./USER_GUIDE.md#365-koreader-sync-quick-setup).
+[KOReader Sync quick setup](./USER_GUIDE.md#367-koreader-sync-quick-setup).
For more details about the scope of the project, see the [SCOPE.md](SCOPE.md) document.
diff --git a/docs/focus-reading.md b/docs/focus-reading.md
new file mode 100644
index 00000000..5063e4b4
--- /dev/null
+++ b/docs/focus-reading.md
@@ -0,0 +1,33 @@
+# Focus Reading
+
+Focus Reading is a reading aid that bolds the first portion of each word, guiding your eyes to natural fixation points and helping you read faster with less effort. Some readers — particularly those with ADHD — find it helps them stay engaged with the text and reduces mind-wandering. It is inspired by the Bionic Reading technique.
+
+
+
+*Left: Focus Reading off. Right: Focus Reading on. Both using Literata.*
+
+## Enabling Focus Reading
+
+1. Open **Settings > Reader**
+2. Toggle **Focus Reading** on
+
+Toggling the setting will trigger a re-index of your current book, the same as when changing font settings. Once indexing is complete, page turns proceed as normal. No changes are made to your EPUB files.
+
+## Examples
+
+
+
+*Focus Reading with Noto Serif font*
+
+
+
+*Focus Reading with Merriweather font*
+
+
+
+*Focus Reading with Atkinson Hyperlegible Next font*
+
+## Notes
+
+- Focus Reading only applies to regular body text. Already-bold text (headings, emphasis) is left unchanged.
+- The setting is per-device, not per-book — it applies to all books while enabled.
diff --git a/docs/hyphenation-trie-format.md b/docs/hyphenation-trie-format.md
index 54c12fa2..ebe54229 100644
--- a/docs/hyphenation-trie-format.md
+++ b/docs/hyphenation-trie-format.md
@@ -49,5 +49,5 @@ A convenient script `update_hyphenation.sh` is used to update all languages.
To use it, run:
```sh
-./scripts/update_hypenation.sh
+./scripts/update_hyphenation.sh
```
diff --git a/docs/images/focus-reading/focus-reading-atkinson.jpg b/docs/images/focus-reading/focus-reading-atkinson.jpg
new file mode 100644
index 00000000..8ec77b10
Binary files /dev/null and b/docs/images/focus-reading/focus-reading-atkinson.jpg differ
diff --git a/docs/images/focus-reading/focus-reading-merriweather.jpg b/docs/images/focus-reading/focus-reading-merriweather.jpg
new file mode 100644
index 00000000..59d82371
Binary files /dev/null and b/docs/images/focus-reading/focus-reading-merriweather.jpg differ
diff --git a/docs/images/focus-reading/focus-reading-notoserif.jpg b/docs/images/focus-reading/focus-reading-notoserif.jpg
new file mode 100644
index 00000000..8ffc869d
Binary files /dev/null and b/docs/images/focus-reading/focus-reading-notoserif.jpg differ
diff --git a/docs/images/focus-reading/focus-reading.jpg b/docs/images/focus-reading/focus-reading.jpg
new file mode 100644
index 00000000..97318e7b
Binary files /dev/null and b/docs/images/focus-reading/focus-reading.jpg differ
diff --git a/docs/sd-card-fonts.md b/docs/sd-card-fonts.md
index 39f56e5e..db9b1277 100644
--- a/docs/sd-card-fonts.md
+++ b/docs/sd-card-fonts.md
@@ -10,7 +10,7 @@ There are three ways to install fonts:
### Option 1: Download from device (recommended)
1. Connect your CrossPoint reader to WiFi
-2. Go to **Settings > System > Download Fonts**
+2. Go to **Settings > System > Manage Fonts**
3. Browse available font families and tap to download
4. Downloaded fonts appear immediately in **Settings > Reader > Font Family**
@@ -24,28 +24,38 @@ There are three ways to install fonts:
### Option 3: Manual SD card copy
1. Download font files from the
- [Releases page](https://github.com/crosspoint-reader/crosspoint-reader/releases/tag/sd-fonts)
-2. Copy font family folders to `/.crosspoint/fonts/` on your SD card:
+ [crosspoint-fonts repository](https://github.com/crosspoint-reader/crosspoint-fonts)
+2. Copy font family folders to one of two locations on your SD card:
+
+ - `/.fonts/` — hidden directory (preferred; keeps the SD root tidy
+ when mounted on a desktop)
+ - `/fonts/` — visible directory (use this if your OS hides dot-files
+ and you'd rather see the folder in your file manager)
+
+ Both roots are always scanned at boot and the results are merged: a
+ family installed in `/fonts/` shows up even when `/.fonts/` also
+ exists, and vice versa. The two roots only collide if the same family
+ name appears in both — in that case the copy in `/.fonts/` wins and
+ the duplicate in `/fonts/` is ignored.
SD Card Root/
- └── .crosspoint/
- └── fonts/
- ├── Bookerly-SD/
- │ ├── Bookerly-SD_12.cpfont
- │ ├── Bookerly-SD_14.cpfont
- │ ├── Bookerly-SD_16.cpfont
- │ └── Bookerly-SD_18.cpfont
+ ├── .fonts/ ← Hidden root (preferred)
+ │ └── Literata/
+ │ ├── Literata_12.cpfont
+ │ ├── Literata_14.cpfont
+ │ ├── Literata_16.cpfont
+ │ └── Literata_18.cpfont
+ └── fonts/ ← Visible root (equally valid)
+ └── Merriweather/
+ ├── Merriweather_12.cpfont
└── ...
3. Insert the SD card and power on your CrossPoint reader
## Available Pre-Built Fonts
-| Font | Best For | Languages |
-|------|----------|-----------|
-| Bookerly-SD | General reading | English, Western European |
-| NotoSansExtended | Multi-script reading | European, Greek, Cyrillic, Georgian, Armenian, Ethiopic |
-| NotoSansCJK | Chinese/Japanese/Korean | CJK + ASCII |
+The current list of pre-built fonts is maintained in the
+[crosspoint-fonts repository](https://github.com/crosspoint-reader/crosspoint-fonts).
## Converting Custom Fonts
diff --git a/lib/EpdFont/SdCardFontManager.cpp b/lib/EpdFont/SdCardFontManager.cpp
index 804df07d..a6032336 100644
--- a/lib/EpdFont/SdCardFontManager.cpp
+++ b/lib/EpdFont/SdCardFontManager.cpp
@@ -28,21 +28,25 @@ 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 targetPtSize) {
+bool SdCardFontManager::loadFamily(const SdCardFontFamilyInfo& family, GfxRenderer& renderer, uint8_t fontSizeEnum) {
// Unload any previously loaded family first
if (!loadedFamilyName_.empty()) {
unloadAll(renderer);
}
- // 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) {
+ // 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()) {
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());
@@ -66,8 +70,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 (target=%u)", selected->path.c_str(), selected->pointSize, fontId,
- font->styleCount(), targetPtSize);
+ LOG_DBG("SDMGR", "Loaded %s size=%u id=%d styles=%u (sizeEnum=%u)", selected->path.c_str(), selected->pointSize,
+ fontId, font->styleCount(), fontSizeEnum);
EpdFontFamily fontFamily(font->getEpdFont(0), font->getEpdFont(1), font->getEpdFont(2), font->getEpdFont(3));
renderer.insertFont(fontId, fontFamily);
diff --git a/lib/EpdFont/SdCardFontManager.h b/lib/EpdFont/SdCardFontManager.h
index def66e8d..aec07472 100644
--- a/lib/EpdFont/SdCardFontManager.h
+++ b/lib/EpdFont/SdCardFontManager.h
@@ -15,16 +15,12 @@ class SdCardFontManager {
SdCardFontManager(const SdCardFontManager&) = delete;
SdCardFontManager& operator=(const SdCardFontManager&) = delete;
- // 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.
+ // 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.
// Returns true on success.
- bool loadFamily(const SdCardFontFamilyInfo& family, GfxRenderer& renderer, uint8_t targetPtSize);
+ bool loadFamily(const SdCardFontFamilyInfo& family, GfxRenderer& renderer, uint8_t fontSizeEnum);
// Unload everything, unregister from renderer.
void unloadAll(GfxRenderer& renderer);
diff --git a/lib/EpdFont/SdCardFontRegistry.cpp b/lib/EpdFont/SdCardFontRegistry.cpp
index af1009b0..2e0f145c 100644
--- a/lib/EpdFont/SdCardFontRegistry.cpp
+++ b/lib/EpdFont/SdCardFontRegistry.cpp
@@ -4,8 +4,6 @@
#include
#include
-#include
-#include
#include
// --- SdCardFontFamilyInfo helpers ---
@@ -40,21 +38,6 @@ std::vector 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(f.pointSize) - static_cast(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) {
diff --git a/lib/EpdFont/SdCardFontRegistry.h b/lib/EpdFont/SdCardFontRegistry.h
index aae932d9..f96035ed 100644
--- a/lib/EpdFont/SdCardFontRegistry.h
+++ b/lib/EpdFont/SdCardFontRegistry.h
@@ -20,15 +20,6 @@ struct SdCardFontFamilyInfo {
const SdCardFontFileInfo* findFile(uint8_t size, uint8_t style = 0) const;
bool hasSize(uint8_t size) const;
std::vector 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 {
diff --git a/lib/EpdFont/scripts/build-sd-fonts.py b/lib/EpdFont/scripts/build-sd-fonts.py
index 9f29c00d..8f981fbe 100755
--- a/lib/EpdFont/scripts/build-sd-fonts.py
+++ b/lib/EpdFont/scripts/build-sd-fonts.py
@@ -17,6 +17,12 @@ Usage:
# Generate only specific families
python3 build-sd-fonts.py --only Literata,IBMPlexMono
+
+ # Stream child process output for debugging
+ python3 build-sd-fonts.py --verbose
+
+ # Override the per-family timeout (default: 600s)
+ python3 build-sd-fonts.py --timeout 1200
"""
import argparse
@@ -25,6 +31,8 @@ import shutil
import subprocess
import sys
import tempfile
+import threading
+import time
import urllib.request
from concurrent.futures import ProcessPoolExecutor, as_completed
from pathlib import Path
@@ -141,7 +149,16 @@ def resolve_font_path(style_spec: dict, family_name: str, style_name: str) -> Pa
return resolved
-def build_family(family: dict, output_base: Path) -> tuple[str, bool, str]:
+def _stream_pipe(pipe, prefix: str, dest: list[str]):
+ """Read lines from a pipe, print with prefix, and accumulate into dest."""
+ for line in pipe:
+ dest.append(line)
+ print(f" [{prefix}] {line}", end="", flush=True)
+
+
+def build_family(
+ family: dict, output_base: Path, verbose: bool = False, timeout: int = 600
+) -> tuple[str, bool, str]:
"""Build a single font family. Returns (name, success, message)."""
name = family["name"]
output_dir = output_base / name
@@ -185,18 +202,52 @@ def build_family(family: dict, output_base: Path) -> tuple[str, bool, str]:
cmd.append("--force-autohint")
# Run fontconvert_sdcard.py
+ start = time.monotonic()
try:
- result = subprocess.run(
- cmd,
- capture_output=True,
- text=True,
- timeout=600,
- )
- if result.returncode != 0:
- return name, False, result.stderr.strip() or f"Exit code {result.returncode}"
- return name, True, ""
- except subprocess.TimeoutExpired:
- return name, False, "Timed out after 600s"
+ if verbose:
+ proc = subprocess.Popen(
+ cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True
+ )
+ stdout_lines: list[str] = []
+ stderr_lines: list[str] = []
+ t_out = threading.Thread(
+ target=_stream_pipe, args=(proc.stdout, name, stdout_lines)
+ )
+ t_err = threading.Thread(
+ target=_stream_pipe, args=(proc.stderr, f"{name}/err", stderr_lines)
+ )
+ t_out.start()
+ t_err.start()
+ try:
+ proc.wait(timeout=timeout)
+ except subprocess.TimeoutExpired:
+ proc.kill()
+ proc.wait()
+ elapsed = time.monotonic() - start
+ return name, False, f"Timed out after {elapsed:.0f}s"
+ finally:
+ t_out.join()
+ t_err.join()
+
+ if proc.returncode != 0:
+ err = "".join(stderr_lines).strip()
+ return name, False, err or f"Exit code {proc.returncode}"
+ return name, True, ""
+ else:
+ result = subprocess.run(
+ cmd, capture_output=True, text=True, timeout=timeout,
+ )
+ if result.returncode != 0:
+ return name, False, result.stderr.strip() or f"Exit code {result.returncode}"
+ return name, True, ""
+ except subprocess.TimeoutExpired as e:
+ elapsed = time.monotonic() - start
+ tail = ""
+ captured = getattr(e, "stderr", None) or getattr(e, "stdout", None)
+ if captured:
+ lines = captured.strip().splitlines()
+ tail = "\n Last output:\n" + "\n".join(f" | {l}" for l in lines[-20:])
+ return name, False, f"Timed out after {elapsed:.0f}s{tail}"
except Exception as e:
return name, False, str(e)
@@ -252,6 +303,14 @@ def main():
help="Max parallel jobs (default: number of families)"
)
parser.add_argument("--clean", action="store_true", help="Clean output directory before building")
+ parser.add_argument(
+ "--verbose", "-v", action="store_true",
+ help="Stream child process output in real time (useful for debugging timeouts)"
+ )
+ parser.add_argument(
+ "--timeout", type=int, default=600,
+ help="Per-family timeout in seconds (default: 600)"
+ )
args = parser.parse_args()
if args.manifest and not args.base_url:
@@ -303,12 +362,14 @@ def main():
# Build phase (parallel)
max_workers = args.jobs or len(families)
- print(f"\n=== Building {len(families)} families ({max_workers} parallel jobs) ===\n")
+ verbose = args.verbose
+ timeout = args.timeout
+ print(f"\n=== Building {len(families)} families ({max_workers} parallel jobs, timeout {timeout}s) ===\n")
failed = []
with ProcessPoolExecutor(max_workers=max_workers) as executor:
futures = {
- executor.submit(build_family, family, output_base): family["name"]
+ executor.submit(build_family, family, output_base, verbose, timeout): family["name"]
for family in families
}
for future in as_completed(futures):
diff --git a/lib/EpdFont/scripts/fontconvert.py b/lib/EpdFont/scripts/fontconvert.py
index 8e7b17a9..a55fab75 100755
--- a/lib/EpdFont/scripts/fontconvert.py
+++ b/lib/EpdFont/scripts/fontconvert.py
@@ -1,12 +1,10 @@
#!python3
-import freetype
import zlib
import sys
import re
import math
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
@@ -27,6 +25,9 @@ parser.add_argument("--force-autohint", dest="force_autohint", action="store_tru
parser.add_argument("--pnum", dest="pnum", action="store_true", help="Use proportional numerals (pnum OpenType feature) instead of default tabular figures. Reduces visual gaps between digits in running prose.")
args = parser.parse_args()
+import freetype
+from fontTools.ttLib import TTFont
+
GlyphProps = namedtuple("GlyphProps", ["width", "height", "advance_x", "left", "top", "data_length", "data_offset", "code_point"])
font_stack = [freetype.Face(f) for f in args.fontstack]
diff --git a/lib/EpdFont/scripts/fontconvert_sdcard.py b/lib/EpdFont/scripts/fontconvert_sdcard.py
index baf3875f..f0f83c8e 100755
--- a/lib/EpdFont/scripts/fontconvert_sdcard.py
+++ b/lib/EpdFont/scripts/fontconvert_sdcard.py
@@ -22,7 +22,8 @@ Usage:
"""
-import freetype
+from __future__ import annotations
+
import struct
import sys
import os
@@ -31,8 +32,6 @@ import math
import argparse
from collections import namedtuple
-from fontTools.ttLib import TTFont
-
from cpfont_version import CPFONT_VERSION
# --- Unicode interval presets ---
@@ -252,6 +251,8 @@ def extract_kerning_fonttools(font_path, codepoints, ppem):
codepoints. Values are scaled from font design units to integer
pixels at ppem.
"""
+ from fontTools.ttLib import TTFont
+
font = TTFont(font_path)
units_per_em = font['head'].unitsPerEm
cmap = font.getBestCmap() or {}
@@ -402,6 +403,8 @@ def extract_ligatures_fonttools(font_path, codepoints):
Returns list of (packed_pair, ligature_codepoint) for the given codepoints.
Multi-character ligatures are decomposed into chained pairs.
"""
+ from fontTools.ttLib import TTFont
+
font = TTFont(font_path)
cmap = font.getBestCmap() or {}
@@ -514,6 +517,8 @@ def extract_ligatures_fonttools(font_path, codepoints):
def rasterize_font_style(fontfile, size, intervals, style_id=0, force_autohint=False):
"""Rasterize all glyphs for one font style. Returns StyleRasterData."""
+ import freetype
+
style_names = {0: "regular", 1: "bold", 2: "italic", 3: "bolditalic"}
style_label = style_names.get(style_id, str(style_id))
@@ -535,14 +540,16 @@ def rasterize_font_style(fontfile, size, intervals, style_id=0, force_autohint=F
return face
return None
- # Validate intervals: remove codepoints not present in the font
+ # Validate intervals: remove codepoints not present in the font.
+ # Only check glyph existence via get_char_index — do NOT call
+ # load_glyph here, as that triggers FT_LOAD_RENDER at the target
+ # DPI and doubles total rasterization time for no benefit.
print(f" [{style_label}] Validating intervals against font...", file=sys.stderr)
validated_intervals = []
for i_start, i_end in intervals:
start = i_start
for code_point in range(i_start, i_end + 1):
- f = load_glyph(code_point)
- if f is None:
+ if face.get_char_index(code_point) == 0:
if start < code_point:
validated_intervals.append((start, code_point - 1))
start = code_point + 1
@@ -575,13 +582,18 @@ def rasterize_font_style(fontfile, size, intervals, style_id=0, force_autohint=F
# 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.
+ #
+ # Cache bitmap.buffer in a local — ctypes struct field access
+ # creates a new Python wrapper object each time, so re-evaluating
+ # it per pixel is catastrophically slow.
pixels4g = []
px = 0
+ buf = bitmap.buffer
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]
+ v = buf[row_offset + x]
if x % 2 == 0:
px = (v >> 4)
else:
diff --git a/lib/EpdFont/scripts/verify_compression.py b/lib/EpdFont/scripts/verify_compression.py
index c4b13e17..c2320c78 100644
--- a/lib/EpdFont/scripts/verify_compression.py
+++ b/lib/EpdFont/scripts/verify_compression.py
@@ -234,6 +234,9 @@ def main():
if len(sys.argv) < 2:
print(f"Usage: {sys.argv[0]} ", file=sys.stderr)
sys.exit(1)
+ if sys.argv[1] in ("-h", "--help"):
+ print(f"Usage: {sys.argv[0]} ")
+ sys.exit(0)
font_dir = sys.argv[1]
if not os.path.isdir(font_dir):
diff --git a/lib/Epub/Epub/ParsedText.cpp b/lib/Epub/Epub/ParsedText.cpp
index 6f31f5f2..e348270b 100644
--- a/lib/Epub/Epub/ParsedText.cpp
+++ b/lib/Epub/Epub/ParsedText.cpp
@@ -74,21 +74,170 @@ uint16_t measureWordWidth(const GfxRenderer& renderer, const int fontId, const s
return renderer.getTextAdvanceX(fontId, sanitized.c_str(), style);
}
+// Checks if a UTF-8 codepoint should be counted as part of a word for Focus Reading
+bool isWordCharacter(uint32_t cp) {
+ // ASCII range (Catches 95%+ of characters immediately)
+ if (cp < 128) {
+ // Bitwise trick: (cp | 0x20) converts uppercase ASCII to lowercase.
+ // This checks for A-Z and a-z mathematically, avoiding memory lookups and
+ return ((cp | 0x20) >= 'a' && (cp | 0x20) <= 'z') || cp == '\'';
+ }
+
+ // General Punctuation Block, Currency, Math, Arrows, & Symbols (0x2000 - 0x2BFF)
+ if (cp >= 0x2000 && cp <= 0x2BFF) {
+ // Explicitly allow smart quotes, reject all other general punctuation (em-dashes, etc.)
+ return cp == 0x2018 || cp == 0x2019;
+ }
+
+ // Latin-1 Punctuation Block (0x00A1 - 0x00BF)
+ if (cp >= 0x00A1 && cp <= 0x00BF) {
+ // Allow ordinal indicators and micro sign, reject the rest (¡, ¿, «, », etc.)
+ return cp == 0x00AA || cp == 0x00B5 || cp == 0x00BA;
+ }
+
+ // Rejects Two-em dash, Three-em dash, Double oblique hyphen, etc.
+ if (cp >= 0x2E00 && cp <= 0x2E7F) return false;
+
+ // Rejects Modifier Minus (0x02D7), Small Hyphen (0xFE63), and Fullwidth Hyphen (0xFF0D)
+ if (cp == 0x02D7 || cp == 0xFE63 || cp == 0xFF0D) return false;
+ // Assume all other Unicode ranges (accented letters, Cyrillic, Greek, etc.) are valid
+
+ return true;
+}
+
} // namespace
void ParsedText::addWord(std::string word, const EpdFontFamily::Style fontStyle, const bool underline,
const bool attachToPrevious) {
if (word.empty()) return;
- words.push_back(std::move(word));
- EpdFontFamily::Style combinedStyle = fontStyle;
+ EpdFontFamily::Style baseStyle = fontStyle;
if (underline) {
- combinedStyle = static_cast(combinedStyle | EpdFontFamily::UNDERLINE);
+ baseStyle = static_cast(baseStyle | EpdFontFamily::UNDERLINE);
}
- wordStyles.push_back(combinedStyle);
- wordContinues.push_back(attachToPrevious);
-}
+ // Already-bold text should stay fully bold; focus splitting would make its suffix regular later.
+ if (!this->focusReadingEnabled || (baseStyle & EpdFontFamily::BOLD) != 0) {
+ words.push_back(std::move(word));
+ wordStyles.push_back(baseStyle);
+ wordContinues.push_back(attachToPrevious);
+ wordIsFocusSuffix.push_back(false);
+ return;
+ }
+
+ // --- FOCUS READING LOGIC BELOW ---
+
+ // Pre-reserve capacity to prevent mid-word heap reallocations.
+ size_t maxPossibleNewTokens = word.length();
+ size_t requiredSize = words.size() + maxPossibleNewTokens;
+
+ if (words.capacity() < requiredSize) {
+ // Emulate standard geometric growth (doubling) to ensure we don't reallocate on every word.
+ size_t newCapacity = words.capacity() * 2;
+
+ // Ensure the doubled capacity is actually enough for this specific word
+ if (newCapacity < requiredSize) {
+ newCapacity = requiredSize;
+ }
+ // Set a sensible minimum starting size so the first few words don't trigger tiny reallocations
+ if (newCapacity < 16) {
+ newCapacity = 16;
+ }
+
+ words.reserve(newCapacity);
+ wordStyles.reserve(newCapacity);
+ wordContinues.reserve(newCapacity);
+ wordIsFocusSuffix.reserve(newCapacity);
+ }
+
+ // Lambda helper to process and push individual sub-segments of the string
+ // Use std::string_view to avoid heap allocations when slicing
+ auto processSegment = [&](std::string_view segment, bool isWord, bool attach) {
+ if (!isWord) {
+ // Punctuation and Numbers stay regular
+ words.emplace_back(segment);
+ wordStyles.push_back(baseStyle);
+ wordContinues.push_back(attach);
+ wordIsFocusSuffix.push_back(false);
+ } else {
+ size_t charCount = 0;
+ const unsigned char* countPtr = reinterpret_cast(segment.data());
+ const unsigned char* countEnd = countPtr + segment.length();
+
+ while (countPtr < countEnd) {
+ utf8NextCodepoint(&countPtr);
+ charCount++;
+ }
+
+ // Target 45% for 1-bold at 4 chars and 3-bold at 7 chars with floor truncation
+ constexpr size_t FOCUS_READING_PERCENT = 45;
+ size_t targetBoldChars = (charCount * FOCUS_READING_PERCENT) / 100;
+ targetBoldChars = std::clamp(targetBoldChars, 1, 9);
+
+ if (targetBoldChars >= charCount) {
+ // Whole segment is bold - no suffix split needed
+ words.emplace_back(segment);
+ wordStyles.push_back(static_cast(baseStyle | EpdFontFamily::BOLD));
+ wordContinues.push_back(attach);
+ wordIsFocusSuffix.push_back(false);
+ } else {
+ countPtr = reinterpret_cast(segment.data());
+ for (size_t i = 0; i < targetBoldChars; ++i) {
+ utf8NextCodepoint(&countPtr);
+ }
+ size_t splitByteOffset = countPtr - reinterpret_cast(segment.data());
+
+ // Bold prefix
+ words.emplace_back(segment.substr(0, splitByteOffset));
+ wordStyles.push_back(static_cast(baseStyle | EpdFontFamily::BOLD));
+ wordContinues.push_back(attach);
+ wordIsFocusSuffix.push_back(false);
+
+ // Regular suffix - marked so extractLine can merge it back into single TextBlock entry
+ words.emplace_back(segment.substr(splitByteOffset));
+ wordStyles.push_back(baseStyle);
+ wordContinues.push_back(true);
+ wordIsFocusSuffix.push_back(true);
+ }
+ }
+ };
+
+ // Tokenize the string by alternating states (Word vs. Non-Word)
+ const unsigned char* ptr = reinterpret_cast(word.c_str());
+ const unsigned char* end = ptr + word.length();
+
+ const unsigned char* segmentStart = ptr;
+ uint32_t firstCp = utf8NextCodepoint(&ptr); // Consume the first char to determine initial state
+ bool inWordSegment = isWordCharacter(firstCp);
+
+ bool isFirstSegment = true;
+
+ while (ptr < end) {
+ const unsigned char* currentCpStart = ptr;
+ uint32_t cp = utf8NextCodepoint(&ptr);
+ bool isWordChar = isWordCharacter(cp);
+
+ // Whenever the character type flips, slice off the segment we just completed and process it
+ if (isWordChar != inWordSegment) {
+ size_t segmentLen = currentCpStart - segmentStart;
+ std::string_view segment(reinterpret_cast(segmentStart), segmentLen);
+
+ // Only the very first segment inherits the original attachToPrevious flag.
+ // Every subsequent segment MUST attach=true so it glues seamlessly to the prefix.
+ processSegment(segment, inWordSegment, isFirstSegment ? attachToPrevious : true);
+
+ // Setup for the next segment
+ segmentStart = currentCpStart;
+ inWordSegment = isWordChar;
+ isFirstSegment = false;
+ }
+ }
+
+ // Process the final remaining segment
+ size_t segmentLen = end - segmentStart;
+ std::string_view segment(reinterpret_cast(segmentStart), segmentLen);
+ processSegment(segment, inWordSegment, isFirstSegment ? attachToPrevious : true);
+}
// Consumes data to minimize memory usage
void ParsedText::layoutAndExtractLines(const GfxRenderer& renderer, const int fontId, const uint16_t viewportWidth,
const std::function)>& processLine,
@@ -153,6 +302,7 @@ void ParsedText::layoutAndExtractLines(const GfxRenderer& renderer, const int fo
words.erase(words.begin(), words.begin() + consumed);
wordStyles.erase(wordStyles.begin(), wordStyles.begin() + consumed);
wordContinues.erase(wordContinues.begin(), wordContinues.begin() + consumed);
+ wordIsFocusSuffix.erase(wordIsFocusSuffix.begin(), wordIsFocusSuffix.begin() + consumed);
}
}
@@ -436,6 +586,8 @@ bool ParsedText::hyphenateWordAtIndex(const size_t wordIndex, const int availabl
// Insert the remainder word (with matching style and continuation flag) directly after the prefix.
words.insert(words.begin() + wordIndex + 1, remainder);
wordStyles.insert(wordStyles.begin() + wordIndex + 1, style);
+ // The hyphen remainder is not a focus suffix - it starts fresh on the next line.
+ wordIsFocusSuffix.insert(wordIsFocusSuffix.begin() + wordIndex + 1, false);
// Continuation flag handling after splitting a word into prefix + remainder.
//
@@ -500,6 +652,11 @@ void ParsedText::extractLine(const size_t breakIndex, const int pageWidth, const
renderer.getSpaceAdvance(fontId, lastCodepoint(words[lastBreakAt + wordIdx - 1]),
firstCodepoint(words[lastBreakAt + wordIdx]), wordStyles[lastBreakAt + wordIdx - 1]);
} else if (wordIdx > 0 && continuesVec[lastBreakAt + wordIdx]) {
+ // Non-breaking space tokens (" " with continues=true) are visible, stretchable spaces —
+ // count them as justifiable gaps so justifyExtra is distributed to them too.
+ if (words[lastBreakAt + wordIdx] == " ") {
+ actualGapCount++;
+ }
// Cross-boundary kerning for continuation words (e.g. nonbreaking spaces, attached punctuation)
totalNaturalGaps +=
renderer.getKerning(fontId, lastCodepoint(words[lastBreakAt + wordIdx - 1]),
@@ -541,6 +698,11 @@ void ParsedText::extractLine(const size_t breakIndex, const int pageWidth, const
advance +=
renderer.getKerning(fontId, lastCodepoint(words[lastBreakAt + wordIdx]),
firstCodepoint(words[lastBreakAt + wordIdx + 1]), wordStyles[lastBreakAt + wordIdx]);
+ // Non-breaking space tokens are stretchable — expand them during justification like normal spaces.
+ if (words[lastBreakAt + wordIdx] == " " && continuesVec[lastBreakAt + wordIdx] &&
+ blockStyle.alignment == CssTextAlign::Justify && !isLastLine) {
+ advance += justifyExtra;
+ }
xpos += advance;
} else {
int gap = 0;
@@ -567,6 +729,63 @@ void ParsedText::extractLine(const size_t breakIndex, const int pageWidth, const
}
}
- processLine(
- std::make_shared(std::move(lineWords), std::move(lineXPos), std::move(lineWordStyles), blockStyle));
+ // Fast path: when no word on this line was split for focus reading, skip the merge work
+ // entirely and pass empty boundary/suffixX vectors. TextBlock pays zero per-word RAM cost
+ // for these annotations when the vectors are empty.
+ bool lineHasFocusSplit = false;
+ for (size_t i = 0; i < lineWordCount; i++) {
+ if (wordIsFocusSuffix[lastBreakAt + i]) {
+ lineHasFocusSplit = true;
+ break;
+ }
+ }
+
+ if (!lineHasFocusSplit) {
+ processLine(std::make_shared(std::move(lineWords), std::move(lineXPos), std::move(lineWordStyles),
+ std::vector{}, std::vector{}, blockStyle));
+ return;
+ }
+
+ // Slow path: merge focus suffix tokens back into their preceding word entry so each
+ // original word occupies one TextBlock slot. Splits are recorded as per-word annotations
+ // applied at render time, cutting the token count significantly when the feature is active.
+ std::vector outWords;
+ std::vector outXPos;
+ std::vector outStyles;
+ std::vector outBoundaries;
+ std::vector outSuffixX;
+ outWords.reserve(lineWordCount);
+ outXPos.reserve(lineWordCount);
+ outStyles.reserve(lineWordCount);
+ outBoundaries.reserve(lineWordCount);
+ outSuffixX.reserve(lineWordCount);
+
+ for (size_t i = 0; i < lineWordCount; i++) {
+ if (wordIsFocusSuffix[lastBreakAt + i] && !outWords.empty()) {
+ // Focus suffix: merge string into the preceding bold-prefix entry.
+ outWords.back() += lineWords[i];
+ } else {
+ // Normal word: check for a following focus suffix to record the byte boundary.
+ uint8_t boundary = 0;
+ uint16_t suffixX = 0;
+ if (i + 1 < lineWordCount && wordIsFocusSuffix[lastBreakAt + i + 1]) {
+ boundary = static_cast(std::min(lineWords[i].size(), size_t{255}));
+ // Suffix x offset = layout-time advance of the bold prefix, already known from xpos table.
+ suffixX = static_cast(lineXPos[i + 1] - lineXPos[i]);
+ }
+ outWords.push_back(std::move(lineWords[i]));
+ outXPos.push_back(lineXPos[i]);
+ // For focus entries with a suffix, strip BOLD from the stored style.
+ // Render re-applies it to the prefix portion only, via the boundary field.
+ const EpdFontFamily::Style storedStyle =
+ boundary > 0 ? static_cast(lineWordStyles[i] & ~EpdFontFamily::BOLD)
+ : lineWordStyles[i];
+ outStyles.push_back(storedStyle);
+ outBoundaries.push_back(boundary);
+ outSuffixX.push_back(suffixX);
+ }
+ }
+
+ processLine(std::make_shared(std::move(outWords), std::move(outXPos), std::move(outStyles),
+ std::move(outBoundaries), std::move(outSuffixX), blockStyle));
}
diff --git a/lib/Epub/Epub/ParsedText.h b/lib/Epub/Epub/ParsedText.h
index 9d43400e..167d3d26 100644
--- a/lib/Epub/Epub/ParsedText.h
+++ b/lib/Epub/Epub/ParsedText.h
@@ -15,10 +15,12 @@ class GfxRenderer;
class ParsedText {
std::vector words;
std::vector wordStyles;
- std::vector wordContinues; // true = word attaches to previous (no space before it)
+ std::vector wordContinues; // true = word attaches to previous (no space before it)
+ std::vector wordIsFocusSuffix; // true = token is the regular tail of a focus bold-prefix split
BlockStyle blockStyle;
bool extraParagraphSpacing;
bool hyphenationEnabled;
+ bool focusReadingEnabled;
void applyParagraphIndent();
std::vector computeLineBreaks(const GfxRenderer& renderer, int fontId, int pageWidth,
@@ -35,8 +37,11 @@ class ParsedText {
public:
explicit ParsedText(const bool extraParagraphSpacing, const bool hyphenationEnabled = false,
- const BlockStyle& blockStyle = BlockStyle())
- : blockStyle(blockStyle), extraParagraphSpacing(extraParagraphSpacing), hyphenationEnabled(hyphenationEnabled) {}
+ const bool focusReadingEnabled = false, const BlockStyle& blockStyle = BlockStyle())
+ : blockStyle(blockStyle),
+ extraParagraphSpacing(extraParagraphSpacing),
+ hyphenationEnabled(hyphenationEnabled),
+ focusReadingEnabled(focusReadingEnabled) {}
~ParsedText() = default;
void addWord(std::string word, EpdFontFamily::Style fontStyle, bool underline = false, bool attachToPrevious = false);
diff --git a/lib/Epub/Epub/Section.cpp b/lib/Epub/Epub/Section.cpp
index 57a92cd8..7cbd37e1 100644
--- a/lib/Epub/Epub/Section.cpp
+++ b/lib/Epub/Epub/Section.cpp
@@ -13,8 +13,8 @@ namespace {
constexpr uint8_t SECTION_FILE_VERSION = 23;
constexpr uint32_t HEADER_SIZE = sizeof(uint8_t) + sizeof(int) + sizeof(float) + sizeof(bool) + sizeof(uint8_t) +
sizeof(uint16_t) + sizeof(uint16_t) + sizeof(uint16_t) + sizeof(bool) + sizeof(bool) +
- sizeof(uint8_t) + sizeof(uint32_t) + sizeof(uint32_t) + sizeof(uint32_t) +
- sizeof(uint32_t);
+ sizeof(uint8_t) + sizeof(bool) + sizeof(uint32_t) + sizeof(uint32_t) +
+ sizeof(uint32_t) + sizeof(uint32_t);
struct PageLutEntry {
uint32_t fileOffset;
@@ -43,7 +43,8 @@ uint32_t Section::onPageComplete(std::unique_ptr page) {
void Section::writeSectionFileHeader(const int fontId, const float lineCompression, const bool extraParagraphSpacing,
const uint8_t paragraphAlignment, const uint16_t viewportWidth,
const uint16_t viewportHeight, const bool hyphenationEnabled,
- const bool embeddedStyle, const uint8_t imageRendering) {
+ const bool embeddedStyle, const uint8_t imageRendering,
+ const bool focusReadingEnabled) {
if (!file) {
LOG_DBG("SCT", "File not open for writing header");
return;
@@ -51,8 +52,8 @@ void Section::writeSectionFileHeader(const int fontId, const float lineCompressi
static_assert(HEADER_SIZE == sizeof(SECTION_FILE_VERSION) + sizeof(fontId) + sizeof(lineCompression) +
sizeof(extraParagraphSpacing) + sizeof(paragraphAlignment) + sizeof(viewportWidth) +
sizeof(viewportHeight) + sizeof(pageCount) + sizeof(hyphenationEnabled) +
- sizeof(embeddedStyle) + sizeof(imageRendering) + sizeof(uint32_t) +
- sizeof(uint32_t) + sizeof(uint32_t) + sizeof(uint32_t),
+ sizeof(embeddedStyle) + sizeof(imageRendering) + sizeof(focusReadingEnabled) +
+ sizeof(uint32_t) + sizeof(uint32_t) + sizeof(uint32_t) + sizeof(uint32_t),
"Header size mismatch");
serialization::writePod(file, SECTION_FILE_VERSION);
serialization::writePod(file, fontId);
@@ -64,6 +65,7 @@ void Section::writeSectionFileHeader(const int fontId, const float lineCompressi
serialization::writePod(file, hyphenationEnabled);
serialization::writePod(file, embeddedStyle);
serialization::writePod(file, imageRendering);
+ serialization::writePod(file, focusReadingEnabled);
serialization::writePod(file, pageCount); // Placeholder for page count (will be initially 0, patched later)
serialization::writePod(file, static_cast(0)); // Placeholder for LUT offset (patched later)
serialization::writePod(file, static_cast(0)); // Placeholder for anchor map offset (patched later)
@@ -74,7 +76,7 @@ void Section::writeSectionFileHeader(const int fontId, const float lineCompressi
bool Section::loadSectionFile(const int fontId, const float lineCompression, const bool extraParagraphSpacing,
const uint8_t paragraphAlignment, const uint16_t viewportWidth,
const uint16_t viewportHeight, const bool hyphenationEnabled, const bool embeddedStyle,
- const uint8_t imageRendering) {
+ const uint8_t imageRendering, const bool focusReadingEnabled) {
if (!Storage.openFileForRead("SCT", filePath, file)) {
return false;
}
@@ -99,6 +101,7 @@ bool Section::loadSectionFile(const int fontId, const float lineCompression, con
bool fileHyphenationEnabled;
bool fileEmbeddedStyle;
uint8_t fileImageRendering;
+ bool fileFocusReadingEnabled;
serialization::readPod(file, fileFontId);
serialization::readPod(file, fileLineCompression);
serialization::readPod(file, fileExtraParagraphSpacing);
@@ -108,13 +111,13 @@ bool Section::loadSectionFile(const int fontId, const float lineCompression, con
serialization::readPod(file, fileHyphenationEnabled);
serialization::readPod(file, fileEmbeddedStyle);
serialization::readPod(file, fileImageRendering);
+ serialization::readPod(file, fileFocusReadingEnabled);
if (fontId != fileFontId || lineCompression != fileLineCompression ||
extraParagraphSpacing != fileExtraParagraphSpacing || paragraphAlignment != fileParagraphAlignment ||
viewportWidth != fileViewportWidth || viewportHeight != fileViewportHeight ||
hyphenationEnabled != fileHyphenationEnabled || embeddedStyle != fileEmbeddedStyle ||
- imageRendering != fileImageRendering) {
- // Explicit close() required: member variable persists beyond function scope
+ imageRendering != fileImageRendering || focusReadingEnabled != fileFocusReadingEnabled) {
file.close();
LOG_ERR("SCT", "Deserialization failed: Parameters do not match");
clearCache();
@@ -148,7 +151,8 @@ bool Section::clearCache() const {
bool Section::createSectionFile(const int fontId, const float lineCompression, const bool extraParagraphSpacing,
const uint8_t paragraphAlignment, const uint16_t viewportWidth,
const uint16_t viewportHeight, const bool hyphenationEnabled, const bool embeddedStyle,
- const uint8_t imageRendering, const std::function& popupFn) {
+ const uint8_t imageRendering, const bool focusReadingEnabled,
+ const std::function& popupFn) {
const auto localPath = epub->getSpineItem(spineIndex).href;
const auto tmpHtmlPath = epub->getCachePath() + "/.tmp_" + std::to_string(spineIndex) + ".html";
@@ -199,7 +203,7 @@ bool Section::createSectionFile(const int fontId, const float lineCompression, c
return false;
}
writeSectionFileHeader(fontId, lineCompression, extraParagraphSpacing, paragraphAlignment, viewportWidth,
- viewportHeight, hyphenationEnabled, embeddedStyle, imageRendering);
+ viewportHeight, hyphenationEnabled, embeddedStyle, imageRendering, focusReadingEnabled);
std::vector lut = {};
// Derive the content base directory and image cache path prefix for the parser
@@ -219,7 +223,7 @@ bool Section::createSectionFile(const int fontId, const float lineCompression, c
ChapterHtmlSlimParser visitor(
epub, tmpHtmlPath, renderer, fontId, lineCompression, extraParagraphSpacing, paragraphAlignment, viewportWidth,
- viewportHeight, hyphenationEnabled,
+ viewportHeight, hyphenationEnabled, focusReadingEnabled,
[this, &lut](std::unique_ptr page, const uint16_t paragraphIndex, const uint16_t listItemIndex) {
lut.push_back({this->onPageComplete(std::move(page)), paragraphIndex, listItemIndex});
},
diff --git a/lib/Epub/Epub/Section.h b/lib/Epub/Epub/Section.h
index 9688e276..aea02d34 100644
--- a/lib/Epub/Epub/Section.h
+++ b/lib/Epub/Epub/Section.h
@@ -18,7 +18,7 @@ class Section {
void writeSectionFileHeader(int fontId, float lineCompression, bool extraParagraphSpacing, uint8_t paragraphAlignment,
uint16_t viewportWidth, uint16_t viewportHeight, bool hyphenationEnabled,
- bool embeddedStyle, uint8_t imageRendering);
+ bool embeddedStyle, uint8_t imageRendering, bool focusReadingEnabled);
uint32_t onPageComplete(std::unique_ptr page);
public:
@@ -33,11 +33,12 @@ class Section {
~Section() = default;
bool loadSectionFile(int fontId, float lineCompression, bool extraParagraphSpacing, uint8_t paragraphAlignment,
uint16_t viewportWidth, uint16_t viewportHeight, bool hyphenationEnabled, bool embeddedStyle,
- uint8_t imageRendering);
+ uint8_t imageRendering, bool focusReadingEnabled);
bool clearCache() const;
bool createSectionFile(int fontId, float lineCompression, bool extraParagraphSpacing, uint8_t paragraphAlignment,
uint16_t viewportWidth, uint16_t viewportHeight, bool hyphenationEnabled, bool embeddedStyle,
- uint8_t imageRendering, const std::function& popupFn = nullptr);
+ uint8_t imageRendering, bool focusReadingEnabled,
+ const std::function& popupFn = nullptr);
std::unique_ptr loadPageFromSectionFile();
// Look up the page number for an anchor id from the section cache file.
diff --git a/lib/Epub/Epub/blocks/TextBlock.cpp b/lib/Epub/Epub/blocks/TextBlock.cpp
index e3e35d42..9acd8b03 100644
--- a/lib/Epub/Epub/blocks/TextBlock.cpp
+++ b/lib/Epub/Epub/blocks/TextBlock.cpp
@@ -4,18 +4,44 @@
#include
#include
+#include
+
void TextBlock::render(const GfxRenderer& renderer, const int fontId, const int x, const int y) const {
- // Validate iterator bounds before rendering
- if (words.size() != wordXpos.size() || words.size() != wordStyles.size()) {
- LOG_ERR("TXB", "Render skipped: size mismatch (words=%u, xpos=%u, styles=%u)\n", (uint32_t)words.size(),
- (uint32_t)wordXpos.size(), (uint32_t)wordStyles.size());
+ // Focus annotations are optional: empty vectors mean no word in this block has a split.
+ // When present, they must be sized in lockstep with words[].
+ const bool hasFocus = !wordFocusBoundary.empty();
+ if (words.size() != wordXpos.size() || words.size() != wordStyles.size() ||
+ (hasFocus && (words.size() != wordFocusBoundary.size() || words.size() != wordFocusSuffixX.size()))) {
+ LOG_ERR("TXB", "Render skipped: size mismatch (words=%u, xpos=%u, styles=%u, boundary=%u, suffixX=%u)\n",
+ (uint32_t)words.size(), (uint32_t)wordXpos.size(), (uint32_t)wordStyles.size(),
+ (uint32_t)wordFocusBoundary.size(), (uint32_t)wordFocusSuffixX.size());
return;
}
for (size_t i = 0; i < words.size(); i++) {
const int wordX = wordXpos[i] + x;
const EpdFontFamily::Style currentStyle = wordStyles[i];
- renderer.drawText(fontId, wordX, y, words[i].c_str(), true, currentStyle);
+ const uint8_t boundary = hasFocus ? wordFocusBoundary[i] : 0;
+
+ if (boundary > 0) {
+ // Focus split: draw bold prefix, then the regular suffix at a pre-computed x offset.
+ // The bold prefix is bounded to 9 codepoints by the clamp on targetBoldChars in
+ // ParsedText::addWord; 9 UTF-8 codepoints occupy at most 9 * 4 = 36 bytes, +1 for null = 37.
+ // suffixX is computed at cache-creation time to avoid font metric lookups at render time.
+ static constexpr size_t MAX_FOCUS_PREFIX_BYTES = 9 * 4 + 1;
+ char boldBuf[40];
+ static_assert(sizeof(boldBuf) >= MAX_FOCUS_PREFIX_BYTES,
+ "boldBuf too small for max focus prefix (9 codepoints * 4 UTF-8 bytes + null)");
+ const auto boldStyle = static_cast(currentStyle | EpdFontFamily::BOLD);
+ const size_t boldLen = std::min({static_cast(boundary), words[i].size(), sizeof(boldBuf) - 1});
+ memcpy(boldBuf, words[i].c_str(), boldLen);
+ boldBuf[boldLen] = '\0';
+ renderer.drawText(fontId, wordX, y, boldBuf, true, boldStyle);
+ const int suffixX = wordX + wordFocusSuffixX[i];
+ renderer.drawText(fontId, suffixX, y, words[i].c_str() + boldLen, true, currentStyle);
+ } else {
+ renderer.drawText(fontId, wordX, y, words[i].c_str(), true, currentStyle);
+ }
if ((currentStyle & EpdFontFamily::UNDERLINE) != 0) {
const std::string& w = words[i];
@@ -42,9 +68,15 @@ void TextBlock::render(const GfxRenderer& renderer, const int fontId, const int
}
bool TextBlock::serialize(FsFile& file) const {
- if (words.size() != wordXpos.size() || words.size() != wordStyles.size()) {
- LOG_ERR("TXB", "Serialization failed: size mismatch (words=%u, xpos=%u, styles=%u)\n", words.size(),
- wordXpos.size(), wordStyles.size());
+ // Focus annotations are optional; vectors are either empty (no splits in this block)
+ // or sized in lockstep with words[].
+ const bool hasFocus = !wordFocusBoundary.empty();
+ if (words.size() != wordXpos.size() || words.size() != wordStyles.size() ||
+ (hasFocus && (words.size() != wordFocusBoundary.size() || words.size() != wordFocusSuffixX.size()))) {
+ LOG_ERR("TXB", "Serialization failed: size mismatch (words=%u, xpos=%u, styles=%u, boundary=%u, suffixX=%u)\n",
+ static_cast(words.size()), static_cast(wordXpos.size()),
+ static_cast(wordStyles.size()), static_cast(wordFocusBoundary.size()),
+ static_cast(wordFocusSuffixX.size()));
return false;
}
@@ -53,6 +85,13 @@ bool TextBlock::serialize(FsFile& file) const {
for (const auto& w : words) serialization::writeString(file, w);
for (auto x : wordXpos) serialization::writePod(file, x);
for (auto s : wordStyles) serialization::writePod(file, s);
+ // Focus block: 1-byte presence flag, followed by per-word vectors only when present.
+ // Saves 3 bytes/word when focus reading is disabled or no word on this line was split.
+ serialization::writePod(file, static_cast(hasFocus ? 1 : 0));
+ if (hasFocus) {
+ for (auto b : wordFocusBoundary) serialization::writePod(file, b);
+ for (auto sx : wordFocusSuffixX) serialization::writePod(file, sx);
+ }
// Style (alignment + margins/padding/indent)
serialization::writePod(file, blockStyle.alignment);
@@ -76,6 +115,8 @@ std::unique_ptr TextBlock::deserialize(FsFile& file) {
std::vector words;
std::vector wordXpos;
std::vector wordStyles;
+ std::vector wordFocusBoundary;
+ std::vector wordFocusSuffixX;
BlockStyle blockStyle;
// Word count
@@ -94,6 +135,16 @@ std::unique_ptr TextBlock::deserialize(FsFile& file) {
for (auto& w : words) serialization::readString(file, w);
for (auto& x : wordXpos) serialization::readPod(file, x);
for (auto& s : wordStyles) serialization::readPod(file, s);
+ // Focus block: presence flag, then vectors only if present. Empty vectors when absent
+ // signal "no splits in this block" to render() (zero per-word RAM cost).
+ uint8_t hasFocus;
+ serialization::readPod(file, hasFocus);
+ if (hasFocus) {
+ wordFocusBoundary.resize(wc);
+ wordFocusSuffixX.resize(wc);
+ for (auto& b : wordFocusBoundary) serialization::readPod(file, b);
+ for (auto& sx : wordFocusSuffixX) serialization::readPod(file, sx);
+ }
// Style (alignment + margins/padding/indent)
serialization::readPod(file, blockStyle.alignment);
@@ -109,6 +160,7 @@ std::unique_ptr TextBlock::deserialize(FsFile& file) {
serialization::readPod(file, blockStyle.textIndent);
serialization::readPod(file, blockStyle.textIndentDefined);
- return std::unique_ptr(
- new TextBlock(std::move(words), std::move(wordXpos), std::move(wordStyles), blockStyle));
+ return std::unique_ptr(new TextBlock(std::move(words), std::move(wordXpos), std::move(wordStyles),
+ std::move(wordFocusBoundary), std::move(wordFocusSuffixX),
+ blockStyle));
}
diff --git a/lib/Epub/Epub/blocks/TextBlock.h b/lib/Epub/Epub/blocks/TextBlock.h
index 85fdd55a..e60b283c 100644
--- a/lib/Epub/Epub/blocks/TextBlock.h
+++ b/lib/Epub/Epub/blocks/TextBlock.h
@@ -15,14 +15,28 @@ class TextBlock final : public Block {
std::vector words;
std::vector wordXpos;
std::vector wordStyles;
+ // Per-word focus boundary: N > 0 means the first N bytes of words[i] are rendered bold,
+ // the remainder in the base style. 0 means no split (whole word uses wordStyles[i]).
+ // N encodes the bold PREFIX length only — bounded to 9 codepoints (≤36 UTF-8 bytes) by
+ // FOCUS_READING_PERCENT's 1..9 clamp in ParsedText::addWord, so it always fits in uint8_t.
+ // Vector is empty when no focus splits exist anywhere in the block (zero per-word RAM cost
+ // when focus reading is disabled, or on lines that happen to contain no splittable words).
+ std::vector wordFocusBoundary;
+ // Pre-computed pixel offset from word start to the regular suffix, stored when boundary > 0.
+ // Eliminates getTextAdvanceX from the render path. 0 when boundary == 0.
+ // Empty in lockstep with wordFocusBoundary.
+ std::vector wordFocusSuffixX;
BlockStyle blockStyle;
public:
explicit TextBlock(std::vector words, std::vector word_xpos,
- std::vector word_styles, const BlockStyle& blockStyle = BlockStyle())
+ std::vector word_styles, std::vector focus_boundary,
+ std::vector focus_suffix_x, const BlockStyle& blockStyle = BlockStyle())
: words(std::move(words)),
wordXpos(std::move(word_xpos)),
wordStyles(std::move(word_styles)),
+ wordFocusBoundary(std::move(focus_boundary)),
+ wordFocusSuffixX(std::move(focus_suffix_x)),
blockStyle(blockStyle) {}
~TextBlock() override = default;
void setBlockStyle(const BlockStyle& blockStyle) { this->blockStyle = blockStyle; }
diff --git a/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp b/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp
index c9f89e2e..a25a1dbd 100644
--- a/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp
+++ b/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp
@@ -141,7 +141,7 @@ void ChapterHtmlSlimParser::startNewTextBlock(const BlockStyle& blockStyle) {
anchorData.push_back({std::move(pendingAnchorId), static_cast(completedPageCount)});
pendingAnchorId.clear();
}
- currentTextBlock.reset(new ParsedText(extraParagraphSpacing, hyphenationEnabled, blockStyle));
+ currentTextBlock.reset(new ParsedText(extraParagraphSpacing, hyphenationEnabled, focusReadingEnabled, blockStyle));
wordsExtractedInBlock = 0;
}
diff --git a/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.h b/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.h
index 5802bb94..aeaea7f5 100644
--- a/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.h
+++ b/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.h
@@ -47,6 +47,7 @@ class ChapterHtmlSlimParser {
uint16_t viewportWidth;
uint16_t viewportHeight;
bool hyphenationEnabled;
+ bool focusReadingEnabled;
const CssParser* cssParser;
bool embeddedStyle;
uint8_t imageRendering;
@@ -101,6 +102,7 @@ class ChapterHtmlSlimParser {
const int fontId, const float lineCompression, const bool extraParagraphSpacing,
const uint8_t paragraphAlignment, const uint16_t viewportWidth,
const uint16_t viewportHeight, const bool hyphenationEnabled,
+ const bool focusReadingEnabled,
const std::function, uint16_t, uint16_t)>& completePageFn,
const bool embeddedStyle, const std::string& contentBase,
const std::string& imageBasePath, const uint8_t imageRendering = 0,
@@ -116,6 +118,7 @@ class ChapterHtmlSlimParser {
viewportWidth(viewportWidth),
viewportHeight(viewportHeight),
hyphenationEnabled(hyphenationEnabled),
+ focusReadingEnabled(focusReadingEnabled),
completePageFn(completePageFn),
popupFn(popupFn),
cssParser(cssParser),
diff --git a/lib/GfxRenderer/GfxRenderer.cpp b/lib/GfxRenderer/GfxRenderer.cpp
index dcedb4e2..13283ef3 100644
--- a/lib/GfxRenderer/GfxRenderer.cpp
+++ b/lib/GfxRenderer/GfxRenderer.cpp
@@ -10,6 +10,35 @@
#include "FontCacheManager.h"
+namespace {
+
+const char* resolveVisualText(const char* text, std::string& visualBuffer, int paragraphLevel);
+
+/**
+ * Resolves the requested style to the best available style in the given SD card font.
+ * Falls back gracefully when the font lacks the requested variant.
+ */
+uint8_t resolveSdCardStyle(const SdCardFont& font, const EpdFontFamily::Style style) {
+ // Indexed by styleBits (0=REGULAR, 1=BOLD, 2=ITALIC, 3=BOLD_ITALIC)
+ static const uint8_t kFallbacks[4][4] = {
+ // REGULAR: REGULAR → BOLD → ITALIC → BOLD_ITALIC
+ {EpdFontFamily::REGULAR, EpdFontFamily::BOLD, EpdFontFamily::ITALIC, EpdFontFamily::BOLD_ITALIC},
+ // BOLD: BOLD → BOLD_ITALIC → REGULAR → ITALIC
+ {EpdFontFamily::BOLD, EpdFontFamily::BOLD_ITALIC, EpdFontFamily::REGULAR, EpdFontFamily::ITALIC},
+ // ITALIC: ITALIC → REGULAR → BOLD → BOLD_ITALIC (REGULAR before BOLD!)
+ {EpdFontFamily::ITALIC, EpdFontFamily::REGULAR, EpdFontFamily::BOLD, EpdFontFamily::BOLD_ITALIC},
+ // BOLD_ITALIC: BOLD_ITALIC → BOLD → ITALIC → REGULAR
+ {EpdFontFamily::BOLD_ITALIC, EpdFontFamily::BOLD, EpdFontFamily::ITALIC, EpdFontFamily::REGULAR},
+ };
+
+ const uint8_t styleBits = static_cast(style) & 0x03;
+ for (uint8_t candidate : kFallbacks[styleBits]) {
+ if (font.hasStyle(candidate)) return candidate;
+ }
+ return EpdFontFamily::REGULAR; // no-variant-at-all safety net
+}
+} // namespace
+
const uint8_t* GfxRenderer::getGlyphBitmap(const EpdFontData* fontData, const EpdGlyph* glyph) const {
if (fontData->groups != nullptr) {
auto* fd = fontCacheManager_ ? fontCacheManager_->getDecompressor() : nullptr;
@@ -1074,7 +1103,8 @@ int GfxRenderer::getSpaceWidth(const int fontId, const EpdFontFamily::Style styl
// Advance table fast-path for SD card fonts during layout
auto sdIt = sdCardFonts_.find(fontId);
if (sdIt != sdCardFonts_.end() && sdIt->second->hasAdvanceTable()) {
- return fp4::toPixel(sdIt->second->getAdvance(' ', static_cast(style)));
+ const uint8_t resolvedStyle = resolveSdCardStyle(*sdIt->second, style);
+ return fp4::toPixel(sdIt->second->getAdvance(' ', resolvedStyle));
}
const auto fontIt = fontMap.find(fontId);
@@ -1094,7 +1124,8 @@ int GfxRenderer::getSpaceAdvance(const int fontId, const uint32_t leftCp, const
// so we return just the space advance without kerning.
auto sdIt = sdCardFonts_.find(fontId);
if (sdIt != sdCardFonts_.end() && sdIt->second->hasAdvanceTable()) {
- return fp4::toPixel(sdIt->second->getAdvance(' ', static_cast(style)));
+ const uint8_t resolvedStyle = resolveSdCardStyle(*sdIt->second, style);
+ return fp4::toPixel(sdIt->second->getAdvance(' ', resolvedStyle));
}
const auto fontIt = fontMap.find(fontId);
@@ -1124,7 +1155,7 @@ int GfxRenderer::getTextAdvanceX(const int fontId, const char* text, EpdFontFami
auto sdIt = sdCardFonts_.find(fontId);
if (sdIt != sdCardFonts_.end() && sdIt->second->hasAdvanceTable()) {
int32_t widthFP = 0;
- const uint8_t styleIdx = static_cast(style);
+ const uint8_t styleIdx = resolveSdCardStyle(*sdIt->second, style);
while (uint32_t cp = utf8NextCodepoint(reinterpret_cast(&text))) {
widthFP += sdIt->second->getAdvance(cp, styleIdx);
}
diff --git a/lib/I18n/translations/belarusian.yaml b/lib/I18n/translations/belarusian.yaml
index 245ecc10..f854cae8 100644
--- a/lib/I18n/translations/belarusian.yaml
+++ b/lib/I18n/translations/belarusian.yaml
@@ -259,6 +259,7 @@ STR_SECTION_PREFIX: "Раздзел"
STR_UPLOAD: "Адправіць"
STR_BOOK_S_STYLE: "Стыль кнігі"
STR_EMBEDDED_STYLE: "Убудаваны стыль"
+STR_FOCUS_READING: "Фокуснае чытанне"
STR_OPDS_SERVER_URL: "URL OPDS сервера"
STR_SCREENSHOT_BUTTON: "Зрабіць здымак экрана"
STR_IMAGES: "Выявы"
diff --git a/lib/I18n/translations/catalan.yaml b/lib/I18n/translations/catalan.yaml
index 377ab05e..40b02ea0 100644
--- a/lib/I18n/translations/catalan.yaml
+++ b/lib/I18n/translations/catalan.yaml
@@ -287,6 +287,7 @@ STR_SECTION_PREFIX: "Secció "
STR_UPLOAD: "Puja"
STR_BOOK_S_STYLE: "Estil del llibre"
STR_EMBEDDED_STYLE: "Estil incrustat"
+STR_FOCUS_READING: "Lectura enfocada"
STR_OPDS_SERVER_URL: "URL del servidor OPDS"
STR_FOOTNOTES: "Notes al peu"
STR_NO_FOOTNOTES: "No hi ha notes al peu en aquesta pàgina"
diff --git a/lib/I18n/translations/czech.yaml b/lib/I18n/translations/czech.yaml
index a769aa07..379bd690 100644
--- a/lib/I18n/translations/czech.yaml
+++ b/lib/I18n/translations/czech.yaml
@@ -264,6 +264,7 @@ STR_SECTION_PREFIX: "Sekce"
STR_UPLOAD: "Nahrát"
STR_BOOK_S_STYLE: "Styl knihy"
STR_EMBEDDED_STYLE: "Vložený styl"
+STR_FOCUS_READING: "Soustředěné čtení"
STR_OPDS_SERVER_URL: "URL serveru OPDS"
STR_SCREENSHOT_BUTTON: "Udělat snímek obrazovky"
STR_TILT_PAGE_TURN: "Otáčení stránek nakloněním"
diff --git a/lib/I18n/translations/danish.yaml b/lib/I18n/translations/danish.yaml
index 91b13025..9a1b979d 100644
--- a/lib/I18n/translations/danish.yaml
+++ b/lib/I18n/translations/danish.yaml
@@ -287,6 +287,7 @@ STR_SECTION_PREFIX: "Afsnit "
STR_UPLOAD: "Upload"
STR_BOOK_S_STYLE: "Bogens stil"
STR_EMBEDDED_STYLE: "Indlejret stil"
+STR_FOCUS_READING: "Fokuslæsning"
STR_OPDS_SERVER_URL: "OPDS Server URL"
STR_FOOTNOTES: "Fodnoter"
STR_NO_FOOTNOTES: "Ingen fodnoter på denne side"
diff --git a/lib/I18n/translations/dutch.yaml b/lib/I18n/translations/dutch.yaml
index 47041116..12593f0a 100644
--- a/lib/I18n/translations/dutch.yaml
+++ b/lib/I18n/translations/dutch.yaml
@@ -287,6 +287,7 @@ STR_SECTION_PREFIX: "Sectie "
STR_UPLOAD: "Uploaden"
STR_BOOK_S_STYLE: "Stijl van boek"
STR_EMBEDDED_STYLE: "Ingebedde stijl"
+STR_FOCUS_READING: "Gefocust lezen"
STR_OPDS_SERVER_URL: "OPDS-server URL"
STR_FOOTNOTES: "Voetnoten"
STR_NO_FOOTNOTES: "Geen voetnoten op deze pagina"
diff --git a/lib/I18n/translations/english.yaml b/lib/I18n/translations/english.yaml
index 969af61c..ab9917f1 100644
--- a/lib/I18n/translations/english.yaml
+++ b/lib/I18n/translations/english.yaml
@@ -296,6 +296,7 @@ STR_SECTION_PREFIX: "Section "
STR_UPLOAD: "Upload"
STR_BOOK_S_STYLE: "Book's Style"
STR_EMBEDDED_STYLE: "Embedded Style"
+STR_FOCUS_READING: "Focus Reading"
STR_OPDS_SERVER_URL: "OPDS Server URL"
STR_SET_SLEEP_COVER: "Set Cover"
STR_FOOTNOTES: "Footnotes"
@@ -310,8 +311,8 @@ STR_DELETE_CONFIRM: "Delete this server?"
STR_OPDS_SERVERS: "OPDS Servers"
STR_AUTO_TURN_ENABLED: "Auto Turn Enabled: "
STR_AUTO_TURN_PAGES_PER_MIN: "Auto Turn (Pages Per Minute)"
-STR_DOWNLOAD_FONTS: "Download Fonts"
-STR_FONT_DOWNLOAD: "Font Download"
+STR_MANAGE_FONTS: "Manage Fonts"
+STR_FONT_BROWSER: "Font Browser"
STR_LOADING_FONT_LIST: "Loading font list..."
STR_NO_FONTS_AVAILABLE: "No fonts available"
STR_FONT_INSTALLED: "Font installed!"
@@ -322,7 +323,8 @@ STR_SD_CARD_FULL: "Insufficient SD card space"
STR_FILES_LABEL: "Files: "
STR_SIZE_LABEL: "Size: "
STR_REDOWNLOAD: "Re-download"
-STR_DOWNLOAD_ALL: "Download / Update All"
+STR_DOWNLOAD_ALL: "Download All"
+STR_UPDATE_ALL: "Update All"
STR_ALL_FONTS_INSTALLED: "All fonts installed!"
STR_UPDATE_AVAILABLE: "Update"
STR_CRASH_TITLE: "System Crash"
diff --git a/lib/I18n/translations/finnish.yaml b/lib/I18n/translations/finnish.yaml
index 99a4d974..93672291 100644
--- a/lib/I18n/translations/finnish.yaml
+++ b/lib/I18n/translations/finnish.yaml
@@ -262,6 +262,7 @@ STR_SECTION_PREFIX: "Osio "
STR_UPLOAD: "Lähetä"
STR_BOOK_S_STYLE: "Kirjan tyyli"
STR_EMBEDDED_STYLE: "Upotettu tyyli"
+STR_FOCUS_READING: "Keskittynyt lukeminen"
STR_OPDS_SERVER_URL: "OPDS-palvelimen osoite"
STR_SCREENSHOT_BUTTON: "Ota kuvakaappaus"
STR_TILT_PAGE_TURN: "Sivunkääntö kallistamalla"
diff --git a/lib/I18n/translations/french.yaml b/lib/I18n/translations/french.yaml
index fb299ab7..0f43e82b 100644
--- a/lib/I18n/translations/french.yaml
+++ b/lib/I18n/translations/french.yaml
@@ -288,6 +288,7 @@ STR_SECTION_PREFIX: "Section "
STR_UPLOAD: "Envoyer"
STR_BOOK_S_STYLE: "Style du livre"
STR_EMBEDDED_STYLE: "Style intégré"
+STR_FOCUS_READING: "Lecture focalisée"
STR_OPDS_SERVER_URL: "URL serveur OPDS"
STR_FOOTNOTES: "Notes de bas de page"
STR_NO_FOOTNOTES: "Aucune note sur cette page"
diff --git a/lib/I18n/translations/german.yaml b/lib/I18n/translations/german.yaml
index 28312b81..d2f8d0e6 100644
--- a/lib/I18n/translations/german.yaml
+++ b/lib/I18n/translations/german.yaml
@@ -289,6 +289,7 @@ STR_SECTION_PREFIX: "Abschnitt"
STR_UPLOAD: "Hochladen"
STR_BOOK_S_STYLE: "Buch-Stil"
STR_EMBEDDED_STYLE: "Eingebetteter Stil"
+STR_FOCUS_READING: "Fokus-Lesen"
STR_OPDS_SERVER_URL: "OPDS-Server-URL"
STR_SCREENSHOT_BUTTON: "Screenshot aufnehmen"
STR_FOOTNOTES: "Fußnoten"
diff --git a/lib/I18n/translations/hungarian.yaml b/lib/I18n/translations/hungarian.yaml
index 759768aa..070f2a28 100644
--- a/lib/I18n/translations/hungarian.yaml
+++ b/lib/I18n/translations/hungarian.yaml
@@ -284,6 +284,7 @@ STR_SECTION_PREFIX: "Szakasz "
STR_UPLOAD: "Feltöltés"
STR_BOOK_S_STYLE: "Könyv stílusa"
STR_EMBEDDED_STYLE: "Beágyazott stílus"
+STR_FOCUS_READING: "Fókuszált olvasás"
STR_OPDS_SERVER_URL: "OPDS szerver URL"
STR_FOOTNOTES: "Lábjegyzetek"
STR_NO_FOOTNOTES: "Nincsenek lábjegyzetek ezen az oldalon"
diff --git a/lib/I18n/translations/italian.yaml b/lib/I18n/translations/italian.yaml
index 3c3bb17b..4a180b4f 100644
--- a/lib/I18n/translations/italian.yaml
+++ b/lib/I18n/translations/italian.yaml
@@ -288,6 +288,7 @@ STR_SECTION_PREFIX: "Sezione "
STR_UPLOAD: "Carica"
STR_BOOK_S_STYLE: "Stile libro"
STR_EMBEDDED_STYLE: "Stile integrato dell'epub"
+STR_FOCUS_READING: "Lettura focalizzata"
STR_OPDS_SERVER_URL: "Server OPDS"
STR_FOOTNOTES: "Note a piè pagina"
STR_NO_FOOTNOTES: "Nessuna nota in questa pagina"
diff --git a/lib/I18n/translations/kazakh.yaml b/lib/I18n/translations/kazakh.yaml
index fbee8021..5d9c03e9 100644
--- a/lib/I18n/translations/kazakh.yaml
+++ b/lib/I18n/translations/kazakh.yaml
@@ -258,6 +258,7 @@ STR_SECTION_PREFIX: "Бөлім "
STR_UPLOAD: "Жүктеп салу"
STR_BOOK_S_STYLE: "Кітап стилі"
STR_EMBEDDED_STYLE: "Кірістірілген стиль"
+STR_FOCUS_READING: "Зейінді оқу"
STR_OPDS_SERVER_URL: "OPDS сервері URL"
STR_NO_FILES_FOUND: "Файлдар табылмады"
STR_IMAGES: "Суреттер"
diff --git a/lib/I18n/translations/lithuanian.yaml b/lib/I18n/translations/lithuanian.yaml
index bae0764a..d7619a1f 100644
--- a/lib/I18n/translations/lithuanian.yaml
+++ b/lib/I18n/translations/lithuanian.yaml
@@ -284,6 +284,7 @@ STR_SECTION_PREFIX: "Dalis "
STR_UPLOAD: "Įkelti"
STR_BOOK_S_STYLE: "Knygos stilius"
STR_EMBEDDED_STYLE: "Integruotas stilius"
+STR_FOCUS_READING: "Sufokusuotas skaitymas"
STR_OPDS_SERVER_URL: "OPDS URL"
STR_FOOTNOTES: "Išnašos"
STR_NO_FOOTNOTES: "Šiame psl. išnašų nėra"
diff --git a/lib/I18n/translations/polish.yaml b/lib/I18n/translations/polish.yaml
index b85a8719..384dbac8 100644
--- a/lib/I18n/translations/polish.yaml
+++ b/lib/I18n/translations/polish.yaml
@@ -296,6 +296,7 @@ STR_SECTION_PREFIX: "Sekcja "
STR_UPLOAD: "Wyślij"
STR_BOOK_S_STYLE: "Styl książki"
STR_EMBEDDED_STYLE: "Style wbudowane w EPUB"
+STR_FOCUS_READING: "Czytanie skupione"
STR_OPDS_SERVER_URL: "URL serwera OPDS"
STR_SET_SLEEP_COVER: "Ustaw okładkę"
STR_FOOTNOTES: "Przypisy"
@@ -322,7 +323,8 @@ 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_DOWNLOAD_ALL: "Pobierz wszystkie"
+STR_UPDATE_ALL: "Uaktualnij wszystkie"
STR_ALL_FONTS_INSTALLED: "Wszystkie czcionki zainstalowane!"
STR_UPDATE_AVAILABLE: "Uaktualnij"
STR_CRASH_TITLE: "Awaria systemu"
diff --git a/lib/I18n/translations/portuguese.yaml b/lib/I18n/translations/portuguese.yaml
index e6249adb..97b447b9 100644
--- a/lib/I18n/translations/portuguese.yaml
+++ b/lib/I18n/translations/portuguese.yaml
@@ -264,6 +264,7 @@ STR_SECTION_PREFIX: "Seção"
STR_UPLOAD: "Enviar"
STR_BOOK_S_STYLE: "Estilo do livro"
STR_EMBEDDED_STYLE: "Estilo embutido"
+STR_FOCUS_READING: "Leitura focada"
STR_OPDS_SERVER_URL: "URL do servidor OPDS"
STR_SCREENSHOT_BUTTON: "Capturar tela"
STR_TILT_PAGE_TURN: "Virar página por inclinação"
diff --git a/lib/I18n/translations/romanian.yaml b/lib/I18n/translations/romanian.yaml
index d4e004db..84d7cac9 100644
--- a/lib/I18n/translations/romanian.yaml
+++ b/lib/I18n/translations/romanian.yaml
@@ -287,6 +287,7 @@ STR_SECTION_PREFIX: "Secţiune "
STR_UPLOAD: "Încărcare"
STR_BOOK_S_STYLE: "Stilul cărţii"
STR_EMBEDDED_STYLE: "Stil încorporat"
+STR_FOCUS_READING: "Lectură concentrată"
STR_OPDS_SERVER_URL: "URL server OPDS"
STR_FOOTNOTES: "Note de subsol"
STR_NO_FOOTNOTES: "Nicio notă de subsol"
diff --git a/lib/I18n/translations/russian.yaml b/lib/I18n/translations/russian.yaml
index 6f00d6c3..005a5eea 100644
--- a/lib/I18n/translations/russian.yaml
+++ b/lib/I18n/translations/russian.yaml
@@ -291,6 +291,7 @@ STR_SECTION_PREFIX: "Раздел "
STR_UPLOAD: "Отправить"
STR_BOOK_S_STYLE: "Стиль книги"
STR_EMBEDDED_STYLE: "Встроенный стиль"
+STR_FOCUS_READING: "Фокусное чтение"
STR_OPDS_SERVER_URL: "URL OPDS сервера"
STR_SCREENSHOT_BUTTON: "Сделать снимок экрана"
STR_AUTO_TURN_ENABLED: "Автоперелистывание: "
diff --git a/lib/I18n/translations/slovenian.yaml b/lib/I18n/translations/slovenian.yaml
index 20103772..7e506661 100644
--- a/lib/I18n/translations/slovenian.yaml
+++ b/lib/I18n/translations/slovenian.yaml
@@ -284,6 +284,7 @@ STR_SECTION_PREFIX: "Razdelek "
STR_UPLOAD: "Naloži"
STR_BOOK_S_STYLE: "Slog knjige"
STR_EMBEDDED_STYLE: "Vgrajen slog"
+STR_FOCUS_READING: "Fokusirano branje"
STR_OPDS_SERVER_URL: "URL OPDS strežnika"
STR_FOOTNOTES: "Opombe"
STR_NO_FOOTNOTES: "Na tej strani ni opomb"
diff --git a/lib/I18n/translations/spanish.yaml b/lib/I18n/translations/spanish.yaml
index ea2e56dc..9b4af843 100644
--- a/lib/I18n/translations/spanish.yaml
+++ b/lib/I18n/translations/spanish.yaml
@@ -288,6 +288,7 @@ STR_SECTION_PREFIX: "Secc.:"
STR_UPLOAD: "Subir"
STR_BOOK_S_STYLE: "Estilo del libro"
STR_EMBEDDED_STYLE: "Estilo integrado"
+STR_FOCUS_READING: "Lectura enfocada"
STR_OPDS_SERVER_URL: "URL del servidor OPDS"
STR_FOOTNOTES: "Pie de página"
STR_NO_FOOTNOTES: "No hay notas al pie de esta página"
diff --git a/lib/I18n/translations/swedish.yaml b/lib/I18n/translations/swedish.yaml
index 84ac108e..9a71fd32 100644
--- a/lib/I18n/translations/swedish.yaml
+++ b/lib/I18n/translations/swedish.yaml
@@ -296,6 +296,7 @@ STR_SECTION_PREFIX: "Sektion"
STR_UPLOAD: "Uppladdning"
STR_BOOK_S_STYLE: "Bokstil"
STR_EMBEDDED_STYLE: "Inbäddad stil"
+STR_FOCUS_READING: "Fokusläsning"
STR_OPDS_SERVER_URL: "OPDS-serveradress"
STR_SET_SLEEP_COVER: "Ställ in omslag"
STR_FOOTNOTES: "Fotnoter"
@@ -310,8 +311,8 @@ 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_MANAGE_FONTS: "Hantera teckensnitt"
+STR_FONT_BROWSER: "Teckensnittsbläddrare"
STR_LOADING_FONT_LIST: "Laddar teckensnittslista..."
STR_NO_FONTS_AVAILABLE: "Inga teckensnitt tillgängliga"
STR_FONT_INSTALLED: "Teckensnitt installerat!"
@@ -322,7 +323,8 @@ 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_DOWNLOAD_ALL: "Ladda ner alla"
+STR_UPDATE_ALL: "Uppdatera alla"
STR_ALL_FONTS_INSTALLED: "Alla teckensnitt installerade!"
STR_UPDATE_AVAILABLE: "Uppdatering"
STR_CRASH_TITLE: "Systemkrasch"
diff --git a/lib/I18n/translations/turkish.yaml b/lib/I18n/translations/turkish.yaml
index 066af673..6e0784ce 100644
--- a/lib/I18n/translations/turkish.yaml
+++ b/lib/I18n/translations/turkish.yaml
@@ -262,6 +262,7 @@ STR_SECTION_PREFIX: "Bölüm "
STR_UPLOAD: "Yükle"
STR_BOOK_S_STYLE: "Kitabın Stili"
STR_EMBEDDED_STYLE: "Gömülü Stil"
+STR_FOCUS_READING: "Odaklanmış Okuma"
STR_OPDS_SERVER_URL: "OPDS Sunucu Adresi"
STR_AUTO_TURN_ENABLED: "Otomatik Çevirme Etkin: "
STR_AUTO_TURN_PAGES_PER_MIN: "Otomatik Çevirme (Dakikada Sayfa)"
diff --git a/lib/I18n/translations/ukrainian.yaml b/lib/I18n/translations/ukrainian.yaml
index dfe0d019..2797f931 100644
--- a/lib/I18n/translations/ukrainian.yaml
+++ b/lib/I18n/translations/ukrainian.yaml
@@ -87,7 +87,7 @@ STR_TIME_TO_SLEEP: "Перехід в режим сну"
STR_SHOW_HIDDEN_FILES: "Показати приховані файли"
STR_REFRESH_FREQ: "Частота оновлення екрану"
STR_KOREADER_SYNC: "Синхронізація KOReader"
-STR_CHECK_UPDATES: "Перевірити оновлення"
+STR_CHECK_UPDATES: "Перевірити оновлення системи"
STR_LANGUAGE: "Мова"
STR_CLEAR_READING_CACHE: "Очистити кеш книг"
STR_USERNAME: "Ім'я користувача"
@@ -171,6 +171,7 @@ STR_NO_UPDATE: "Оновлень немає"
STR_UPDATE_FAILED: "Оновлення не вдалося"
STR_UPDATE_COMPLETE: "Оновлення завершено"
STR_POWER_ON_HINT: "Натисніть і утримуйте кнопку живлення, щоб увімкнути"
+STR_RESTARTING_HINT: "Перезавантаження... Якщо пристрій не перезавантажується, утримуйте кнопку живлення кілька секунд."
STR_NO_ENTRIES: "Записів не знайдено"
STR_DOWNLOADING: "Завантаження..."
STR_DOWNLOAD_FAILED: "Завантаження не вдалося"
@@ -228,6 +229,9 @@ STR_EXAMPLE_BOOK: "Назва книги"
STR_PREVIEW: "Перегляд"
STR_TITLE: "Назва"
STR_BATTERY: "Акумулятор"
+STR_XTC_STATUS_BAR: "XTC Рядок прогресу"
+STR_BOTTOM: "Низ"
+STR_TOP: "Верх"
STR_UI_THEME: "Тема інтерфейсу"
STR_THEME_CLASSIC: "Класична"
STR_THEME_LYRA: "Lyra"
@@ -292,7 +296,9 @@ STR_SECTION_PREFIX: "Розділ "
STR_UPLOAD: "Завантажити"
STR_BOOK_S_STYLE: "Стиль книги"
STR_EMBEDDED_STYLE: "Вбудований стиль"
+STR_FOCUS_READING: "Фокусне читання"
STR_OPDS_SERVER_URL: "URL сервера OPDS"
+STR_SET_SLEEP_COVER: "Як обкл."
STR_FOOTNOTES: "Примітки"
STR_NO_FOOTNOTES: "На цій сторінці немає приміток"
STR_LINK: "[посилання]"
@@ -305,6 +311,22 @@ STR_DELETE_CONFIRM: "Видалити цей сервер?"
STR_OPDS_SERVERS: "Сервери OPDS"
STR_AUTO_TURN_ENABLED: "Автоперегортання увімк: "
STR_AUTO_TURN_PAGES_PER_MIN: "Автоперегортання (ст/хв)"
+STR_MANAGE_FONTS: "Керування шрифтами"
+STR_FONT_BROWSER: "Перегляд шрифтів"
+STR_LOADING_FONT_LIST: "Оновлення списку шрифтів..."
+STR_NO_FONTS_AVAILABLE: "Шрифти недоступні"
+STR_FONT_INSTALLED: "Шрифт встановлено!"
+STR_FONT_INSTALL_FAILED: "Не вдалося встановити шрифт"
+STR_INSTALLED: "Встановлено"
+STR_CONFIRM_DOWNLOAD_PROMPT: "Завантажити?"
+STR_SD_CARD_FULL: "Недостатньо місця на SD-карті"
+STR_FILES_LABEL: "Файли: "
+STR_SIZE_LABEL: "Розмір: "
+STR_REDOWNLOAD: "Завантажити повторно"
+STR_DOWNLOAD_ALL: "Завантажити все"
+STR_UPDATE_ALL: "Оновити все"
+STR_ALL_FONTS_INSTALLED: "Всі шрифти встановлено!"
+STR_UPDATE_AVAILABLE: "Оновити"
STR_CRASH_TITLE: "Збій Системи"
STR_CRASH_DESCRIPTION: "Дані про збій збережено в crash_report.txt. Додайте цей файл до вашого звіту про помилку."
STR_CRASH_REASON: "Причина збою:"
@@ -325,3 +347,16 @@ STR_KB_HINT_SECONDARY_CHAR: "Затисніть ВИБРАТИ для додат
STR_KB_HINT_UPPER_SECONDARY: "Затисніть ВИБРАТИ для ВЕЛИКИХ літер / символів"
STR_KB_HINT_LOWER_SECONDARY: "Затисніть ВИБРАТИ для малих літер / символів"
STR_KB_HINT_URL_SNIPPETS: "Натисніть URL для вибору шаблонів"
+STR_SD_FIRMWARE_UPDATE: "Оновлення системи з SD-карти"
+STR_SELECT_FIRMWARE_FILE: "Оберіть файл оновлення (.bin)"
+STR_NO_BIN_FILES: "Не знайдено .bin файлів"
+STR_VALIDATING_FIRMWARE: "Перевірка цілісності файлу..."
+STR_INVALID_FIRMWARE: "Невірний файл оновлення системи"
+STR_FIRMWARE_TOO_LARGE: "Файл оновлення системи не вміщується в розділ пам'яті"
+STR_FIRMWARE_TOO_SMALL: "Файл оновлення системи занадто малий"
+STR_FIRMWARE_UPDATE_PROMPT: "Оновити систему?"
+STR_FIRMWARE_FILE_OPEN_FAILED: "Неможливо відкрити файл"
+STR_FIRMWARE_WRITE_FAILED: "Помилка запису прошивки"
+STR_FIRMWARE_UPDATE_DO_NOT_POWER_OFF: "Не вимикайте пристрій!"
+STR_RECOVERY_MODE: "Режим відновлення"
+STR_RECOVERY_MODE_HINT: "Помістіть firmware.bin у корінь SD-карти та виберіть його"
diff --git a/open-x4-sdk b/open-x4-sdk
index 7d86603a..a64a3c29 160000
--- a/open-x4-sdk
+++ b/open-x4-sdk
@@ -1 +1 @@
-Subproject commit 7d86603ad27709a9a766bb5ad893cfc39e60777e
+Subproject commit a64a3c29bebc59b2ccdfe15492cfc4b5e4c26360
diff --git a/scripts/convert_icon.py b/scripts/convert_icon.py
index 56ea7d4b..6e20fc8c 100644
--- a/scripts/convert_icon.py
+++ b/scripts/convert_icon.py
@@ -1,18 +1,21 @@
-import sys
import os
-from PIL import Image
-import cairosvg
import io
+import sys
threshold = 128
+USAGE = 'Usage: python scripts/convert_icon.py input.png|input.svg output_name width height'
def svg_to_png_bytes(svg_path, width, height):
+ import cairosvg
+
with open(svg_path, 'rb') as f:
svg_data = f.read()
png_bytes = cairosvg.svg2png(bytestring=svg_data, output_width=width, output_height=height)
return png_bytes
def load_image(path, width, height):
+ from PIL import Image
+
ext = os.path.splitext(path)[1].lower()
if ext == '.svg':
png_bytes = svg_to_png_bytes(path, width, height)
@@ -58,8 +61,11 @@ def image_to_c_array(img, array_name):
return c
def main():
- if len(sys.argv) < 5:
- print('Usage: python convert_image.py input.png output_name width height')
+ if any(arg in ('-h', '--help') for arg in sys.argv[1:]):
+ print(USAGE)
+ sys.exit(0)
+ if len(sys.argv) != 5:
+ print(USAGE)
sys.exit(1)
input_path, output_name, width, height = sys.argv[1:5]
array_name = output_name.capitalize() + 'Icon'
@@ -77,4 +83,4 @@ def main():
print(f'Wrote {output_path}')
if __name__ == '__main__':
- main()
\ No newline at end of file
+ main()
diff --git a/scripts/debugging_monitor.py b/scripts/debugging_monitor.py
index 30c3533e..fa2b8547 100755
--- a/scripts/debugging_monitor.py
+++ b/scripts/debugging_monitor.py
@@ -35,6 +35,43 @@ import threading
from collections import deque
from datetime import datetime
+DEFAULT_BAUDRATE = 115200
+
+
+def build_arg_parser() -> argparse.ArgumentParser:
+ parser = argparse.ArgumentParser(
+ description="ESP32 Serial Monitor with Memory Graph - Real-time monitoring, graphing, and command interface"
+ )
+ parser.add_argument(
+ "port",
+ nargs="?",
+ default=None,
+ help="Serial port (leave empty for autodetection)",
+ )
+ parser.add_argument(
+ "--baud",
+ type=int,
+ default=DEFAULT_BAUDRATE,
+ help=f"Baud rate (default: {DEFAULT_BAUDRATE})",
+ )
+ parser.add_argument(
+ "--filter",
+ type=str,
+ default="",
+ help="Only display lines containing this keyword (case-insensitive)",
+ )
+ parser.add_argument(
+ "--suppress",
+ type=str,
+ default="",
+ help="Suppress lines containing this keyword (case-insensitive)",
+ )
+ return parser
+
+
+if any(arg in ("-h", "--help") for arg in sys.argv[1:]):
+ build_arg_parser().parse_args()
+
# Try to import potentially missing packages
PACKAGE_MAPPING: dict[str, str] = {
"serial": "pyserial",
@@ -401,34 +438,7 @@ def main() -> None:
- Screenshot capture capability
- Graceful shutdown on Ctrl-C or window close
"""
- parser = argparse.ArgumentParser(
- description="ESP32 Serial Monitor with Memory Graph - Real-time monitoring, graphing, and command interface"
- )
- default_baudrate = 115200
- parser.add_argument(
- "port",
- nargs="?",
- default=None,
- help="Serial port (leave empty for autodetection)",
- )
- parser.add_argument(
- "--baud",
- type=int,
- default=default_baudrate,
- help=f"Baud rate (default: {default_baudrate})",
- )
- parser.add_argument(
- "--filter",
- type=str,
- default="",
- help="Only display lines containing this keyword (case-insensitive)",
- )
- parser.add_argument(
- "--suppress",
- type=str,
- default="",
- help="Suppress lines containing this keyword (case-insensitive)",
- )
+ parser = build_arg_parser()
args = parser.parse_args()
port = args.port
if port is None:
diff --git a/scripts/generate-font-manifest.py b/scripts/generate-font-manifest.py
index e962c162..80ef73f8 100755
--- a/scripts/generate-font-manifest.py
+++ b/scripts/generate-font-manifest.py
@@ -16,6 +16,8 @@ The input directory may be flat (all .cpfont files in one dir) or nested
convention _.cpfont.
"""
+from __future__ import annotations
+
import argparse
import json
import os
diff --git a/scripts/generate_kerning_ligature_epub.py b/scripts/generate_kerning_ligature_epub.py
index ae3c9f66..1ba640ca 100644
--- a/scripts/generate_kerning_ligature_epub.py
+++ b/scripts/generate_kerning_ligature_epub.py
@@ -137,10 +137,15 @@ Also includes:
import io
import os
+import sys
import zipfile
import uuid
from datetime import datetime
+if any(arg in ("-h", "--help") for arg in sys.argv[1:]):
+ print(__doc__.strip())
+ sys.exit(0)
+
try:
from PIL import Image, ImageDraw, ImageFont
except ImportError:
diff --git a/scripts/generate_test_bmps.py b/scripts/generate_test_bmps.py
index 0e0c7894..2fbdd630 100644
--- a/scripts/generate_test_bmps.py
+++ b/scripts/generate_test_bmps.py
@@ -326,4 +326,7 @@ def main():
if __name__ == '__main__':
+ if any(arg in ("-h", "--help") for arg in sys.argv[1:]):
+ print(__doc__.strip())
+ sys.exit(0)
main()
diff --git a/scripts/generate_test_epub.py b/scripts/generate_test_epub.py
index e585420c..ae97708a 100644
--- a/scripts/generate_test_epub.py
+++ b/scripts/generate_test_epub.py
@@ -11,9 +11,14 @@ Creates EPUBs with annotated JPEG and PNG images to verify:
"""
import os
+import sys
import zipfile
from pathlib import Path
+if any(arg in ("-h", "--help") for arg in sys.argv[1:]):
+ print(__doc__.strip())
+ sys.exit(0)
+
try:
from PIL import Image, ImageDraw, ImageFont
except ImportError:
diff --git a/scripts/update_hypenation.sh b/scripts/update_hyphenation.sh
similarity index 100%
rename from scripts/update_hypenation.sh
rename to scripts/update_hyphenation.sh
diff --git a/src/CrossPointSettings.h b/src/CrossPointSettings.h
index bd6a8502..0b769005 100644
--- a/src/CrossPointSettings.h
+++ b/src/CrossPointSettings.h
@@ -213,6 +213,8 @@ class CrossPointSettings {
uint8_t fadingFix = 0;
// Use book's embedded CSS styles for EPUB rendering (1 = enabled, 0 = disabled)
uint8_t embeddedStyle = 1;
+ // Focus Reading - emphasizes the first part of words with bold
+ uint8_t focusReadingEnabled = 0;
// SD card font family name (empty = use built-in fontFamily)
char sdFontFamilyName[32] = "";
// Show hidden files/directories (starting with '.') in the file browser (0 = hidden, 1 = show)
diff --git a/src/SdCardFontSystem.cpp b/src/SdCardFontSystem.cpp
index 8e59d6ed..b771bede 100644
--- a/src/SdCardFontSystem.cpp
+++ b/src/SdCardFontSystem.cpp
@@ -5,15 +5,10 @@
#include "CrossPointSettings.h"
-// 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() {
+static uint8_t fontSizeEnumFromSettings() {
uint8_t e = SETTINGS.fontSize;
if (e >= CrossPointSettings::FONT_SIZE_COUNT) e = 1; // default to MEDIUM
- return FONT_SIZE_TO_PT[e];
+ return e;
}
void SdCardFontSystem::begin(GfxRenderer& renderer) {
@@ -30,7 +25,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, targetPtSizeFromSettings())) {
+ if (manager_.loadFamily(*family, renderer, fontSizeEnumFromSettings())) {
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);
@@ -58,7 +53,7 @@ void SdCardFontSystem::ensureLoaded(GfxRenderer& renderer) {
const char* wantedFamily = SETTINGS.sdFontFamilyName;
const std::string& currentFamily = manager_.currentFamilyName();
- const uint8_t targetPt = targetPtSizeFromSettings();
+ const uint8_t sizeEnum = fontSizeEnumFromSettings();
if (wantedFamily[0] == '\0') {
if (!currentFamily.empty()) {
@@ -67,8 +62,8 @@ void SdCardFontSystem::ensureLoaded(GfxRenderer& renderer) {
return;
}
- // 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
+ // 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
// just rediscovered (file may have been replaced on disk).
bool familyMatches = (currentFamily == wantedFamily);
if (familyMatches) {
@@ -79,11 +74,13 @@ void SdCardFontSystem::ensureLoaded(GfxRenderer& renderer) {
SETTINGS.sdFontFamilyName[0] = '\0';
return;
}
- 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]" : "");
+ 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]" : "");
}
if (!currentFamily.empty()) {
@@ -92,7 +89,7 @@ void SdCardFontSystem::ensureLoaded(GfxRenderer& renderer) {
const auto* family = registry_.findFamily(wantedFamily);
if (family) {
- if (manager_.loadFamily(*family, renderer, targetPt)) {
+ if (manager_.loadFamily(*family, renderer, sizeEnum)) {
LOG_DBG("SDFS", "Loaded SD font family: %s", wantedFamily);
} else {
LOG_ERR("SDFS", "Failed to load SD font family: %s (clearing)", wantedFamily);
diff --git a/src/SettingsList.h b/src/SettingsList.h
index 9af7101e..d8a118c3 100644
--- a/src/SettingsList.h
+++ b/src/SettingsList.h
@@ -145,6 +145,8 @@ inline std::vector getSettingsList(const SdCardFontRegistry* regist
"paragraphAlignment", StrId::STR_CAT_READER),
SettingInfo::Toggle(StrId::STR_EMBEDDED_STYLE, &CrossPointSettings::embeddedStyle, "embeddedStyle",
StrId::STR_CAT_READER),
+ SettingInfo::Toggle(StrId::STR_FOCUS_READING, &CrossPointSettings::focusReadingEnabled, "focusReadingEnabled",
+ StrId::STR_CAT_READER),
SettingInfo::Toggle(StrId::STR_HYPHENATION, &CrossPointSettings::hyphenationEnabled, "hyphenationEnabled",
StrId::STR_CAT_READER),
SettingInfo::Enum(StrId::STR_ORIENTATION, &CrossPointSettings::orientation,
diff --git a/src/activities/reader/EpubReaderActivity.cpp b/src/activities/reader/EpubReaderActivity.cpp
index 3de6d69d..7732762b 100644
--- a/src/activities/reader/EpubReaderActivity.cpp
+++ b/src/activities/reader/EpubReaderActivity.cpp
@@ -609,7 +609,7 @@ void EpubReaderActivity::render(RenderLock&& lock) {
if (!section->loadSectionFile(SETTINGS.getReaderFontId(), SETTINGS.getReaderLineCompression(),
SETTINGS.extraParagraphSpacing, SETTINGS.paragraphAlignment, viewportWidth,
viewportHeight, SETTINGS.hyphenationEnabled, SETTINGS.embeddedStyle,
- SETTINGS.imageRendering)) {
+ SETTINGS.imageRendering, SETTINGS.focusReadingEnabled)) {
LOG_DBG("ERS", "Cache not found, building...");
GUI.drawPopup(renderer, tr(STR_INDEXING));
@@ -619,7 +619,7 @@ void EpubReaderActivity::render(RenderLock&& lock) {
if (!section->createSectionFile(SETTINGS.getReaderFontId(), SETTINGS.getReaderLineCompression(),
SETTINGS.extraParagraphSpacing, SETTINGS.paragraphAlignment, viewportWidth,
viewportHeight, SETTINGS.hyphenationEnabled, SETTINGS.embeddedStyle,
- SETTINGS.imageRendering, popupFn)) {
+ SETTINGS.imageRendering, SETTINGS.focusReadingEnabled, popupFn)) {
LOG_ERR("ERS", "Failed to persist page data to SD");
section.reset();
showPendingSyncSaveError();
@@ -750,7 +750,7 @@ void EpubReaderActivity::silentIndexNextChapterIfNeeded(const uint16_t viewportW
if (nextSection.loadSectionFile(SETTINGS.getReaderFontId(), SETTINGS.getReaderLineCompression(),
SETTINGS.extraParagraphSpacing, SETTINGS.paragraphAlignment, viewportWidth,
viewportHeight, SETTINGS.hyphenationEnabled, SETTINGS.embeddedStyle,
- SETTINGS.imageRendering)) {
+ SETTINGS.imageRendering, SETTINGS.focusReadingEnabled)) {
return;
}
@@ -758,7 +758,7 @@ void EpubReaderActivity::silentIndexNextChapterIfNeeded(const uint16_t viewportW
if (!nextSection.createSectionFile(SETTINGS.getReaderFontId(), SETTINGS.getReaderLineCompression(),
SETTINGS.extraParagraphSpacing, SETTINGS.paragraphAlignment, viewportWidth,
viewportHeight, SETTINGS.hyphenationEnabled, SETTINGS.embeddedStyle,
- SETTINGS.imageRendering)) {
+ SETTINGS.imageRendering, SETTINGS.focusReadingEnabled)) {
LOG_ERR("ERS", "Failed silent indexing for chapter: %d", nextSpineIndex);
}
}
diff --git a/src/activities/settings/FontDownloadActivity.cpp b/src/activities/settings/FontDownloadActivity.cpp
index ce18ce67..0acba062 100644
--- a/src/activities/settings/FontDownloadActivity.cpp
+++ b/src/activities/settings/FontDownloadActivity.cpp
@@ -11,6 +11,7 @@
#include "MappedInputManager.h"
#include "SdCardFontGlobals.h"
#include "activities/network/WifiSelectionActivity.h"
+#include "activities/util/ConfirmationActivity.h"
#include "components/UITheme.h"
#include "fontIds.h"
#include "network/HttpDownloader.h"
@@ -106,6 +107,7 @@ bool FontDownloadActivity::fetchAndParseManifest() {
baseUrl_ = doc["baseUrl"] | "";
families_.clear();
+ fontInstaller_.refreshRegistry();
JsonArray familiesArr = doc["families"].as();
families_.reserve(familiesArr.size());
@@ -171,7 +173,7 @@ bool FontDownloadActivity::fetchAndParseManifest() {
void FontDownloadActivity::downloadAll() {
for (size_t i = 0; i < families_.size(); i++) {
- if (families_[i].installed && !families_[i].hasUpdate) continue;
+ if (families_[i].installed) continue;
downloadFamily(families_[i]);
if (state_ == ERROR) return;
}
@@ -182,10 +184,59 @@ void FontDownloadActivity::downloadAll() {
}
}
-size_t FontDownloadActivity::totalUninstalledSize() const {
+void FontDownloadActivity::updateAll() {
+ for (size_t i = 0; i < families_.size(); i++) {
+ if (!families_[i].hasUpdate) continue;
+ downloadFamily(families_[i]);
+ if (state_ == ERROR) return;
+ }
+
+ {
+ RenderLock lock(*this);
+ state_ = COMPLETE;
+ }
+}
+
+bool FontDownloadActivity::showDownloadAllRow() const {
+ for (const auto& f : families_) {
+ if (!f.installed) return true;
+ }
+ return false;
+}
+
+bool FontDownloadActivity::showUpdateAllRow() const {
+ for (const auto& f : families_) {
+ if (f.hasUpdate) return true;
+ }
+ return false;
+}
+
+int FontDownloadActivity::specialRowCount() const {
+ return (showDownloadAllRow() ? 1 : 0) + (showUpdateAllRow() ? 1 : 0);
+}
+
+bool FontDownloadActivity::isDownloadAllRow(int index) const { return showDownloadAllRow() && index == 0; }
+
+bool FontDownloadActivity::isUpdateAllRow(int index) const {
+ return showUpdateAllRow() && index == (showDownloadAllRow() ? 1 : 0);
+}
+
+int FontDownloadActivity::listItemCount() const {
+ return families_.empty() ? 0 : static_cast(families_.size()) + specialRowCount();
+}
+
+size_t FontDownloadActivity::totalDownloadSize() const {
size_t total = 0;
for (const auto& f : families_) {
- if (!f.installed || f.hasUpdate) total += f.totalSize;
+ if (!f.installed) total += f.totalSize;
+ }
+ return total;
+}
+
+size_t FontDownloadActivity::totalUpdateSize() const {
+ size_t total = 0;
+ for (const auto& f : families_) {
+ if (f.hasUpdate) total += f.totalSize;
}
return total;
}
@@ -297,6 +348,7 @@ void FontDownloadActivity::downloadFamily(ManifestFamily& family) {
fontInstaller_.refreshRegistry();
family.installed = true;
+ family.hasUpdate = false;
{
RenderLock lock(*this);
@@ -304,6 +356,47 @@ void FontDownloadActivity::downloadFamily(ManifestFamily& family) {
}
}
+void FontDownloadActivity::promptDeleteSelectedFamily() {
+ const int pendingDeleteFamilyIndex = familyIndexFromList(selectedIndex_);
+ if (pendingDeleteFamilyIndex < 0 || pendingDeleteFamilyIndex >= static_cast(families_.size())) {
+ return;
+ }
+
+ std::string heading = tr(STR_DELETE);
+ const auto& family = families_[pendingDeleteFamilyIndex];
+ std::string body = family.name;
+ startActivityForResult(std::make_unique(renderer, mappedInput, heading, body),
+ [this](const ActivityResult& result) { onDeleteConfirmationResult(result); });
+}
+
+void FontDownloadActivity::onDeleteConfirmationResult(const ActivityResult& result) {
+ if (result.isCancelled) {
+ requestUpdate();
+ return;
+ }
+
+ auto& family = families_[familyIndexFromList(selectedIndex_)];
+
+ if (fontInstaller_.deleteFamily(family.name.c_str()) != FontInstaller::Error::OK) {
+ RenderLock lock(*this);
+ state_ = ERROR;
+ errorMessage_ = "Failed to delete font";
+ } else {
+ fontInstaller_.refreshRegistry();
+ family.installed = false;
+ family.hasUpdate = false;
+ }
+
+ requestUpdate();
+}
+
+bool FontDownloadActivity::isSelectedFamilyDeletable() const {
+ if (isDownloadAllRow(selectedIndex_) || isUpdateAllRow(selectedIndex_)) return false;
+ if (selectedIndex_ < specialRowCount() || selectedIndex_ >= listItemCount()) return false;
+ const auto& family = families_[familyIndexFromList(selectedIndex_)];
+ return family.installed && !family.hasUpdate;
+}
+
// --- Input handling ---
void FontDownloadActivity::loop() {
@@ -338,12 +431,17 @@ void FontDownloadActivity::loop() {
if (mappedInput.wasPressed(MappedInputManager::Button::Confirm)) {
if (!families_.empty()) {
- if (isDownloadAllSelected()) {
+ if (isDownloadAllRow(selectedIndex_)) {
downloadAll();
+ } else if (isUpdateAllRow(selectedIndex_)) {
+ updateAll();
} else {
- const auto& family = families_[familyIndexFromList(selectedIndex_)];
+ auto& family = families_[familyIndexFromList(selectedIndex_)];
if (!family.installed || family.hasUpdate) {
- downloadFamily(families_[familyIndexFromList(selectedIndex_)]);
+ downloadFamily(family);
+ } else {
+ promptDeleteSelectedFamily();
+ return;
}
}
requestUpdateAndWait();
@@ -403,7 +501,7 @@ void FontDownloadActivity::render(RenderLock&&) {
renderer.clearScreen();
- GUI.drawHeader(renderer, Rect{0, metrics.topPadding, pageWidth, metrics.headerHeight}, tr(STR_FONT_DOWNLOAD));
+ GUI.drawHeader(renderer, Rect{0, metrics.topPadding, pageWidth, metrics.headerHeight}, tr(STR_FONT_BROWSER));
const auto lineHeight = renderer.getLineHeight(UI_10_FONT_ID);
const auto contentTop = metrics.topPadding + metrics.headerHeight + metrics.verticalSpacing;
@@ -422,18 +520,21 @@ void FontDownloadActivity::render(RenderLock&&) {
Rect{0, contentTop, pageWidth, pageHeight - contentTop - metrics.buttonHintsHeight - metrics.verticalSpacing},
listItemCount(), selectedIndex_,
[this](int index) -> std::string {
- if (index == 0) {
- return std::string(tr(STR_DOWNLOAD_ALL)) + " (" + formatSize(totalUninstalledSize()) + ")";
+ if (isDownloadAllRow(index)) {
+ return std::string(tr(STR_DOWNLOAD_ALL)) + " (" + formatSize(totalDownloadSize()) + ")";
+ }
+ if (isUpdateAllRow(index)) {
+ return std::string(tr(STR_UPDATE_ALL)) + " (" + formatSize(totalUpdateSize()) + ")";
}
return families_[familyIndexFromList(index)].name;
},
[this](int index) -> std::string {
- if (index == 0) return "";
+ if (isDownloadAllRow(index) || isUpdateAllRow(index)) return "";
return families_[familyIndexFromList(index)].description;
},
nullptr,
[this](int index) -> std::string {
- if (index == 0) return "";
+ if (isDownloadAllRow(index) || isUpdateAllRow(index)) return "";
const auto& f = families_[familyIndexFromList(index)];
if (f.hasUpdate) return tr(STR_UPDATE_AVAILABLE);
if (f.installed) return tr(STR_INSTALLED);
@@ -441,12 +542,16 @@ void FontDownloadActivity::render(RenderLock&&) {
},
true,
[this](int index) -> bool {
- if (index == 0) return false;
+ if (isDownloadAllRow(index) || isUpdateAllRow(index)) return false;
const auto& f = families_[familyIndexFromList(index)];
return f.installed && !f.hasUpdate;
});
- const auto labels = mappedInput.mapLabels(tr(STR_BACK), tr(STR_DOWNLOAD), tr(STR_DIR_UP), tr(STR_DIR_DOWN));
+ const auto labels = mappedInput.mapLabels(tr(STR_BACK),
+ isSelectedFamilyDeletable() ? tr(STR_DELETE)
+ : isUpdateAllRow(selectedIndex_) ? tr(STR_UPDATE)
+ : tr(STR_DOWNLOAD),
+ tr(STR_DIR_UP), tr(STR_DIR_DOWN));
GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
}
} else if (state_ == DOWNLOADING) {
@@ -466,10 +571,6 @@ void FontDownloadActivity::render(RenderLock&&) {
renderer,
Rect{metrics.contentSidePadding, barY, pageWidth - metrics.contentSidePadding * 2, metrics.progressBarHeight},
static_cast(progress * 100), 100);
-
- int percentY = barY + metrics.progressBarHeight + metrics.verticalSpacing;
- renderer.drawCenteredText(UI_10_FONT_ID, percentY,
- (std::to_string(static_cast(progress * 100)) + "%").c_str());
} else if (state_ == COMPLETE) {
renderer.drawCenteredText(UI_10_FONT_ID, centerY, tr(STR_FONT_INSTALLED), true, EpdFontFamily::BOLD);
const auto labels = mappedInput.mapLabels(tr(STR_BACK), "", "", "");
diff --git a/src/activities/settings/FontDownloadActivity.h b/src/activities/settings/FontDownloadActivity.h
index 16d52fa9..7f76e7c6 100644
--- a/src/activities/settings/FontDownloadActivity.h
+++ b/src/activities/settings/FontDownloadActivity.h
@@ -84,10 +84,19 @@ class FontDownloadActivity : public Activity {
bool fetchAndParseManifest();
void downloadFamily(ManifestFamily& family);
void downloadAll();
+ void updateAll();
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(families_.size()) + 1; }
- size_t totalUninstalledSize() const;
+ bool showDownloadAllRow() const;
+ bool showUpdateAllRow() const;
+ int specialRowCount() const;
+ bool isDownloadAllRow(int index) const;
+ bool isUpdateAllRow(int index) const;
+ bool isSelectedFamilyDeletable() const;
+ void promptDeleteSelectedFamily();
+ void onDeleteConfirmationResult(const ActivityResult& result);
+ int familyIndexFromList(int listIndex) const { return listIndex - specialRowCount(); }
+ int listItemCount() const;
+ size_t totalDownloadSize() const;
+ size_t totalUpdateSize() const;
static std::string formatSize(size_t bytes);
};
diff --git a/src/activities/settings/OtaUpdateActivity.cpp b/src/activities/settings/OtaUpdateActivity.cpp
index 7bf1a444..db39830e 100644
--- a/src/activities/settings/OtaUpdateActivity.cpp
+++ b/src/activities/settings/OtaUpdateActivity.cpp
@@ -116,8 +116,8 @@ void OtaUpdateActivity::render(RenderLock&&) {
static_cast(updaterProgress * 100), 100);
y += metrics.progressBarHeight + metrics.verticalSpacing;
- renderer.drawCenteredText(UI_10_FONT_ID, y,
- (std::to_string(static_cast(updaterProgress * 100)) + "%").c_str());
+ // Percent label is drawn by BaseTheme::drawProgressBar; this slot is left intentionally empty
+ // so the bytes line below stays at the same Y it was at when the activity drew its own percent.
y += height + metrics.verticalSpacing;
renderer.drawCenteredText(
UI_10_FONT_ID, y,
diff --git a/src/activities/settings/SdFirmwareUpdateActivity.cpp b/src/activities/settings/SdFirmwareUpdateActivity.cpp
index 7e73a7f9..b0ca1f63 100644
--- a/src/activities/settings/SdFirmwareUpdateActivity.cpp
+++ b/src/activities/settings/SdFirmwareUpdateActivity.cpp
@@ -230,7 +230,8 @@ void SdFirmwareUpdateActivity::render(RenderLock&&) {
Rect{metrics.contentSidePadding, y, pageWidth - metrics.contentSidePadding * 2, metrics.progressBarHeight},
static_cast(pct), 100);
y += metrics.progressBarHeight + metrics.verticalSpacing;
- renderer.drawCenteredText(UI_10_FONT_ID, y, (std::to_string(pct) + "%").c_str());
+ // Percent label is drawn by BaseTheme::drawProgressBar; this slot is left intentionally empty
+ // so the do-not-power-off line below stays at the same Y as before.
y += lineHeight + metrics.verticalSpacing;
renderer.drawCenteredText(UI_10_FONT_ID, y, tr(STR_FIRMWARE_UPDATE_DO_NOT_POWER_OFF));
} else if (state == State::SUCCESS) {
diff --git a/src/activities/settings/SettingsActivity.cpp b/src/activities/settings/SettingsActivity.cpp
index 9239ff91..adf99acd 100644
--- a/src/activities/settings/SettingsActivity.cpp
+++ b/src/activities/settings/SettingsActivity.cpp
@@ -57,9 +57,9 @@ void SettingsActivity::rebuildSettingsLists() {
systemSettings.push_back(SettingInfo::Action(StrId::STR_CHECK_UPDATES, SettingAction::CheckForUpdates));
systemSettings.push_back(SettingInfo::Action(StrId::STR_SD_FIRMWARE_UPDATE, SettingAction::SdFirmwareUpdate));
systemSettings.push_back(SettingInfo::Action(StrId::STR_LANGUAGE, SettingAction::Language));
- // Insert "Download Fonts" right after the font family setting so users discover it naturally
+ // Insert "Manage Fonts" right after the font family setting so users discover it naturally
readerSettings.insert(readerSettings.begin() + 1,
- SettingInfo::Action(StrId::STR_DOWNLOAD_FONTS, SettingAction::DownloadFonts));
+ SettingInfo::Action(StrId::STR_MANAGE_FONTS, SettingAction::DownloadFonts));
readerSettings.push_back(SettingInfo::Action(StrId::STR_CUSTOMISE_STATUS_BAR, SettingAction::CustomiseStatusBar));
// Update currentSettings pointer and count for the active category
diff --git a/src/activities/util/BmpViewerActivity.cpp b/src/activities/util/BmpViewerActivity.cpp
index 4a29086b..0387bc55 100644
--- a/src/activities/util/BmpViewerActivity.cpp
+++ b/src/activities/util/BmpViewerActivity.cpp
@@ -97,7 +97,12 @@ void BmpViewerActivity::onEnter() {
}
// 4. Prepare Rendering
- const auto labels = mappedInput.mapLabels(tr(STR_BACK), tr(STR_SET_SLEEP_COVER), "", "");
+ bool hasPrevious = (siblingImages.size() > 1 && currentImageIndex > 0);
+ bool hasNext = (siblingImages.size() > 1 && currentImageIndex != -1 &&
+ currentImageIndex < static_cast(siblingImages.size()) - 1);
+
+ const auto labels =
+ mappedInput.mapLabels(tr(STR_BACK), tr(STR_SET_SLEEP_COVER), (hasPrevious ? "<" : ""), (hasNext ? ">" : ""));
GUI.fillPopupProgress(renderer, popupRect, 50);
@@ -185,7 +190,8 @@ void BmpViewerActivity::loop() {
return;
}
- if (mappedInput.wasReleased(MappedInputManager::Button::Up)) {
+ if (mappedInput.wasReleased(MappedInputManager::Button::Left) ||
+ mappedInput.wasReleased(MappedInputManager::Button::Up)) {
if (siblingImages.size() > 1 && currentImageIndex > 0) {
currentImageIndex--;
std::string dirPath = FsHelpers::extractFolderPath(filePath);
@@ -196,7 +202,8 @@ void BmpViewerActivity::loop() {
return;
}
- if (mappedInput.wasReleased(MappedInputManager::Button::Down)) {
+ if (mappedInput.wasReleased(MappedInputManager::Button::Right) ||
+ mappedInput.wasReleased(MappedInputManager::Button::Down)) {
if (siblingImages.size() > 1 && currentImageIndex != -1 &&
currentImageIndex < static_cast(siblingImages.size()) - 1) {
currentImageIndex++;
diff --git a/src/components/themes/roundedraff/RoundedRaffTheme.cpp b/src/components/themes/roundedraff/RoundedRaffTheme.cpp
index 3e0ede09..3d5ea90c 100644
--- a/src/components/themes/roundedraff/RoundedRaffTheme.cpp
+++ b/src/components/themes/roundedraff/RoundedRaffTheme.cpp
@@ -5,7 +5,6 @@
#include
#include
-#include
#include
#include
@@ -44,18 +43,6 @@ void drawScrollBar(const GfxRenderer& renderer, Rect rect, int itemCount, int pa
renderer.fillRect(barX, thumbY, barW, thumbH);
}
-std::string sanitizeButtonLabel(std::string label) {
- // Remove common directional prefixes/symbols (e.g. "<< Home", unsupported icon glyphs).
- while (!label.empty() && !std::isalnum(static_cast(label[0]))) {
- label.erase(0, 1);
- }
- // Trim any extra left spaces.
- while (!label.empty() && label[0] == ' ') {
- label.erase(0, 1);
- }
- return label;
-}
-
} // namespace
int coverWidth = 0;
@@ -412,11 +399,11 @@ void RoundedRaffTheme::drawButtonHints(GfxRenderer& renderer, const char* btn1,
const bool backDisabled = (btn1 == nullptr || btn1[0] == '\0');
const int leftGroupX = sidePadding;
const int rightGroupX = leftGroupX + groupWidth + groupGap;
- const std::string backLabel = backDisabled ? "" : sanitizeButtonLabel(std::string(btn1));
+ const std::string backLabel = backDisabled ? "" : std::string(btn1);
// Callers should provide the button labels. If a label is not specified, it should render empty.
- const std::string selectText = (btn2 && btn2[0] != '\0') ? sanitizeButtonLabel(std::string(btn2)) : "";
- const std::string upText = (btn3 && btn3[0] != '\0') ? sanitizeButtonLabel(std::string(btn3)) : "";
- const std::string downText = (btn4 && btn4[0] != '\0') ? sanitizeButtonLabel(std::string(btn4)) : "";
+ const std::string selectText = (btn2 && btn2[0] != '\0') ? std::string(btn2) : "";
+ const std::string upText = (btn3 && btn3[0] != '\0') ? std::string(btn3) : "";
+ const std::string downText = (btn4 && btn4[0] != '\0') ? std::string(btn4) : "";
// Ensure button hints always "win" visually even if other elements accidentally render into this area.
renderer.fillRect(leftGroupX, hintY, groupWidth, hintHeight, false);
diff --git a/test/hyphenation_eval/resources/generate_hyphenation_test_data.py b/test/hyphenation_eval/resources/generate_hyphenation_test_data.py
index ec38ee67..288336e1 100644
--- a/test/hyphenation_eval/resources/generate_hyphenation_test_data.py
+++ b/test/hyphenation_eval/resources/generate_hyphenation_test_data.py
@@ -16,7 +16,6 @@ Requirements:
import argparse
import re
from collections import Counter
-import pyphen
from pathlib import Path
import zipfile
@@ -75,6 +74,8 @@ def generate_hyphenation_data(
min_prefix: Minimum characters allowed before the first hyphen (default: 2)
min_suffix: Minimum characters allowed after the last hyphen (default: 2)
"""
+ import pyphen
+
print(f"Reading from: {input_file}")
# Read the input file