Merge remote-tracking branch 'upstream/master' into release/1.3.0

This commit is contained in:
Zach Nelson
2026-05-12 12:37:43 -05:00
68 changed files with 857 additions and 237 deletions
+3 -3
View File
@@ -40,7 +40,7 @@ jobs:
echo "metadata=$(python3 -c 'from cpfont_version import FONTS_MANIFEST_VERSION; print(FONTS_MANIFEST_VERSION)')" >> "$GITHUB_OUTPUT" echo "metadata=$(python3 -c 'from cpfont_version import FONTS_MANIFEST_VERSION; print(FONTS_MANIFEST_VERSION)')" >> "$GITHUB_OUTPUT"
- name: Build SD card fonts - 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 - name: Flatten output for release assets
run: | run: |
@@ -83,7 +83,7 @@ jobs:
--title "$TITLE" \ --title "$TITLE" \
--notes "Pre-built \`.cpfont\` font files for CrossPoint Reader. --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." 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. 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."
+1 -1
View File
@@ -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). 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 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. For more details about the scope of the project, see the [SCOPE.md](SCOPE.md) document.
+33
View File
@@ -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.
<img src="./images/focus-reading/focus-reading.jpg" height="500" alt="Comparison of the same page with and without Focus Reading enabled" />
*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
<img src="./images/focus-reading/focus-reading-notoserif.jpg" height="500" alt="Focus Reading with Noto Serif font" />
*Focus Reading with Noto Serif font*
<img src="./images/focus-reading/focus-reading-merriweather.jpg" height="500" alt="Focus Reading with Merriweather font" />
*Focus Reading with Merriweather font*
<img src="./images/focus-reading/focus-reading-atkinson.jpg" height="500" alt="Focus Reading with Atkinson Hyperlegible Next 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.
+1 -1
View File
@@ -49,5 +49,5 @@ A convenient script `update_hyphenation.sh` is used to update all languages.
To use it, run: To use it, run:
```sh ```sh
./scripts/update_hypenation.sh ./scripts/update_hyphenation.sh
``` ```
Binary file not shown.

After

Width:  |  Height:  |  Size: 216 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 211 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 207 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 260 KiB

+25 -15
View File
@@ -10,7 +10,7 @@ There are three ways to install fonts:
### Option 1: Download from device (recommended) ### Option 1: Download from device (recommended)
1. Connect your CrossPoint reader to WiFi 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 3. Browse available font families and tap to download
4. Downloaded fonts appear immediately in **Settings > Reader > Font Family** 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 ### Option 3: Manual SD card copy
1. Download font files from the 1. Download font files from the
[Releases page](https://github.com/crosspoint-reader/crosspoint-reader/releases/tag/sd-fonts) [crosspoint-fonts repository](https://github.com/crosspoint-reader/crosspoint-fonts)
2. Copy font family folders to `/.crosspoint/fonts/` on your SD card: 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/ SD Card Root/
── .crosspoint/ ── .fonts/ ← Hidden root (preferred)
└── fonts/ └── Literata/
├── Bookerly-SD/ ├── Literata_12.cpfont
├── Bookerly-SD_12.cpfont ├── Literata_14.cpfont
├── Bookerly-SD_14.cpfont ├── Literata_16.cpfont
│ ├── Bookerly-SD_16.cpfont └── Literata_18.cpfont
│ └── Bookerly-SD_18.cpfont └── fonts/ ← Visible root (equally valid)
└── Merriweather/
├── Merriweather_12.cpfont
└── ... └── ...
3. Insert the SD card and power on your CrossPoint reader 3. Insert the SD card and power on your CrossPoint reader
## Available Pre-Built Fonts ## Available Pre-Built Fonts
| Font | Best For | Languages | The current list of pre-built fonts is maintained in the
|------|----------|-----------| [crosspoint-fonts repository](https://github.com/crosspoint-reader/crosspoint-fonts).
| Bookerly-SD | General reading | English, Western European |
| NotoSansExtended | Multi-script reading | European, Greek, Cyrillic, Georgian, Armenian, Ethiopic |
| NotoSansCJK | Chinese/Japanese/Korean | CJK + ASCII |
## Converting Custom Fonts ## Converting Custom Fonts
+12 -8
View File
@@ -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 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 // Unload any previously loaded family first
if (!loadedFamilyName_.empty()) { if (!loadedFamilyName_.empty()) {
unloadAll(renderer); unloadAll(renderer);
} }
// Pick the single file whose size is closest to targetPtSize. Loading // Select by ordinal position: sort available sizes, then map the font size
// only one size bounds resident memory (intervals + kern/ligature tables // enum (SMALL=0 .. EXTRA_LARGE=3) to the corresponding slot. When the
// per style) to one file's worth, vs. N_sizes × per-file overhead. // family has fewer sizes than 4, clamp to the last available size.
const SdCardFontFileInfo* selected = family.pickClosestSize(targetPtSize); auto sizes = family.availableSizes();
if (!selected) { if (sizes.empty()) {
LOG_ERR("SDMGR", "Family %s has no files to load", family.name.c_str()); LOG_ERR("SDMGR", "Family %s has no files to load", family.name.c_str());
return false; 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(); auto* font = new (std::nothrow) SdCardFont();
if (!font) { if (!font) {
LOG_ERR("SDMGR", "Failed to allocate SdCardFont for %s", selected->path.c_str()); 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); renderer.registerSdCardFont(fontId, font);
loaded_.push_back({font, fontId, selected->pointSize}); 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, LOG_DBG("SDMGR", "Loaded %s size=%u id=%d styles=%u (sizeEnum=%u)", selected->path.c_str(), selected->pointSize,
font->styleCount(), targetPtSize); fontId, font->styleCount(), fontSizeEnum);
EpdFontFamily fontFamily(font->getEpdFont(0), font->getEpdFont(1), font->getEpdFont(2), font->getEpdFont(3)); EpdFontFamily fontFamily(font->getEpdFont(0), font->getEpdFont(1), font->getEpdFont(2), font->getEpdFont(3));
renderer.insertFont(fontId, fontFamily); renderer.insertFont(fontId, fontFamily);
+5 -9
View File
@@ -15,16 +15,12 @@ class SdCardFontManager {
SdCardFontManager(const SdCardFontManager&) = delete; SdCardFontManager(const SdCardFontManager&) = delete;
SdCardFontManager& operator=(const SdCardFontManager&) = delete; SdCardFontManager& operator=(const SdCardFontManager&) = delete;
// Load the single size whose pointSize is closest to targetPtSize. Only one // Load the font file matching fontSizeEnum (SMALL=0 .. EXTRA_LARGE=3) by
// .cpfont file is loaded; other sizes remain on disk. This keeps resident // ordinal position in the family's sorted size list. Only one .cpfont file
// interval + kern/ligature tables to one size's worth of memory. // is loaded; other sizes remain on disk. This keeps resident interval +
// // kern/ligature tables to one size's worth of memory.
// Closest-pt selection is robust against families that don't ship the
// canonical {12,14,16,18}: a family with only [10,14,18] still resolves
// any reasonable target, where ordinal slot-mapping by SMALL..EXTRA_LARGE
// would mis-select.
// Returns true on success. // 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. // Unload everything, unregister from renderer.
void unloadAll(GfxRenderer& renderer); void unloadAll(GfxRenderer& renderer);
-17
View File
@@ -4,8 +4,6 @@
#include <Logging.h> #include <Logging.h>
#include <algorithm> #include <algorithm>
#include <climits>
#include <cstdlib>
#include <cstring> #include <cstring>
// --- SdCardFontFamilyInfo helpers --- // --- SdCardFontFamilyInfo helpers ---
@@ -40,21 +38,6 @@ std::vector<uint8_t> SdCardFontFamilyInfo::availableSizes() const {
return sizes; return sizes;
} }
const SdCardFontFileInfo* SdCardFontFamilyInfo::pickClosestSize(uint8_t targetPtSize) const {
const SdCardFontFileInfo* selected = nullptr;
int bestDiff = INT_MAX;
for (const auto& f : files) {
int diff = std::abs(static_cast<int>(f.pointSize) - static_cast<int>(targetPtSize));
// Strict < ensures the first scan wins on ties; then tie-break by smaller
// pointSize to make the choice independent of filesystem enumeration order.
if (diff < bestDiff || (diff == bestDiff && selected && f.pointSize < selected->pointSize)) {
bestDiff = diff;
selected = &f;
}
}
return selected;
}
// --- SdCardFontRegistry --- // --- SdCardFontRegistry ---
bool SdCardFontRegistry::parseFilename(const char* filename, uint8_t& size, uint8_t& style) { bool SdCardFontRegistry::parseFilename(const char* filename, uint8_t& size, uint8_t& style) {
-9
View File
@@ -20,15 +20,6 @@ struct SdCardFontFamilyInfo {
const SdCardFontFileInfo* findFile(uint8_t size, uint8_t style = 0) const; const SdCardFontFileInfo* findFile(uint8_t size, uint8_t style = 0) const;
bool hasSize(uint8_t size) const; bool hasSize(uint8_t size) const;
std::vector<uint8_t> availableSizes() const; std::vector<uint8_t> availableSizes() const;
// Pick the file whose pointSize is closest to targetPtSize. On ties (equal
// distance) prefers the smaller pointSize so behaviour is deterministic
// across SD card layouts. Returns nullptr when files is empty.
//
// Robust against families that don't ship the canonical {12,14,16,18} set:
// a family with only [10,14,18] resolves target 12 → 10 (or 14 on tie),
// target 16 → 14 or 18, etc., instead of mis-indexing by ordinal slot.
const SdCardFontFileInfo* pickClosestSize(uint8_t targetPtSize) const;
}; };
class SdCardFontRegistry { class SdCardFontRegistry {
+75 -14
View File
@@ -17,6 +17,12 @@ Usage:
# Generate only specific families # Generate only specific families
python3 build-sd-fonts.py --only Literata,IBMPlexMono 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 import argparse
@@ -25,6 +31,8 @@ import shutil
import subprocess import subprocess
import sys import sys
import tempfile import tempfile
import threading
import time
import urllib.request import urllib.request
from concurrent.futures import ProcessPoolExecutor, as_completed from concurrent.futures import ProcessPoolExecutor, as_completed
from pathlib import Path from pathlib import Path
@@ -141,7 +149,16 @@ def resolve_font_path(style_spec: dict, family_name: str, style_name: str) -> Pa
return resolved 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).""" """Build a single font family. Returns (name, success, message)."""
name = family["name"] name = family["name"]
output_dir = output_base / 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") cmd.append("--force-autohint")
# Run fontconvert_sdcard.py # Run fontconvert_sdcard.py
start = time.monotonic()
try: try:
result = subprocess.run( if verbose:
cmd, proc = subprocess.Popen(
capture_output=True, cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True
text=True, )
timeout=600, stdout_lines: list[str] = []
) stderr_lines: list[str] = []
if result.returncode != 0: t_out = threading.Thread(
return name, False, result.stderr.strip() or f"Exit code {result.returncode}" target=_stream_pipe, args=(proc.stdout, name, stdout_lines)
return name, True, "" )
except subprocess.TimeoutExpired: t_err = threading.Thread(
return name, False, "Timed out after 600s" 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: except Exception as e:
return name, False, str(e) return name, False, str(e)
@@ -252,6 +303,14 @@ def main():
help="Max parallel jobs (default: number of families)" help="Max parallel jobs (default: number of families)"
) )
parser.add_argument("--clean", action="store_true", help="Clean output directory before building") 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() args = parser.parse_args()
if args.manifest and not args.base_url: if args.manifest and not args.base_url:
@@ -303,12 +362,14 @@ def main():
# Build phase (parallel) # Build phase (parallel)
max_workers = args.jobs or len(families) 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 = [] failed = []
with ProcessPoolExecutor(max_workers=max_workers) as executor: with ProcessPoolExecutor(max_workers=max_workers) as executor:
futures = { 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 family in families
} }
for future in as_completed(futures): for future in as_completed(futures):
+3 -2
View File
@@ -1,12 +1,10 @@
#!python3 #!python3
import freetype
import zlib import zlib
import sys import sys
import re import re
import math import math
import argparse import argparse
from collections import namedtuple from collections import namedtuple
from fontTools.ttLib import TTFont
# Force UTF-8 stdout so that `python fontconvert.py … > foo.h` on Windows # 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 # (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.") 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() 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"]) 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] font_stack = [freetype.Face(f) for f in args.fontstack]
+19 -7
View File
@@ -22,7 +22,8 @@ Usage:
""" """
import freetype from __future__ import annotations
import struct import struct
import sys import sys
import os import os
@@ -31,8 +32,6 @@ import math
import argparse import argparse
from collections import namedtuple from collections import namedtuple
from fontTools.ttLib import TTFont
from cpfont_version import CPFONT_VERSION from cpfont_version import CPFONT_VERSION
# --- Unicode interval presets --- # --- 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 codepoints. Values are scaled from font design units to integer
pixels at ppem. pixels at ppem.
""" """
from fontTools.ttLib import TTFont
font = TTFont(font_path) font = TTFont(font_path)
units_per_em = font['head'].unitsPerEm units_per_em = font['head'].unitsPerEm
cmap = font.getBestCmap() or {} 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. Returns list of (packed_pair, ligature_codepoint) for the given codepoints.
Multi-character ligatures are decomposed into chained pairs. Multi-character ligatures are decomposed into chained pairs.
""" """
from fontTools.ttLib import TTFont
font = TTFont(font_path) font = TTFont(font_path)
cmap = font.getBestCmap() or {} 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): def rasterize_font_style(fontfile, size, intervals, style_id=0, force_autohint=False):
"""Rasterize all glyphs for one font style. Returns StyleRasterData.""" """Rasterize all glyphs for one font style. Returns StyleRasterData."""
import freetype
style_names = {0: "regular", 1: "bold", 2: "italic", 3: "bolditalic"} style_names = {0: "regular", 1: "bold", 2: "italic", 3: "bolditalic"}
style_label = style_names.get(style_id, str(style_id)) 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 face
return None 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) print(f" [{style_label}] Validating intervals against font...", file=sys.stderr)
validated_intervals = [] validated_intervals = []
for i_start, i_end in intervals: for i_start, i_end in intervals:
start = i_start start = i_start
for code_point in range(i_start, i_end + 1): for code_point in range(i_start, i_end + 1):
f = load_glyph(code_point) if face.get_char_index(code_point) == 0:
if f is None:
if start < code_point: if start < code_point:
validated_intervals.append((start, code_point - 1)) validated_intervals.append((start, code_point - 1))
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 # pitch == width and a top-down layout — that holds in the common
# case but breaks on padded or flipped bitmaps and corrupts the # case but breaks on padded or flipped bitmaps and corrupts the
# output. Walk by (row, col) using the real pitch instead. # 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 = [] pixels4g = []
px = 0 px = 0
buf = bitmap.buffer
abs_pitch = abs(bitmap.pitch) abs_pitch = abs(bitmap.pitch)
for y in range(bitmap.rows): for y in range(bitmap.rows):
row_offset = y * abs_pitch if bitmap.pitch >= 0 else (bitmap.rows - 1 - y) * abs_pitch row_offset = y * abs_pitch if bitmap.pitch >= 0 else (bitmap.rows - 1 - y) * abs_pitch
for x in range(bitmap.width): for x in range(bitmap.width):
v = bitmap.buffer[row_offset + x] v = buf[row_offset + x]
if x % 2 == 0: if x % 2 == 0:
px = (v >> 4) px = (v >> 4)
else: else:
@@ -234,6 +234,9 @@ def main():
if len(sys.argv) < 2: if len(sys.argv) < 2:
print(f"Usage: {sys.argv[0]} <font_headers_directory>", file=sys.stderr) print(f"Usage: {sys.argv[0]} <font_headers_directory>", file=sys.stderr)
sys.exit(1) sys.exit(1)
if sys.argv[1] in ("-h", "--help"):
print(f"Usage: {sys.argv[0]} <font_headers_directory>")
sys.exit(0)
font_dir = sys.argv[1] font_dir = sys.argv[1]
if not os.path.isdir(font_dir): if not os.path.isdir(font_dir):
+227 -8
View File
@@ -74,21 +74,170 @@ uint16_t measureWordWidth(const GfxRenderer& renderer, const int fontId, const s
return renderer.getTextAdvanceX(fontId, sanitized.c_str(), style); 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 <cctype>
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 } // namespace
void ParsedText::addWord(std::string word, const EpdFontFamily::Style fontStyle, const bool underline, void ParsedText::addWord(std::string word, const EpdFontFamily::Style fontStyle, const bool underline,
const bool attachToPrevious) { const bool attachToPrevious) {
if (word.empty()) return; if (word.empty()) return;
words.push_back(std::move(word)); EpdFontFamily::Style baseStyle = fontStyle;
EpdFontFamily::Style combinedStyle = fontStyle;
if (underline) { if (underline) {
combinedStyle = static_cast<EpdFontFamily::Style>(combinedStyle | EpdFontFamily::UNDERLINE); baseStyle = static_cast<EpdFontFamily::Style>(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<const unsigned char*>(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<size_t>(targetBoldChars, 1, 9);
if (targetBoldChars >= charCount) {
// Whole segment is bold - no suffix split needed
words.emplace_back(segment);
wordStyles.push_back(static_cast<EpdFontFamily::Style>(baseStyle | EpdFontFamily::BOLD));
wordContinues.push_back(attach);
wordIsFocusSuffix.push_back(false);
} else {
countPtr = reinterpret_cast<const unsigned char*>(segment.data());
for (size_t i = 0; i < targetBoldChars; ++i) {
utf8NextCodepoint(&countPtr);
}
size_t splitByteOffset = countPtr - reinterpret_cast<const unsigned char*>(segment.data());
// Bold prefix
words.emplace_back(segment.substr(0, splitByteOffset));
wordStyles.push_back(static_cast<EpdFontFamily::Style>(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<const unsigned char*>(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<const char*>(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<const char*>(segmentStart), segmentLen);
processSegment(segment, inWordSegment, isFirstSegment ? attachToPrevious : true);
}
// Consumes data to minimize memory usage // Consumes data to minimize memory usage
void ParsedText::layoutAndExtractLines(const GfxRenderer& renderer, const int fontId, const uint16_t viewportWidth, void ParsedText::layoutAndExtractLines(const GfxRenderer& renderer, const int fontId, const uint16_t viewportWidth,
const std::function<void(std::shared_ptr<TextBlock>)>& processLine, const std::function<void(std::shared_ptr<TextBlock>)>& processLine,
@@ -153,6 +302,7 @@ void ParsedText::layoutAndExtractLines(const GfxRenderer& renderer, const int fo
words.erase(words.begin(), words.begin() + consumed); words.erase(words.begin(), words.begin() + consumed);
wordStyles.erase(wordStyles.begin(), wordStyles.begin() + consumed); wordStyles.erase(wordStyles.begin(), wordStyles.begin() + consumed);
wordContinues.erase(wordContinues.begin(), wordContinues.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. // Insert the remainder word (with matching style and continuation flag) directly after the prefix.
words.insert(words.begin() + wordIndex + 1, remainder); words.insert(words.begin() + wordIndex + 1, remainder);
wordStyles.insert(wordStyles.begin() + wordIndex + 1, style); 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. // 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]), renderer.getSpaceAdvance(fontId, lastCodepoint(words[lastBreakAt + wordIdx - 1]),
firstCodepoint(words[lastBreakAt + wordIdx]), wordStyles[lastBreakAt + wordIdx - 1]); firstCodepoint(words[lastBreakAt + wordIdx]), wordStyles[lastBreakAt + wordIdx - 1]);
} else if (wordIdx > 0 && continuesVec[lastBreakAt + wordIdx]) { } 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) // Cross-boundary kerning for continuation words (e.g. nonbreaking spaces, attached punctuation)
totalNaturalGaps += totalNaturalGaps +=
renderer.getKerning(fontId, lastCodepoint(words[lastBreakAt + wordIdx - 1]), renderer.getKerning(fontId, lastCodepoint(words[lastBreakAt + wordIdx - 1]),
@@ -541,6 +698,11 @@ void ParsedText::extractLine(const size_t breakIndex, const int pageWidth, const
advance += advance +=
renderer.getKerning(fontId, lastCodepoint(words[lastBreakAt + wordIdx]), renderer.getKerning(fontId, lastCodepoint(words[lastBreakAt + wordIdx]),
firstCodepoint(words[lastBreakAt + wordIdx + 1]), wordStyles[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; xpos += advance;
} else { } else {
int gap = 0; int gap = 0;
@@ -567,6 +729,63 @@ void ParsedText::extractLine(const size_t breakIndex, const int pageWidth, const
} }
} }
processLine( // Fast path: when no word on this line was split for focus reading, skip the merge work
std::make_shared<TextBlock>(std::move(lineWords), std::move(lineXPos), std::move(lineWordStyles), blockStyle)); // 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<TextBlock>(std::move(lineWords), std::move(lineXPos), std::move(lineWordStyles),
std::vector<uint8_t>{}, std::vector<uint16_t>{}, 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<std::string> outWords;
std::vector<int16_t> outXPos;
std::vector<EpdFontFamily::Style> outStyles;
std::vector<uint8_t> outBoundaries;
std::vector<uint16_t> 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<uint8_t>(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<uint16_t>(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<EpdFontFamily::Style>(lineWordStyles[i] & ~EpdFontFamily::BOLD)
: lineWordStyles[i];
outStyles.push_back(storedStyle);
outBoundaries.push_back(boundary);
outSuffixX.push_back(suffixX);
}
}
processLine(std::make_shared<TextBlock>(std::move(outWords), std::move(outXPos), std::move(outStyles),
std::move(outBoundaries), std::move(outSuffixX), blockStyle));
} }
+8 -3
View File
@@ -15,10 +15,12 @@ class GfxRenderer;
class ParsedText { class ParsedText {
std::vector<std::string> words; std::vector<std::string> words;
std::vector<EpdFontFamily::Style> wordStyles; std::vector<EpdFontFamily::Style> wordStyles;
std::vector<bool> wordContinues; // true = word attaches to previous (no space before it) std::vector<bool> wordContinues; // true = word attaches to previous (no space before it)
std::vector<bool> wordIsFocusSuffix; // true = token is the regular tail of a focus bold-prefix split
BlockStyle blockStyle; BlockStyle blockStyle;
bool extraParagraphSpacing; bool extraParagraphSpacing;
bool hyphenationEnabled; bool hyphenationEnabled;
bool focusReadingEnabled;
void applyParagraphIndent(); void applyParagraphIndent();
std::vector<size_t> computeLineBreaks(const GfxRenderer& renderer, int fontId, int pageWidth, std::vector<size_t> computeLineBreaks(const GfxRenderer& renderer, int fontId, int pageWidth,
@@ -35,8 +37,11 @@ class ParsedText {
public: public:
explicit ParsedText(const bool extraParagraphSpacing, const bool hyphenationEnabled = false, explicit ParsedText(const bool extraParagraphSpacing, const bool hyphenationEnabled = false,
const BlockStyle& blockStyle = BlockStyle()) const bool focusReadingEnabled = false, const BlockStyle& blockStyle = BlockStyle())
: blockStyle(blockStyle), extraParagraphSpacing(extraParagraphSpacing), hyphenationEnabled(hyphenationEnabled) {} : blockStyle(blockStyle),
extraParagraphSpacing(extraParagraphSpacing),
hyphenationEnabled(hyphenationEnabled),
focusReadingEnabled(focusReadingEnabled) {}
~ParsedText() = default; ~ParsedText() = default;
void addWord(std::string word, EpdFontFamily::Style fontStyle, bool underline = false, bool attachToPrevious = false); void addWord(std::string word, EpdFontFamily::Style fontStyle, bool underline = false, bool attachToPrevious = false);
+15 -11
View File
@@ -13,8 +13,8 @@ namespace {
constexpr uint8_t SECTION_FILE_VERSION = 23; constexpr uint8_t SECTION_FILE_VERSION = 23;
constexpr uint32_t HEADER_SIZE = sizeof(uint8_t) + sizeof(int) + sizeof(float) + sizeof(bool) + sizeof(uint8_t) + 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(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(uint8_t) + sizeof(bool) + sizeof(uint32_t) + sizeof(uint32_t) +
sizeof(uint32_t); sizeof(uint32_t) + sizeof(uint32_t);
struct PageLutEntry { struct PageLutEntry {
uint32_t fileOffset; uint32_t fileOffset;
@@ -43,7 +43,8 @@ uint32_t Section::onPageComplete(std::unique_ptr<Page> page) {
void Section::writeSectionFileHeader(const int fontId, const float lineCompression, const bool extraParagraphSpacing, void Section::writeSectionFileHeader(const int fontId, const float lineCompression, const bool extraParagraphSpacing,
const uint8_t paragraphAlignment, const uint16_t viewportWidth, const uint8_t paragraphAlignment, const uint16_t viewportWidth,
const uint16_t viewportHeight, const bool hyphenationEnabled, 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) { if (!file) {
LOG_DBG("SCT", "File not open for writing header"); LOG_DBG("SCT", "File not open for writing header");
return; 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) + static_assert(HEADER_SIZE == sizeof(SECTION_FILE_VERSION) + sizeof(fontId) + sizeof(lineCompression) +
sizeof(extraParagraphSpacing) + sizeof(paragraphAlignment) + sizeof(viewportWidth) + sizeof(extraParagraphSpacing) + sizeof(paragraphAlignment) + sizeof(viewportWidth) +
sizeof(viewportHeight) + sizeof(pageCount) + sizeof(hyphenationEnabled) + sizeof(viewportHeight) + sizeof(pageCount) + sizeof(hyphenationEnabled) +
sizeof(embeddedStyle) + sizeof(imageRendering) + sizeof(uint32_t) + sizeof(embeddedStyle) + sizeof(imageRendering) + sizeof(focusReadingEnabled) +
sizeof(uint32_t) + sizeof(uint32_t) + sizeof(uint32_t), sizeof(uint32_t) + sizeof(uint32_t) + sizeof(uint32_t) + sizeof(uint32_t),
"Header size mismatch"); "Header size mismatch");
serialization::writePod(file, SECTION_FILE_VERSION); serialization::writePod(file, SECTION_FILE_VERSION);
serialization::writePod(file, fontId); serialization::writePod(file, fontId);
@@ -64,6 +65,7 @@ void Section::writeSectionFileHeader(const int fontId, const float lineCompressi
serialization::writePod(file, hyphenationEnabled); serialization::writePod(file, hyphenationEnabled);
serialization::writePod(file, embeddedStyle); serialization::writePod(file, embeddedStyle);
serialization::writePod(file, imageRendering); 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, pageCount); // Placeholder for page count (will be initially 0, patched later)
serialization::writePod(file, static_cast<uint32_t>(0)); // Placeholder for LUT offset (patched later) serialization::writePod(file, static_cast<uint32_t>(0)); // Placeholder for LUT offset (patched later)
serialization::writePod(file, static_cast<uint32_t>(0)); // Placeholder for anchor map offset (patched later) serialization::writePod(file, static_cast<uint32_t>(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, bool Section::loadSectionFile(const int fontId, const float lineCompression, const bool extraParagraphSpacing,
const uint8_t paragraphAlignment, const uint16_t viewportWidth, const uint8_t paragraphAlignment, const uint16_t viewportWidth,
const uint16_t viewportHeight, const bool hyphenationEnabled, const bool embeddedStyle, 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)) { if (!Storage.openFileForRead("SCT", filePath, file)) {
return false; return false;
} }
@@ -99,6 +101,7 @@ bool Section::loadSectionFile(const int fontId, const float lineCompression, con
bool fileHyphenationEnabled; bool fileHyphenationEnabled;
bool fileEmbeddedStyle; bool fileEmbeddedStyle;
uint8_t fileImageRendering; uint8_t fileImageRendering;
bool fileFocusReadingEnabled;
serialization::readPod(file, fileFontId); serialization::readPod(file, fileFontId);
serialization::readPod(file, fileLineCompression); serialization::readPod(file, fileLineCompression);
serialization::readPod(file, fileExtraParagraphSpacing); 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, fileHyphenationEnabled);
serialization::readPod(file, fileEmbeddedStyle); serialization::readPod(file, fileEmbeddedStyle);
serialization::readPod(file, fileImageRendering); serialization::readPod(file, fileImageRendering);
serialization::readPod(file, fileFocusReadingEnabled);
if (fontId != fileFontId || lineCompression != fileLineCompression || if (fontId != fileFontId || lineCompression != fileLineCompression ||
extraParagraphSpacing != fileExtraParagraphSpacing || paragraphAlignment != fileParagraphAlignment || extraParagraphSpacing != fileExtraParagraphSpacing || paragraphAlignment != fileParagraphAlignment ||
viewportWidth != fileViewportWidth || viewportHeight != fileViewportHeight || viewportWidth != fileViewportWidth || viewportHeight != fileViewportHeight ||
hyphenationEnabled != fileHyphenationEnabled || embeddedStyle != fileEmbeddedStyle || hyphenationEnabled != fileHyphenationEnabled || embeddedStyle != fileEmbeddedStyle ||
imageRendering != fileImageRendering) { imageRendering != fileImageRendering || focusReadingEnabled != fileFocusReadingEnabled) {
// Explicit close() required: member variable persists beyond function scope
file.close(); file.close();
LOG_ERR("SCT", "Deserialization failed: Parameters do not match"); LOG_ERR("SCT", "Deserialization failed: Parameters do not match");
clearCache(); clearCache();
@@ -148,7 +151,8 @@ bool Section::clearCache() const {
bool Section::createSectionFile(const int fontId, const float lineCompression, const bool extraParagraphSpacing, bool Section::createSectionFile(const int fontId, const float lineCompression, const bool extraParagraphSpacing,
const uint8_t paragraphAlignment, const uint16_t viewportWidth, const uint8_t paragraphAlignment, const uint16_t viewportWidth,
const uint16_t viewportHeight, const bool hyphenationEnabled, const bool embeddedStyle, const uint16_t viewportHeight, const bool hyphenationEnabled, const bool embeddedStyle,
const uint8_t imageRendering, const std::function<void()>& popupFn) { const uint8_t imageRendering, const bool focusReadingEnabled,
const std::function<void()>& popupFn) {
const auto localPath = epub->getSpineItem(spineIndex).href; const auto localPath = epub->getSpineItem(spineIndex).href;
const auto tmpHtmlPath = epub->getCachePath() + "/.tmp_" + std::to_string(spineIndex) + ".html"; 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; return false;
} }
writeSectionFileHeader(fontId, lineCompression, extraParagraphSpacing, paragraphAlignment, viewportWidth, writeSectionFileHeader(fontId, lineCompression, extraParagraphSpacing, paragraphAlignment, viewportWidth,
viewportHeight, hyphenationEnabled, embeddedStyle, imageRendering); viewportHeight, hyphenationEnabled, embeddedStyle, imageRendering, focusReadingEnabled);
std::vector<PageLutEntry> lut = {}; std::vector<PageLutEntry> lut = {};
// Derive the content base directory and image cache path prefix for the parser // 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( ChapterHtmlSlimParser visitor(
epub, tmpHtmlPath, renderer, fontId, lineCompression, extraParagraphSpacing, paragraphAlignment, viewportWidth, epub, tmpHtmlPath, renderer, fontId, lineCompression, extraParagraphSpacing, paragraphAlignment, viewportWidth,
viewportHeight, hyphenationEnabled, viewportHeight, hyphenationEnabled, focusReadingEnabled,
[this, &lut](std::unique_ptr<Page> page, const uint16_t paragraphIndex, const uint16_t listItemIndex) { [this, &lut](std::unique_ptr<Page> page, const uint16_t paragraphIndex, const uint16_t listItemIndex) {
lut.push_back({this->onPageComplete(std::move(page)), paragraphIndex, listItemIndex}); lut.push_back({this->onPageComplete(std::move(page)), paragraphIndex, listItemIndex});
}, },
+4 -3
View File
@@ -18,7 +18,7 @@ class Section {
void writeSectionFileHeader(int fontId, float lineCompression, bool extraParagraphSpacing, uint8_t paragraphAlignment, void writeSectionFileHeader(int fontId, float lineCompression, bool extraParagraphSpacing, uint8_t paragraphAlignment,
uint16_t viewportWidth, uint16_t viewportHeight, bool hyphenationEnabled, 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> page); uint32_t onPageComplete(std::unique_ptr<Page> page);
public: public:
@@ -33,11 +33,12 @@ class Section {
~Section() = default; ~Section() = default;
bool loadSectionFile(int fontId, float lineCompression, bool extraParagraphSpacing, uint8_t paragraphAlignment, bool loadSectionFile(int fontId, float lineCompression, bool extraParagraphSpacing, uint8_t paragraphAlignment,
uint16_t viewportWidth, uint16_t viewportHeight, bool hyphenationEnabled, bool embeddedStyle, uint16_t viewportWidth, uint16_t viewportHeight, bool hyphenationEnabled, bool embeddedStyle,
uint8_t imageRendering); uint8_t imageRendering, bool focusReadingEnabled);
bool clearCache() const; bool clearCache() const;
bool createSectionFile(int fontId, float lineCompression, bool extraParagraphSpacing, uint8_t paragraphAlignment, bool createSectionFile(int fontId, float lineCompression, bool extraParagraphSpacing, uint8_t paragraphAlignment,
uint16_t viewportWidth, uint16_t viewportHeight, bool hyphenationEnabled, bool embeddedStyle, uint16_t viewportWidth, uint16_t viewportHeight, bool hyphenationEnabled, bool embeddedStyle,
uint8_t imageRendering, const std::function<void()>& popupFn = nullptr); uint8_t imageRendering, bool focusReadingEnabled,
const std::function<void()>& popupFn = nullptr);
std::unique_ptr<Page> loadPageFromSectionFile(); std::unique_ptr<Page> loadPageFromSectionFile();
// Look up the page number for an anchor id from the section cache file. // Look up the page number for an anchor id from the section cache file.
+62 -10
View File
@@ -4,18 +4,44 @@
#include <Logging.h> #include <Logging.h>
#include <Serialization.h> #include <Serialization.h>
#include <cstring>
void TextBlock::render(const GfxRenderer& renderer, const int fontId, const int x, const int y) const { void TextBlock::render(const GfxRenderer& renderer, const int fontId, const int x, const int y) const {
// Validate iterator bounds before rendering // Focus annotations are optional: empty vectors mean no word in this block has a split.
if (words.size() != wordXpos.size() || words.size() != wordStyles.size()) { // When present, they must be sized in lockstep with words[].
LOG_ERR("TXB", "Render skipped: size mismatch (words=%u, xpos=%u, styles=%u)\n", (uint32_t)words.size(), const bool hasFocus = !wordFocusBoundary.empty();
(uint32_t)wordXpos.size(), (uint32_t)wordStyles.size()); 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; return;
} }
for (size_t i = 0; i < words.size(); i++) { for (size_t i = 0; i < words.size(); i++) {
const int wordX = wordXpos[i] + x; const int wordX = wordXpos[i] + x;
const EpdFontFamily::Style currentStyle = wordStyles[i]; 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<EpdFontFamily::Style>(currentStyle | EpdFontFamily::BOLD);
const size_t boldLen = std::min<size_t>({static_cast<size_t>(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) { if ((currentStyle & EpdFontFamily::UNDERLINE) != 0) {
const std::string& w = words[i]; 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 { bool TextBlock::serialize(FsFile& file) const {
if (words.size() != wordXpos.size() || words.size() != wordStyles.size()) { // Focus annotations are optional; vectors are either empty (no splits in this block)
LOG_ERR("TXB", "Serialization failed: size mismatch (words=%u, xpos=%u, styles=%u)\n", words.size(), // or sized in lockstep with words[].
wordXpos.size(), wordStyles.size()); 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<uint32_t>(words.size()), static_cast<uint32_t>(wordXpos.size()),
static_cast<uint32_t>(wordStyles.size()), static_cast<uint32_t>(wordFocusBoundary.size()),
static_cast<uint32_t>(wordFocusSuffixX.size()));
return false; return false;
} }
@@ -53,6 +85,13 @@ bool TextBlock::serialize(FsFile& file) const {
for (const auto& w : words) serialization::writeString(file, w); for (const auto& w : words) serialization::writeString(file, w);
for (auto x : wordXpos) serialization::writePod(file, x); for (auto x : wordXpos) serialization::writePod(file, x);
for (auto s : wordStyles) serialization::writePod(file, s); 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<uint8_t>(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) // Style (alignment + margins/padding/indent)
serialization::writePod(file, blockStyle.alignment); serialization::writePod(file, blockStyle.alignment);
@@ -76,6 +115,8 @@ std::unique_ptr<TextBlock> TextBlock::deserialize(FsFile& file) {
std::vector<std::string> words; std::vector<std::string> words;
std::vector<int16_t> wordXpos; std::vector<int16_t> wordXpos;
std::vector<EpdFontFamily::Style> wordStyles; std::vector<EpdFontFamily::Style> wordStyles;
std::vector<uint8_t> wordFocusBoundary;
std::vector<uint16_t> wordFocusSuffixX;
BlockStyle blockStyle; BlockStyle blockStyle;
// Word count // Word count
@@ -94,6 +135,16 @@ std::unique_ptr<TextBlock> TextBlock::deserialize(FsFile& file) {
for (auto& w : words) serialization::readString(file, w); for (auto& w : words) serialization::readString(file, w);
for (auto& x : wordXpos) serialization::readPod(file, x); for (auto& x : wordXpos) serialization::readPod(file, x);
for (auto& s : wordStyles) serialization::readPod(file, s); 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) // Style (alignment + margins/padding/indent)
serialization::readPod(file, blockStyle.alignment); serialization::readPod(file, blockStyle.alignment);
@@ -109,6 +160,7 @@ std::unique_ptr<TextBlock> TextBlock::deserialize(FsFile& file) {
serialization::readPod(file, blockStyle.textIndent); serialization::readPod(file, blockStyle.textIndent);
serialization::readPod(file, blockStyle.textIndentDefined); serialization::readPod(file, blockStyle.textIndentDefined);
return std::unique_ptr<TextBlock>( return std::unique_ptr<TextBlock>(new TextBlock(std::move(words), std::move(wordXpos), std::move(wordStyles),
new TextBlock(std::move(words), std::move(wordXpos), std::move(wordStyles), blockStyle)); std::move(wordFocusBoundary), std::move(wordFocusSuffixX),
blockStyle));
} }
+15 -1
View File
@@ -15,14 +15,28 @@ class TextBlock final : public Block {
std::vector<std::string> words; std::vector<std::string> words;
std::vector<int16_t> wordXpos; std::vector<int16_t> wordXpos;
std::vector<EpdFontFamily::Style> wordStyles; std::vector<EpdFontFamily::Style> 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<uint8_t> 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<uint16_t> wordFocusSuffixX;
BlockStyle blockStyle; BlockStyle blockStyle;
public: public:
explicit TextBlock(std::vector<std::string> words, std::vector<int16_t> word_xpos, explicit TextBlock(std::vector<std::string> words, std::vector<int16_t> word_xpos,
std::vector<EpdFontFamily::Style> word_styles, const BlockStyle& blockStyle = BlockStyle()) std::vector<EpdFontFamily::Style> word_styles, std::vector<uint8_t> focus_boundary,
std::vector<uint16_t> focus_suffix_x, const BlockStyle& blockStyle = BlockStyle())
: words(std::move(words)), : words(std::move(words)),
wordXpos(std::move(word_xpos)), wordXpos(std::move(word_xpos)),
wordStyles(std::move(word_styles)), wordStyles(std::move(word_styles)),
wordFocusBoundary(std::move(focus_boundary)),
wordFocusSuffixX(std::move(focus_suffix_x)),
blockStyle(blockStyle) {} blockStyle(blockStyle) {}
~TextBlock() override = default; ~TextBlock() override = default;
void setBlockStyle(const BlockStyle& blockStyle) { this->blockStyle = blockStyle; } void setBlockStyle(const BlockStyle& blockStyle) { this->blockStyle = blockStyle; }
@@ -141,7 +141,7 @@ void ChapterHtmlSlimParser::startNewTextBlock(const BlockStyle& blockStyle) {
anchorData.push_back({std::move(pendingAnchorId), static_cast<uint16_t>(completedPageCount)}); anchorData.push_back({std::move(pendingAnchorId), static_cast<uint16_t>(completedPageCount)});
pendingAnchorId.clear(); pendingAnchorId.clear();
} }
currentTextBlock.reset(new ParsedText(extraParagraphSpacing, hyphenationEnabled, blockStyle)); currentTextBlock.reset(new ParsedText(extraParagraphSpacing, hyphenationEnabled, focusReadingEnabled, blockStyle));
wordsExtractedInBlock = 0; wordsExtractedInBlock = 0;
} }
@@ -47,6 +47,7 @@ class ChapterHtmlSlimParser {
uint16_t viewportWidth; uint16_t viewportWidth;
uint16_t viewportHeight; uint16_t viewportHeight;
bool hyphenationEnabled; bool hyphenationEnabled;
bool focusReadingEnabled;
const CssParser* cssParser; const CssParser* cssParser;
bool embeddedStyle; bool embeddedStyle;
uint8_t imageRendering; uint8_t imageRendering;
@@ -101,6 +102,7 @@ class ChapterHtmlSlimParser {
const int fontId, const float lineCompression, const bool extraParagraphSpacing, const int fontId, const float lineCompression, const bool extraParagraphSpacing,
const uint8_t paragraphAlignment, const uint16_t viewportWidth, const uint8_t paragraphAlignment, const uint16_t viewportWidth,
const uint16_t viewportHeight, const bool hyphenationEnabled, const uint16_t viewportHeight, const bool hyphenationEnabled,
const bool focusReadingEnabled,
const std::function<void(std::unique_ptr<Page>, uint16_t, uint16_t)>& completePageFn, const std::function<void(std::unique_ptr<Page>, uint16_t, uint16_t)>& completePageFn,
const bool embeddedStyle, const std::string& contentBase, const bool embeddedStyle, const std::string& contentBase,
const std::string& imageBasePath, const uint8_t imageRendering = 0, const std::string& imageBasePath, const uint8_t imageRendering = 0,
@@ -116,6 +118,7 @@ class ChapterHtmlSlimParser {
viewportWidth(viewportWidth), viewportWidth(viewportWidth),
viewportHeight(viewportHeight), viewportHeight(viewportHeight),
hyphenationEnabled(hyphenationEnabled), hyphenationEnabled(hyphenationEnabled),
focusReadingEnabled(focusReadingEnabled),
completePageFn(completePageFn), completePageFn(completePageFn),
popupFn(popupFn), popupFn(popupFn),
cssParser(cssParser), cssParser(cssParser),
+34 -3
View File
@@ -10,6 +10,35 @@
#include "FontCacheManager.h" #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<uint8_t>(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 { const uint8_t* GfxRenderer::getGlyphBitmap(const EpdFontData* fontData, const EpdGlyph* glyph) const {
if (fontData->groups != nullptr) { if (fontData->groups != nullptr) {
auto* fd = fontCacheManager_ ? fontCacheManager_->getDecompressor() : 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 // Advance table fast-path for SD card fonts during layout
auto sdIt = sdCardFonts_.find(fontId); auto sdIt = sdCardFonts_.find(fontId);
if (sdIt != sdCardFonts_.end() && sdIt->second->hasAdvanceTable()) { if (sdIt != sdCardFonts_.end() && sdIt->second->hasAdvanceTable()) {
return fp4::toPixel(sdIt->second->getAdvance(' ', static_cast<uint8_t>(style))); const uint8_t resolvedStyle = resolveSdCardStyle(*sdIt->second, style);
return fp4::toPixel(sdIt->second->getAdvance(' ', resolvedStyle));
} }
const auto fontIt = fontMap.find(fontId); 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. // so we return just the space advance without kerning.
auto sdIt = sdCardFonts_.find(fontId); auto sdIt = sdCardFonts_.find(fontId);
if (sdIt != sdCardFonts_.end() && sdIt->second->hasAdvanceTable()) { if (sdIt != sdCardFonts_.end() && sdIt->second->hasAdvanceTable()) {
return fp4::toPixel(sdIt->second->getAdvance(' ', static_cast<uint8_t>(style))); const uint8_t resolvedStyle = resolveSdCardStyle(*sdIt->second, style);
return fp4::toPixel(sdIt->second->getAdvance(' ', resolvedStyle));
} }
const auto fontIt = fontMap.find(fontId); 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); auto sdIt = sdCardFonts_.find(fontId);
if (sdIt != sdCardFonts_.end() && sdIt->second->hasAdvanceTable()) { if (sdIt != sdCardFonts_.end() && sdIt->second->hasAdvanceTable()) {
int32_t widthFP = 0; int32_t widthFP = 0;
const uint8_t styleIdx = static_cast<uint8_t>(style); const uint8_t styleIdx = resolveSdCardStyle(*sdIt->second, style);
while (uint32_t cp = utf8NextCodepoint(reinterpret_cast<const uint8_t**>(&text))) { while (uint32_t cp = utf8NextCodepoint(reinterpret_cast<const uint8_t**>(&text))) {
widthFP += sdIt->second->getAdvance(cp, styleIdx); widthFP += sdIt->second->getAdvance(cp, styleIdx);
} }
+1
View File
@@ -259,6 +259,7 @@ STR_SECTION_PREFIX: "Раздзел"
STR_UPLOAD: "Адправіць" STR_UPLOAD: "Адправіць"
STR_BOOK_S_STYLE: "Стыль кнігі" STR_BOOK_S_STYLE: "Стыль кнігі"
STR_EMBEDDED_STYLE: "Убудаваны стыль" STR_EMBEDDED_STYLE: "Убудаваны стыль"
STR_FOCUS_READING: "Фокуснае чытанне"
STR_OPDS_SERVER_URL: "URL OPDS сервера" STR_OPDS_SERVER_URL: "URL OPDS сервера"
STR_SCREENSHOT_BUTTON: "Зрабіць здымак экрана" STR_SCREENSHOT_BUTTON: "Зрабіць здымак экрана"
STR_IMAGES: "Выявы" STR_IMAGES: "Выявы"
+1
View File
@@ -287,6 +287,7 @@ STR_SECTION_PREFIX: "Secció "
STR_UPLOAD: "Puja" STR_UPLOAD: "Puja"
STR_BOOK_S_STYLE: "Estil del llibre" STR_BOOK_S_STYLE: "Estil del llibre"
STR_EMBEDDED_STYLE: "Estil incrustat" STR_EMBEDDED_STYLE: "Estil incrustat"
STR_FOCUS_READING: "Lectura enfocada"
STR_OPDS_SERVER_URL: "URL del servidor OPDS" STR_OPDS_SERVER_URL: "URL del servidor OPDS"
STR_FOOTNOTES: "Notes al peu" STR_FOOTNOTES: "Notes al peu"
STR_NO_FOOTNOTES: "No hi ha notes al peu en aquesta pàgina" STR_NO_FOOTNOTES: "No hi ha notes al peu en aquesta pàgina"
+1
View File
@@ -264,6 +264,7 @@ STR_SECTION_PREFIX: "Sekce"
STR_UPLOAD: "Nahrát" STR_UPLOAD: "Nahrát"
STR_BOOK_S_STYLE: "Styl knihy" STR_BOOK_S_STYLE: "Styl knihy"
STR_EMBEDDED_STYLE: "Vložený styl" STR_EMBEDDED_STYLE: "Vložený styl"
STR_FOCUS_READING: "Soustředěné čtení"
STR_OPDS_SERVER_URL: "URL serveru OPDS" STR_OPDS_SERVER_URL: "URL serveru OPDS"
STR_SCREENSHOT_BUTTON: "Udělat snímek obrazovky" STR_SCREENSHOT_BUTTON: "Udělat snímek obrazovky"
STR_TILT_PAGE_TURN: "Otáčení stránek nakloněním" STR_TILT_PAGE_TURN: "Otáčení stránek nakloněním"
+1
View File
@@ -287,6 +287,7 @@ STR_SECTION_PREFIX: "Afsnit "
STR_UPLOAD: "Upload" STR_UPLOAD: "Upload"
STR_BOOK_S_STYLE: "Bogens stil" STR_BOOK_S_STYLE: "Bogens stil"
STR_EMBEDDED_STYLE: "Indlejret stil" STR_EMBEDDED_STYLE: "Indlejret stil"
STR_FOCUS_READING: "Fokuslæsning"
STR_OPDS_SERVER_URL: "OPDS Server URL" STR_OPDS_SERVER_URL: "OPDS Server URL"
STR_FOOTNOTES: "Fodnoter" STR_FOOTNOTES: "Fodnoter"
STR_NO_FOOTNOTES: "Ingen fodnoter på denne side" STR_NO_FOOTNOTES: "Ingen fodnoter på denne side"
+1
View File
@@ -287,6 +287,7 @@ STR_SECTION_PREFIX: "Sectie "
STR_UPLOAD: "Uploaden" STR_UPLOAD: "Uploaden"
STR_BOOK_S_STYLE: "Stijl van boek" STR_BOOK_S_STYLE: "Stijl van boek"
STR_EMBEDDED_STYLE: "Ingebedde stijl" STR_EMBEDDED_STYLE: "Ingebedde stijl"
STR_FOCUS_READING: "Gefocust lezen"
STR_OPDS_SERVER_URL: "OPDS-server URL" STR_OPDS_SERVER_URL: "OPDS-server URL"
STR_FOOTNOTES: "Voetnoten" STR_FOOTNOTES: "Voetnoten"
STR_NO_FOOTNOTES: "Geen voetnoten op deze pagina" STR_NO_FOOTNOTES: "Geen voetnoten op deze pagina"
+5 -3
View File
@@ -296,6 +296,7 @@ STR_SECTION_PREFIX: "Section "
STR_UPLOAD: "Upload" STR_UPLOAD: "Upload"
STR_BOOK_S_STYLE: "Book's Style" STR_BOOK_S_STYLE: "Book's Style"
STR_EMBEDDED_STYLE: "Embedded Style" STR_EMBEDDED_STYLE: "Embedded Style"
STR_FOCUS_READING: "Focus Reading"
STR_OPDS_SERVER_URL: "OPDS Server URL" STR_OPDS_SERVER_URL: "OPDS Server URL"
STR_SET_SLEEP_COVER: "Set Cover" STR_SET_SLEEP_COVER: "Set Cover"
STR_FOOTNOTES: "Footnotes" STR_FOOTNOTES: "Footnotes"
@@ -310,8 +311,8 @@ STR_DELETE_CONFIRM: "Delete this server?"
STR_OPDS_SERVERS: "OPDS Servers" STR_OPDS_SERVERS: "OPDS Servers"
STR_AUTO_TURN_ENABLED: "Auto Turn Enabled: " STR_AUTO_TURN_ENABLED: "Auto Turn Enabled: "
STR_AUTO_TURN_PAGES_PER_MIN: "Auto Turn (Pages Per Minute)" STR_AUTO_TURN_PAGES_PER_MIN: "Auto Turn (Pages Per Minute)"
STR_DOWNLOAD_FONTS: "Download Fonts" STR_MANAGE_FONTS: "Manage Fonts"
STR_FONT_DOWNLOAD: "Font Download" STR_FONT_BROWSER: "Font Browser"
STR_LOADING_FONT_LIST: "Loading font list..." STR_LOADING_FONT_LIST: "Loading font list..."
STR_NO_FONTS_AVAILABLE: "No fonts available" STR_NO_FONTS_AVAILABLE: "No fonts available"
STR_FONT_INSTALLED: "Font installed!" STR_FONT_INSTALLED: "Font installed!"
@@ -322,7 +323,8 @@ STR_SD_CARD_FULL: "Insufficient SD card space"
STR_FILES_LABEL: "Files: " STR_FILES_LABEL: "Files: "
STR_SIZE_LABEL: "Size: " STR_SIZE_LABEL: "Size: "
STR_REDOWNLOAD: "Re-download" 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_ALL_FONTS_INSTALLED: "All fonts installed!"
STR_UPDATE_AVAILABLE: "Update" STR_UPDATE_AVAILABLE: "Update"
STR_CRASH_TITLE: "System Crash" STR_CRASH_TITLE: "System Crash"
+1
View File
@@ -262,6 +262,7 @@ STR_SECTION_PREFIX: "Osio "
STR_UPLOAD: "Lähetä" STR_UPLOAD: "Lähetä"
STR_BOOK_S_STYLE: "Kirjan tyyli" STR_BOOK_S_STYLE: "Kirjan tyyli"
STR_EMBEDDED_STYLE: "Upotettu tyyli" STR_EMBEDDED_STYLE: "Upotettu tyyli"
STR_FOCUS_READING: "Keskittynyt lukeminen"
STR_OPDS_SERVER_URL: "OPDS-palvelimen osoite" STR_OPDS_SERVER_URL: "OPDS-palvelimen osoite"
STR_SCREENSHOT_BUTTON: "Ota kuvakaappaus" STR_SCREENSHOT_BUTTON: "Ota kuvakaappaus"
STR_TILT_PAGE_TURN: "Sivunkääntö kallistamalla" STR_TILT_PAGE_TURN: "Sivunkääntö kallistamalla"
+1
View File
@@ -288,6 +288,7 @@ STR_SECTION_PREFIX: "Section "
STR_UPLOAD: "Envoyer" STR_UPLOAD: "Envoyer"
STR_BOOK_S_STYLE: "Style du livre" STR_BOOK_S_STYLE: "Style du livre"
STR_EMBEDDED_STYLE: "Style intégré" STR_EMBEDDED_STYLE: "Style intégré"
STR_FOCUS_READING: "Lecture focalisée"
STR_OPDS_SERVER_URL: "URL serveur OPDS" STR_OPDS_SERVER_URL: "URL serveur OPDS"
STR_FOOTNOTES: "Notes de bas de page" STR_FOOTNOTES: "Notes de bas de page"
STR_NO_FOOTNOTES: "Aucune note sur cette page" STR_NO_FOOTNOTES: "Aucune note sur cette page"
+1
View File
@@ -289,6 +289,7 @@ STR_SECTION_PREFIX: "Abschnitt"
STR_UPLOAD: "Hochladen" STR_UPLOAD: "Hochladen"
STR_BOOK_S_STYLE: "Buch-Stil" STR_BOOK_S_STYLE: "Buch-Stil"
STR_EMBEDDED_STYLE: "Eingebetteter Stil" STR_EMBEDDED_STYLE: "Eingebetteter Stil"
STR_FOCUS_READING: "Fokus-Lesen"
STR_OPDS_SERVER_URL: "OPDS-Server-URL" STR_OPDS_SERVER_URL: "OPDS-Server-URL"
STR_SCREENSHOT_BUTTON: "Screenshot aufnehmen" STR_SCREENSHOT_BUTTON: "Screenshot aufnehmen"
STR_FOOTNOTES: "Fußnoten" STR_FOOTNOTES: "Fußnoten"
+1
View File
@@ -284,6 +284,7 @@ STR_SECTION_PREFIX: "Szakasz "
STR_UPLOAD: "Feltöltés" STR_UPLOAD: "Feltöltés"
STR_BOOK_S_STYLE: "Könyv stílusa" STR_BOOK_S_STYLE: "Könyv stílusa"
STR_EMBEDDED_STYLE: "Beágyazott stílus" STR_EMBEDDED_STYLE: "Beágyazott stílus"
STR_FOCUS_READING: "Fókuszált olvasás"
STR_OPDS_SERVER_URL: "OPDS szerver URL" STR_OPDS_SERVER_URL: "OPDS szerver URL"
STR_FOOTNOTES: "Lábjegyzetek" STR_FOOTNOTES: "Lábjegyzetek"
STR_NO_FOOTNOTES: "Nincsenek lábjegyzetek ezen az oldalon" STR_NO_FOOTNOTES: "Nincsenek lábjegyzetek ezen az oldalon"
+1
View File
@@ -288,6 +288,7 @@ STR_SECTION_PREFIX: "Sezione "
STR_UPLOAD: "Carica" STR_UPLOAD: "Carica"
STR_BOOK_S_STYLE: "Stile libro" STR_BOOK_S_STYLE: "Stile libro"
STR_EMBEDDED_STYLE: "Stile integrato dell'epub" STR_EMBEDDED_STYLE: "Stile integrato dell'epub"
STR_FOCUS_READING: "Lettura focalizzata"
STR_OPDS_SERVER_URL: "Server OPDS" STR_OPDS_SERVER_URL: "Server OPDS"
STR_FOOTNOTES: "Note a piè pagina" STR_FOOTNOTES: "Note a piè pagina"
STR_NO_FOOTNOTES: "Nessuna nota in questa pagina" STR_NO_FOOTNOTES: "Nessuna nota in questa pagina"
+1
View File
@@ -258,6 +258,7 @@ STR_SECTION_PREFIX: "Бөлім "
STR_UPLOAD: "Жүктеп салу" STR_UPLOAD: "Жүктеп салу"
STR_BOOK_S_STYLE: "Кітап стилі" STR_BOOK_S_STYLE: "Кітап стилі"
STR_EMBEDDED_STYLE: "Кірістірілген стиль" STR_EMBEDDED_STYLE: "Кірістірілген стиль"
STR_FOCUS_READING: "Зейінді оқу"
STR_OPDS_SERVER_URL: "OPDS сервері URL" STR_OPDS_SERVER_URL: "OPDS сервері URL"
STR_NO_FILES_FOUND: "Файлдар табылмады" STR_NO_FILES_FOUND: "Файлдар табылмады"
STR_IMAGES: "Суреттер" STR_IMAGES: "Суреттер"
+1
View File
@@ -284,6 +284,7 @@ STR_SECTION_PREFIX: "Dalis "
STR_UPLOAD: "Įkelti" STR_UPLOAD: "Įkelti"
STR_BOOK_S_STYLE: "Knygos stilius" STR_BOOK_S_STYLE: "Knygos stilius"
STR_EMBEDDED_STYLE: "Integruotas stilius" STR_EMBEDDED_STYLE: "Integruotas stilius"
STR_FOCUS_READING: "Sufokusuotas skaitymas"
STR_OPDS_SERVER_URL: "OPDS URL" STR_OPDS_SERVER_URL: "OPDS URL"
STR_FOOTNOTES: "Išnašos" STR_FOOTNOTES: "Išnašos"
STR_NO_FOOTNOTES: "Šiame psl. išnašų nėra" STR_NO_FOOTNOTES: "Šiame psl. išnašų nėra"
+3 -1
View File
@@ -296,6 +296,7 @@ STR_SECTION_PREFIX: "Sekcja "
STR_UPLOAD: "Wyślij" STR_UPLOAD: "Wyślij"
STR_BOOK_S_STYLE: "Styl książki" STR_BOOK_S_STYLE: "Styl książki"
STR_EMBEDDED_STYLE: "Style wbudowane w EPUB" STR_EMBEDDED_STYLE: "Style wbudowane w EPUB"
STR_FOCUS_READING: "Czytanie skupione"
STR_OPDS_SERVER_URL: "URL serwera OPDS" STR_OPDS_SERVER_URL: "URL serwera OPDS"
STR_SET_SLEEP_COVER: "Ustaw okładkę" STR_SET_SLEEP_COVER: "Ustaw okładkę"
STR_FOOTNOTES: "Przypisy" STR_FOOTNOTES: "Przypisy"
@@ -322,7 +323,8 @@ STR_SD_CARD_FULL: "Za mało pamięci na karcie SD"
STR_FILES_LABEL: "Pliki: " STR_FILES_LABEL: "Pliki: "
STR_SIZE_LABEL: "Rozmiar: " STR_SIZE_LABEL: "Rozmiar: "
STR_REDOWNLOAD: "Re-download" 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_ALL_FONTS_INSTALLED: "Wszystkie czcionki zainstalowane!"
STR_UPDATE_AVAILABLE: "Uaktualnij" STR_UPDATE_AVAILABLE: "Uaktualnij"
STR_CRASH_TITLE: "Awaria systemu" STR_CRASH_TITLE: "Awaria systemu"
+1
View File
@@ -264,6 +264,7 @@ STR_SECTION_PREFIX: "Seção"
STR_UPLOAD: "Enviar" STR_UPLOAD: "Enviar"
STR_BOOK_S_STYLE: "Estilo do livro" STR_BOOK_S_STYLE: "Estilo do livro"
STR_EMBEDDED_STYLE: "Estilo embutido" STR_EMBEDDED_STYLE: "Estilo embutido"
STR_FOCUS_READING: "Leitura focada"
STR_OPDS_SERVER_URL: "URL do servidor OPDS" STR_OPDS_SERVER_URL: "URL do servidor OPDS"
STR_SCREENSHOT_BUTTON: "Capturar tela" STR_SCREENSHOT_BUTTON: "Capturar tela"
STR_TILT_PAGE_TURN: "Virar página por inclinação" STR_TILT_PAGE_TURN: "Virar página por inclinação"
+1
View File
@@ -287,6 +287,7 @@ STR_SECTION_PREFIX: "Secţiune "
STR_UPLOAD: "Încărcare" STR_UPLOAD: "Încărcare"
STR_BOOK_S_STYLE: "Stilul cărţii" STR_BOOK_S_STYLE: "Stilul cărţii"
STR_EMBEDDED_STYLE: "Stil încorporat" STR_EMBEDDED_STYLE: "Stil încorporat"
STR_FOCUS_READING: "Lectură concentrată"
STR_OPDS_SERVER_URL: "URL server OPDS" STR_OPDS_SERVER_URL: "URL server OPDS"
STR_FOOTNOTES: "Note de subsol" STR_FOOTNOTES: "Note de subsol"
STR_NO_FOOTNOTES: "Nicio notă de subsol" STR_NO_FOOTNOTES: "Nicio notă de subsol"
+1
View File
@@ -291,6 +291,7 @@ STR_SECTION_PREFIX: "Раздел "
STR_UPLOAD: "Отправить" STR_UPLOAD: "Отправить"
STR_BOOK_S_STYLE: "Стиль книги" STR_BOOK_S_STYLE: "Стиль книги"
STR_EMBEDDED_STYLE: "Встроенный стиль" STR_EMBEDDED_STYLE: "Встроенный стиль"
STR_FOCUS_READING: "Фокусное чтение"
STR_OPDS_SERVER_URL: "URL OPDS сервера" STR_OPDS_SERVER_URL: "URL OPDS сервера"
STR_SCREENSHOT_BUTTON: "Сделать снимок экрана" STR_SCREENSHOT_BUTTON: "Сделать снимок экрана"
STR_AUTO_TURN_ENABLED: "Автоперелистывание: " STR_AUTO_TURN_ENABLED: "Автоперелистывание: "
+1
View File
@@ -284,6 +284,7 @@ STR_SECTION_PREFIX: "Razdelek "
STR_UPLOAD: "Naloži" STR_UPLOAD: "Naloži"
STR_BOOK_S_STYLE: "Slog knjige" STR_BOOK_S_STYLE: "Slog knjige"
STR_EMBEDDED_STYLE: "Vgrajen slog" STR_EMBEDDED_STYLE: "Vgrajen slog"
STR_FOCUS_READING: "Fokusirano branje"
STR_OPDS_SERVER_URL: "URL OPDS strežnika" STR_OPDS_SERVER_URL: "URL OPDS strežnika"
STR_FOOTNOTES: "Opombe" STR_FOOTNOTES: "Opombe"
STR_NO_FOOTNOTES: "Na tej strani ni opomb" STR_NO_FOOTNOTES: "Na tej strani ni opomb"
+1
View File
@@ -288,6 +288,7 @@ STR_SECTION_PREFIX: "Secc.:"
STR_UPLOAD: "Subir" STR_UPLOAD: "Subir"
STR_BOOK_S_STYLE: "Estilo del libro" STR_BOOK_S_STYLE: "Estilo del libro"
STR_EMBEDDED_STYLE: "Estilo integrado" STR_EMBEDDED_STYLE: "Estilo integrado"
STR_FOCUS_READING: "Lectura enfocada"
STR_OPDS_SERVER_URL: "URL del servidor OPDS" STR_OPDS_SERVER_URL: "URL del servidor OPDS"
STR_FOOTNOTES: "Pie de página" STR_FOOTNOTES: "Pie de página"
STR_NO_FOOTNOTES: "No hay notas al pie de esta página" STR_NO_FOOTNOTES: "No hay notas al pie de esta página"
+5 -3
View File
@@ -296,6 +296,7 @@ STR_SECTION_PREFIX: "Sektion"
STR_UPLOAD: "Uppladdning" STR_UPLOAD: "Uppladdning"
STR_BOOK_S_STYLE: "Bokstil" STR_BOOK_S_STYLE: "Bokstil"
STR_EMBEDDED_STYLE: "Inbäddad stil" STR_EMBEDDED_STYLE: "Inbäddad stil"
STR_FOCUS_READING: "Fokusläsning"
STR_OPDS_SERVER_URL: "OPDS-serveradress" STR_OPDS_SERVER_URL: "OPDS-serveradress"
STR_SET_SLEEP_COVER: "Ställ in omslag" STR_SET_SLEEP_COVER: "Ställ in omslag"
STR_FOOTNOTES: "Fotnoter" STR_FOOTNOTES: "Fotnoter"
@@ -310,8 +311,8 @@ STR_DELETE_CONFIRM: "Vill du ta bort den här servern?"
STR_OPDS_SERVERS: "OPDS-servrar" STR_OPDS_SERVERS: "OPDS-servrar"
STR_AUTO_TURN_ENABLED: "Automatisk vändning aktiverad: " STR_AUTO_TURN_ENABLED: "Automatisk vändning aktiverad: "
STR_AUTO_TURN_PAGES_PER_MIN: "Automatisk vändning (sidor per minut)" STR_AUTO_TURN_PAGES_PER_MIN: "Automatisk vändning (sidor per minut)"
STR_DOWNLOAD_FONTS: "Ladda ner teckensnitt" STR_MANAGE_FONTS: "Hantera teckensnitt"
STR_FONT_DOWNLOAD: "Nedladdning av teckensnitt" STR_FONT_BROWSER: "Teckensnittsbläddrare"
STR_LOADING_FONT_LIST: "Laddar teckensnittslista..." STR_LOADING_FONT_LIST: "Laddar teckensnittslista..."
STR_NO_FONTS_AVAILABLE: "Inga teckensnitt tillgängliga" STR_NO_FONTS_AVAILABLE: "Inga teckensnitt tillgängliga"
STR_FONT_INSTALLED: "Teckensnitt installerat!" STR_FONT_INSTALLED: "Teckensnitt installerat!"
@@ -322,7 +323,8 @@ STR_SD_CARD_FULL: "Otillräckligt utrymme på SD-kortet"
STR_FILES_LABEL: "Filer: " STR_FILES_LABEL: "Filer: "
STR_SIZE_LABEL: "Storlek: " STR_SIZE_LABEL: "Storlek: "
STR_REDOWNLOAD: "Ladda ner igen" 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_ALL_FONTS_INSTALLED: "Alla teckensnitt installerade!"
STR_UPDATE_AVAILABLE: "Uppdatering" STR_UPDATE_AVAILABLE: "Uppdatering"
STR_CRASH_TITLE: "Systemkrasch" STR_CRASH_TITLE: "Systemkrasch"
+1
View File
@@ -262,6 +262,7 @@ STR_SECTION_PREFIX: "Bölüm "
STR_UPLOAD: "Yükle" STR_UPLOAD: "Yükle"
STR_BOOK_S_STYLE: "Kitabın Stili" STR_BOOK_S_STYLE: "Kitabın Stili"
STR_EMBEDDED_STYLE: "Gömülü Stil" STR_EMBEDDED_STYLE: "Gömülü Stil"
STR_FOCUS_READING: "Odaklanmış Okuma"
STR_OPDS_SERVER_URL: "OPDS Sunucu Adresi" STR_OPDS_SERVER_URL: "OPDS Sunucu Adresi"
STR_AUTO_TURN_ENABLED: "Otomatik Çevirme Etkin: " STR_AUTO_TURN_ENABLED: "Otomatik Çevirme Etkin: "
STR_AUTO_TURN_PAGES_PER_MIN: "Otomatik Çevirme (Dakikada Sayfa)" STR_AUTO_TURN_PAGES_PER_MIN: "Otomatik Çevirme (Dakikada Sayfa)"
+36 -1
View File
@@ -87,7 +87,7 @@ STR_TIME_TO_SLEEP: "Перехід в режим сну"
STR_SHOW_HIDDEN_FILES: "Показати приховані файли" STR_SHOW_HIDDEN_FILES: "Показати приховані файли"
STR_REFRESH_FREQ: "Частота оновлення екрану" STR_REFRESH_FREQ: "Частота оновлення екрану"
STR_KOREADER_SYNC: "Синхронізація KOReader" STR_KOREADER_SYNC: "Синхронізація KOReader"
STR_CHECK_UPDATES: "Перевірити оновлення" STR_CHECK_UPDATES: "Перевірити оновлення системи"
STR_LANGUAGE: "Мова" STR_LANGUAGE: "Мова"
STR_CLEAR_READING_CACHE: "Очистити кеш книг" STR_CLEAR_READING_CACHE: "Очистити кеш книг"
STR_USERNAME: "Ім'я користувача" STR_USERNAME: "Ім'я користувача"
@@ -171,6 +171,7 @@ STR_NO_UPDATE: "Оновлень немає"
STR_UPDATE_FAILED: "Оновлення не вдалося" STR_UPDATE_FAILED: "Оновлення не вдалося"
STR_UPDATE_COMPLETE: "Оновлення завершено" STR_UPDATE_COMPLETE: "Оновлення завершено"
STR_POWER_ON_HINT: "Натисніть і утримуйте кнопку живлення, щоб увімкнути" STR_POWER_ON_HINT: "Натисніть і утримуйте кнопку живлення, щоб увімкнути"
STR_RESTARTING_HINT: "Перезавантаження... Якщо пристрій не перезавантажується, утримуйте кнопку живлення кілька секунд."
STR_NO_ENTRIES: "Записів не знайдено" STR_NO_ENTRIES: "Записів не знайдено"
STR_DOWNLOADING: "Завантаження..." STR_DOWNLOADING: "Завантаження..."
STR_DOWNLOAD_FAILED: "Завантаження не вдалося" STR_DOWNLOAD_FAILED: "Завантаження не вдалося"
@@ -228,6 +229,9 @@ STR_EXAMPLE_BOOK: "Назва книги"
STR_PREVIEW: "Перегляд" STR_PREVIEW: "Перегляд"
STR_TITLE: "Назва" STR_TITLE: "Назва"
STR_BATTERY: "Акумулятор" STR_BATTERY: "Акумулятор"
STR_XTC_STATUS_BAR: "XTC Рядок прогресу"
STR_BOTTOM: "Низ"
STR_TOP: "Верх"
STR_UI_THEME: "Тема інтерфейсу" STR_UI_THEME: "Тема інтерфейсу"
STR_THEME_CLASSIC: "Класична" STR_THEME_CLASSIC: "Класична"
STR_THEME_LYRA: "Lyra" STR_THEME_LYRA: "Lyra"
@@ -292,7 +296,9 @@ STR_SECTION_PREFIX: "Розділ "
STR_UPLOAD: "Завантажити" STR_UPLOAD: "Завантажити"
STR_BOOK_S_STYLE: "Стиль книги" STR_BOOK_S_STYLE: "Стиль книги"
STR_EMBEDDED_STYLE: "Вбудований стиль" STR_EMBEDDED_STYLE: "Вбудований стиль"
STR_FOCUS_READING: "Фокусне читання"
STR_OPDS_SERVER_URL: "URL сервера OPDS" STR_OPDS_SERVER_URL: "URL сервера OPDS"
STR_SET_SLEEP_COVER: "Як обкл."
STR_FOOTNOTES: "Примітки" STR_FOOTNOTES: "Примітки"
STR_NO_FOOTNOTES: "На цій сторінці немає приміток" STR_NO_FOOTNOTES: "На цій сторінці немає приміток"
STR_LINK: "[посилання]" STR_LINK: "[посилання]"
@@ -305,6 +311,22 @@ STR_DELETE_CONFIRM: "Видалити цей сервер?"
STR_OPDS_SERVERS: "Сервери OPDS" STR_OPDS_SERVERS: "Сервери OPDS"
STR_AUTO_TURN_ENABLED: "Автоперегортання увімк: " STR_AUTO_TURN_ENABLED: "Автоперегортання увімк: "
STR_AUTO_TURN_PAGES_PER_MIN: "Автоперегортання (ст/хв)" 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_TITLE: "Збій Системи"
STR_CRASH_DESCRIPTION: "Дані про збій збережено в crash_report.txt. Додайте цей файл до вашого звіту про помилку." STR_CRASH_DESCRIPTION: "Дані про збій збережено в crash_report.txt. Додайте цей файл до вашого звіту про помилку."
STR_CRASH_REASON: "Причина збою:" STR_CRASH_REASON: "Причина збою:"
@@ -325,3 +347,16 @@ STR_KB_HINT_SECONDARY_CHAR: "Затисніть ВИБРАТИ для додат
STR_KB_HINT_UPPER_SECONDARY: "Затисніть ВИБРАТИ для ВЕЛИКИХ літер / символів" STR_KB_HINT_UPPER_SECONDARY: "Затисніть ВИБРАТИ для ВЕЛИКИХ літер / символів"
STR_KB_HINT_LOWER_SECONDARY: "Затисніть ВИБРАТИ для малих літер / символів" STR_KB_HINT_LOWER_SECONDARY: "Затисніть ВИБРАТИ для малих літер / символів"
STR_KB_HINT_URL_SNIPPETS: "Натисніть URL для вибору шаблонів" 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-карти та виберіть його"
+12 -6
View File
@@ -1,18 +1,21 @@
import sys
import os import os
from PIL import Image
import cairosvg
import io import io
import sys
threshold = 128 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): def svg_to_png_bytes(svg_path, width, height):
import cairosvg
with open(svg_path, 'rb') as f: with open(svg_path, 'rb') as f:
svg_data = f.read() svg_data = f.read()
png_bytes = cairosvg.svg2png(bytestring=svg_data, output_width=width, output_height=height) png_bytes = cairosvg.svg2png(bytestring=svg_data, output_width=width, output_height=height)
return png_bytes return png_bytes
def load_image(path, width, height): def load_image(path, width, height):
from PIL import Image
ext = os.path.splitext(path)[1].lower() ext = os.path.splitext(path)[1].lower()
if ext == '.svg': if ext == '.svg':
png_bytes = svg_to_png_bytes(path, width, height) png_bytes = svg_to_png_bytes(path, width, height)
@@ -58,8 +61,11 @@ def image_to_c_array(img, array_name):
return c return c
def main(): def main():
if len(sys.argv) < 5: if any(arg in ('-h', '--help') for arg in sys.argv[1:]):
print('Usage: python convert_image.py input.png output_name width height') print(USAGE)
sys.exit(0)
if len(sys.argv) != 5:
print(USAGE)
sys.exit(1) sys.exit(1)
input_path, output_name, width, height = sys.argv[1:5] input_path, output_name, width, height = sys.argv[1:5]
array_name = output_name.capitalize() + 'Icon' array_name = output_name.capitalize() + 'Icon'
@@ -77,4 +83,4 @@ def main():
print(f'Wrote {output_path}') print(f'Wrote {output_path}')
if __name__ == '__main__': if __name__ == '__main__':
main() main()
+38 -28
View File
@@ -35,6 +35,43 @@ import threading
from collections import deque from collections import deque
from datetime import datetime 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 # Try to import potentially missing packages
PACKAGE_MAPPING: dict[str, str] = { PACKAGE_MAPPING: dict[str, str] = {
"serial": "pyserial", "serial": "pyserial",
@@ -401,34 +438,7 @@ def main() -> None:
- Screenshot capture capability - Screenshot capture capability
- Graceful shutdown on Ctrl-C or window close - Graceful shutdown on Ctrl-C or window close
""" """
parser = argparse.ArgumentParser( parser = build_arg_parser()
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)",
)
args = parser.parse_args() args = parser.parse_args()
port = args.port port = args.port
if port is None: if port is None:
+2
View File
@@ -16,6 +16,8 @@ The input directory may be flat (all .cpfont files in one dir) or nested
convention <FamilyName>_<size>.cpfont. convention <FamilyName>_<size>.cpfont.
""" """
from __future__ import annotations
import argparse import argparse
import json import json
import os import os
@@ -137,10 +137,15 @@ Also includes:
import io import io
import os import os
import sys
import zipfile import zipfile
import uuid import uuid
from datetime import datetime from datetime import datetime
if any(arg in ("-h", "--help") for arg in sys.argv[1:]):
print(__doc__.strip())
sys.exit(0)
try: try:
from PIL import Image, ImageDraw, ImageFont from PIL import Image, ImageDraw, ImageFont
except ImportError: except ImportError:
+3
View File
@@ -326,4 +326,7 @@ def main():
if __name__ == '__main__': if __name__ == '__main__':
if any(arg in ("-h", "--help") for arg in sys.argv[1:]):
print(__doc__.strip())
sys.exit(0)
main() main()
+5
View File
@@ -11,9 +11,14 @@ Creates EPUBs with annotated JPEG and PNG images to verify:
""" """
import os import os
import sys
import zipfile import zipfile
from pathlib import Path from pathlib import Path
if any(arg in ("-h", "--help") for arg in sys.argv[1:]):
print(__doc__.strip())
sys.exit(0)
try: try:
from PIL import Image, ImageDraw, ImageFont from PIL import Image, ImageDraw, ImageFont
except ImportError: except ImportError:
+2
View File
@@ -213,6 +213,8 @@ class CrossPointSettings {
uint8_t fadingFix = 0; uint8_t fadingFix = 0;
// Use book's embedded CSS styles for EPUB rendering (1 = enabled, 0 = disabled) // Use book's embedded CSS styles for EPUB rendering (1 = enabled, 0 = disabled)
uint8_t embeddedStyle = 1; 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) // SD card font family name (empty = use built-in fontFamily)
char sdFontFamilyName[32] = ""; char sdFontFamilyName[32] = "";
// Show hidden files/directories (starting with '.') in the file browser (0 = hidden, 1 = show) // Show hidden files/directories (starting with '.') in the file browser (0 = hidden, 1 = show)
+14 -17
View File
@@ -5,15 +5,10 @@
#include "CrossPointSettings.h" #include "CrossPointSettings.h"
// Map fontSize enum (SMALL=0, MEDIUM=1, LARGE=2, EXTRA_LARGE=3) to the point static uint8_t fontSizeEnumFromSettings() {
// sizes shipped with the built-in fonts. Used to drive closest-pt selection
// in the SD card font registry (see SdCardFontFamilyInfo::pickClosestSize).
static constexpr uint8_t FONT_SIZE_TO_PT[CrossPointSettings::FONT_SIZE_COUNT] = {12, 14, 16, 18};
static uint8_t targetPtSizeFromSettings() {
uint8_t e = SETTINGS.fontSize; uint8_t e = SETTINGS.fontSize;
if (e >= CrossPointSettings::FONT_SIZE_COUNT) e = 1; // default to MEDIUM if (e >= CrossPointSettings::FONT_SIZE_COUNT) e = 1; // default to MEDIUM
return FONT_SIZE_TO_PT[e]; return e;
} }
void SdCardFontSystem::begin(GfxRenderer& renderer) { void SdCardFontSystem::begin(GfxRenderer& renderer) {
@@ -30,7 +25,7 @@ void SdCardFontSystem::begin(GfxRenderer& renderer) {
if (SETTINGS.sdFontFamilyName[0] != '\0') { if (SETTINGS.sdFontFamilyName[0] != '\0') {
const auto* family = registry_.findFamily(SETTINGS.sdFontFamilyName); const auto* family = registry_.findFamily(SETTINGS.sdFontFamilyName);
if (family) { 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); LOG_DBG("SDFS", "Loaded SD card font family: %s", SETTINGS.sdFontFamilyName);
} else { } else {
LOG_ERR("SDFS", "Failed to load SD font family: %s (clearing)", SETTINGS.sdFontFamilyName); 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 char* wantedFamily = SETTINGS.sdFontFamilyName;
const std::string& currentFamily = manager_.currentFamilyName(); const std::string& currentFamily = manager_.currentFamilyName();
const uint8_t targetPt = targetPtSizeFromSettings(); const uint8_t sizeEnum = fontSizeEnumFromSettings();
if (wantedFamily[0] == '\0') { if (wantedFamily[0] == '\0') {
if (!currentFamily.empty()) { if (!currentFamily.empty()) {
@@ -67,8 +62,8 @@ void SdCardFontSystem::ensureLoaded(GfxRenderer& renderer) {
return; return;
} }
// Reload if family changed OR if the user-selected size now resolves to a // Reload if family changed OR if the user-selected size maps to a
// different on-disk file than what's currently loaded OR if the registry was // different file than what's currently loaded OR if the registry was
// just rediscovered (file may have been replaced on disk). // just rediscovered (file may have been replaced on disk).
bool familyMatches = (currentFamily == wantedFamily); bool familyMatches = (currentFamily == wantedFamily);
if (familyMatches) { if (familyMatches) {
@@ -79,11 +74,13 @@ void SdCardFontSystem::ensureLoaded(GfxRenderer& renderer) {
SETTINGS.sdFontFamilyName[0] = '\0'; SETTINGS.sdFontFamilyName[0] = '\0';
return; return;
} }
const auto* best = family->pickClosestSize(targetPt); auto sizes = family->availableSizes();
const uint8_t bestPt = best ? best->pointSize : 0; uint8_t idx = sizeEnum;
if (!registryWasDirty && bestPt == manager_.currentPointSize()) return; if (idx >= sizes.size()) idx = sizes.size() - 1;
LOG_DBG("SDFS", "Reloading %s: size %u -> %u (target %u)%s", wantedFamily, manager_.currentPointSize(), bestPt, uint8_t wantedPt = sizes.empty() ? 0 : sizes[idx];
targetPt, registryWasDirty ? " [registry dirty]" : ""); 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()) { if (!currentFamily.empty()) {
@@ -92,7 +89,7 @@ void SdCardFontSystem::ensureLoaded(GfxRenderer& renderer) {
const auto* family = registry_.findFamily(wantedFamily); const auto* family = registry_.findFamily(wantedFamily);
if (family) { if (family) {
if (manager_.loadFamily(*family, renderer, targetPt)) { if (manager_.loadFamily(*family, renderer, sizeEnum)) {
LOG_DBG("SDFS", "Loaded SD font family: %s", wantedFamily); LOG_DBG("SDFS", "Loaded SD font family: %s", wantedFamily);
} else { } else {
LOG_ERR("SDFS", "Failed to load SD font family: %s (clearing)", wantedFamily); LOG_ERR("SDFS", "Failed to load SD font family: %s (clearing)", wantedFamily);
+2
View File
@@ -145,6 +145,8 @@ inline std::vector<SettingInfo> getSettingsList(const SdCardFontRegistry* regist
"paragraphAlignment", StrId::STR_CAT_READER), "paragraphAlignment", StrId::STR_CAT_READER),
SettingInfo::Toggle(StrId::STR_EMBEDDED_STYLE, &CrossPointSettings::embeddedStyle, "embeddedStyle", SettingInfo::Toggle(StrId::STR_EMBEDDED_STYLE, &CrossPointSettings::embeddedStyle, "embeddedStyle",
StrId::STR_CAT_READER), 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", SettingInfo::Toggle(StrId::STR_HYPHENATION, &CrossPointSettings::hyphenationEnabled, "hyphenationEnabled",
StrId::STR_CAT_READER), StrId::STR_CAT_READER),
SettingInfo::Enum(StrId::STR_ORIENTATION, &CrossPointSettings::orientation, SettingInfo::Enum(StrId::STR_ORIENTATION, &CrossPointSettings::orientation,
+4 -4
View File
@@ -609,7 +609,7 @@ void EpubReaderActivity::render(RenderLock&& lock) {
if (!section->loadSectionFile(SETTINGS.getReaderFontId(), SETTINGS.getReaderLineCompression(), if (!section->loadSectionFile(SETTINGS.getReaderFontId(), SETTINGS.getReaderLineCompression(),
SETTINGS.extraParagraphSpacing, SETTINGS.paragraphAlignment, viewportWidth, SETTINGS.extraParagraphSpacing, SETTINGS.paragraphAlignment, viewportWidth,
viewportHeight, SETTINGS.hyphenationEnabled, SETTINGS.embeddedStyle, viewportHeight, SETTINGS.hyphenationEnabled, SETTINGS.embeddedStyle,
SETTINGS.imageRendering)) { SETTINGS.imageRendering, SETTINGS.focusReadingEnabled)) {
LOG_DBG("ERS", "Cache not found, building..."); LOG_DBG("ERS", "Cache not found, building...");
GUI.drawPopup(renderer, tr(STR_INDEXING)); GUI.drawPopup(renderer, tr(STR_INDEXING));
@@ -619,7 +619,7 @@ void EpubReaderActivity::render(RenderLock&& lock) {
if (!section->createSectionFile(SETTINGS.getReaderFontId(), SETTINGS.getReaderLineCompression(), if (!section->createSectionFile(SETTINGS.getReaderFontId(), SETTINGS.getReaderLineCompression(),
SETTINGS.extraParagraphSpacing, SETTINGS.paragraphAlignment, viewportWidth, SETTINGS.extraParagraphSpacing, SETTINGS.paragraphAlignment, viewportWidth,
viewportHeight, SETTINGS.hyphenationEnabled, SETTINGS.embeddedStyle, viewportHeight, SETTINGS.hyphenationEnabled, SETTINGS.embeddedStyle,
SETTINGS.imageRendering, popupFn)) { SETTINGS.imageRendering, SETTINGS.focusReadingEnabled, popupFn)) {
LOG_ERR("ERS", "Failed to persist page data to SD"); LOG_ERR("ERS", "Failed to persist page data to SD");
section.reset(); section.reset();
showPendingSyncSaveError(); showPendingSyncSaveError();
@@ -750,7 +750,7 @@ void EpubReaderActivity::silentIndexNextChapterIfNeeded(const uint16_t viewportW
if (nextSection.loadSectionFile(SETTINGS.getReaderFontId(), SETTINGS.getReaderLineCompression(), if (nextSection.loadSectionFile(SETTINGS.getReaderFontId(), SETTINGS.getReaderLineCompression(),
SETTINGS.extraParagraphSpacing, SETTINGS.paragraphAlignment, viewportWidth, SETTINGS.extraParagraphSpacing, SETTINGS.paragraphAlignment, viewportWidth,
viewportHeight, SETTINGS.hyphenationEnabled, SETTINGS.embeddedStyle, viewportHeight, SETTINGS.hyphenationEnabled, SETTINGS.embeddedStyle,
SETTINGS.imageRendering)) { SETTINGS.imageRendering, SETTINGS.focusReadingEnabled)) {
return; return;
} }
@@ -758,7 +758,7 @@ void EpubReaderActivity::silentIndexNextChapterIfNeeded(const uint16_t viewportW
if (!nextSection.createSectionFile(SETTINGS.getReaderFontId(), SETTINGS.getReaderLineCompression(), if (!nextSection.createSectionFile(SETTINGS.getReaderFontId(), SETTINGS.getReaderLineCompression(),
SETTINGS.extraParagraphSpacing, SETTINGS.paragraphAlignment, viewportWidth, SETTINGS.extraParagraphSpacing, SETTINGS.paragraphAlignment, viewportWidth,
viewportHeight, SETTINGS.hyphenationEnabled, SETTINGS.embeddedStyle, viewportHeight, SETTINGS.hyphenationEnabled, SETTINGS.embeddedStyle,
SETTINGS.imageRendering)) { SETTINGS.imageRendering, SETTINGS.focusReadingEnabled)) {
LOG_ERR("ERS", "Failed silent indexing for chapter: %d", nextSpineIndex); LOG_ERR("ERS", "Failed silent indexing for chapter: %d", nextSpineIndex);
} }
} }
+118 -17
View File
@@ -11,6 +11,7 @@
#include "MappedInputManager.h" #include "MappedInputManager.h"
#include "SdCardFontGlobals.h" #include "SdCardFontGlobals.h"
#include "activities/network/WifiSelectionActivity.h" #include "activities/network/WifiSelectionActivity.h"
#include "activities/util/ConfirmationActivity.h"
#include "components/UITheme.h" #include "components/UITheme.h"
#include "fontIds.h" #include "fontIds.h"
#include "network/HttpDownloader.h" #include "network/HttpDownloader.h"
@@ -106,6 +107,7 @@ bool FontDownloadActivity::fetchAndParseManifest() {
baseUrl_ = doc["baseUrl"] | ""; baseUrl_ = doc["baseUrl"] | "";
families_.clear(); families_.clear();
fontInstaller_.refreshRegistry();
JsonArray familiesArr = doc["families"].as<JsonArray>(); JsonArray familiesArr = doc["families"].as<JsonArray>();
families_.reserve(familiesArr.size()); families_.reserve(familiesArr.size());
@@ -171,7 +173,7 @@ bool FontDownloadActivity::fetchAndParseManifest() {
void FontDownloadActivity::downloadAll() { void FontDownloadActivity::downloadAll() {
for (size_t i = 0; i < families_.size(); i++) { 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]); downloadFamily(families_[i]);
if (state_ == ERROR) return; 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<int>(families_.size()) + specialRowCount();
}
size_t FontDownloadActivity::totalDownloadSize() const {
size_t total = 0; size_t total = 0;
for (const auto& f : families_) { 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; return total;
} }
@@ -297,6 +348,7 @@ void FontDownloadActivity::downloadFamily(ManifestFamily& family) {
fontInstaller_.refreshRegistry(); fontInstaller_.refreshRegistry();
family.installed = true; family.installed = true;
family.hasUpdate = false;
{ {
RenderLock lock(*this); 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<int>(families_.size())) {
return;
}
std::string heading = tr(STR_DELETE);
const auto& family = families_[pendingDeleteFamilyIndex];
std::string body = family.name;
startActivityForResult(std::make_unique<ConfirmationActivity>(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 --- // --- Input handling ---
void FontDownloadActivity::loop() { void FontDownloadActivity::loop() {
@@ -338,12 +431,17 @@ void FontDownloadActivity::loop() {
if (mappedInput.wasPressed(MappedInputManager::Button::Confirm)) { if (mappedInput.wasPressed(MappedInputManager::Button::Confirm)) {
if (!families_.empty()) { if (!families_.empty()) {
if (isDownloadAllSelected()) { if (isDownloadAllRow(selectedIndex_)) {
downloadAll(); downloadAll();
} else if (isUpdateAllRow(selectedIndex_)) {
updateAll();
} else { } else {
const auto& family = families_[familyIndexFromList(selectedIndex_)]; auto& family = families_[familyIndexFromList(selectedIndex_)];
if (!family.installed || family.hasUpdate) { if (!family.installed || family.hasUpdate) {
downloadFamily(families_[familyIndexFromList(selectedIndex_)]); downloadFamily(family);
} else {
promptDeleteSelectedFamily();
return;
} }
} }
requestUpdateAndWait(); requestUpdateAndWait();
@@ -403,7 +501,7 @@ void FontDownloadActivity::render(RenderLock&&) {
renderer.clearScreen(); 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 lineHeight = renderer.getLineHeight(UI_10_FONT_ID);
const auto contentTop = metrics.topPadding + metrics.headerHeight + metrics.verticalSpacing; 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}, Rect{0, contentTop, pageWidth, pageHeight - contentTop - metrics.buttonHintsHeight - metrics.verticalSpacing},
listItemCount(), selectedIndex_, listItemCount(), selectedIndex_,
[this](int index) -> std::string { [this](int index) -> std::string {
if (index == 0) { if (isDownloadAllRow(index)) {
return std::string(tr(STR_DOWNLOAD_ALL)) + " (" + formatSize(totalUninstalledSize()) + ")"; 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; return families_[familyIndexFromList(index)].name;
}, },
[this](int index) -> std::string { [this](int index) -> std::string {
if (index == 0) return ""; if (isDownloadAllRow(index) || isUpdateAllRow(index)) return "";
return families_[familyIndexFromList(index)].description; return families_[familyIndexFromList(index)].description;
}, },
nullptr, nullptr,
[this](int index) -> std::string { [this](int index) -> std::string {
if (index == 0) return ""; if (isDownloadAllRow(index) || isUpdateAllRow(index)) return "";
const auto& f = families_[familyIndexFromList(index)]; const auto& f = families_[familyIndexFromList(index)];
if (f.hasUpdate) return tr(STR_UPDATE_AVAILABLE); if (f.hasUpdate) return tr(STR_UPDATE_AVAILABLE);
if (f.installed) return tr(STR_INSTALLED); if (f.installed) return tr(STR_INSTALLED);
@@ -441,12 +542,16 @@ void FontDownloadActivity::render(RenderLock&&) {
}, },
true, true,
[this](int index) -> bool { [this](int index) -> bool {
if (index == 0) return false; if (isDownloadAllRow(index) || isUpdateAllRow(index)) return false;
const auto& f = families_[familyIndexFromList(index)]; const auto& f = families_[familyIndexFromList(index)];
return f.installed && !f.hasUpdate; 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); GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
} }
} else if (state_ == DOWNLOADING) { } else if (state_ == DOWNLOADING) {
@@ -466,10 +571,6 @@ void FontDownloadActivity::render(RenderLock&&) {
renderer, renderer,
Rect{metrics.contentSidePadding, barY, pageWidth - metrics.contentSidePadding * 2, metrics.progressBarHeight}, Rect{metrics.contentSidePadding, barY, pageWidth - metrics.contentSidePadding * 2, metrics.progressBarHeight},
static_cast<int>(progress * 100), 100); static_cast<int>(progress * 100), 100);
int percentY = barY + metrics.progressBarHeight + metrics.verticalSpacing;
renderer.drawCenteredText(UI_10_FONT_ID, percentY,
(std::to_string(static_cast<int>(progress * 100)) + "%").c_str());
} else if (state_ == COMPLETE) { } else if (state_ == COMPLETE) {
renderer.drawCenteredText(UI_10_FONT_ID, centerY, tr(STR_FONT_INSTALLED), true, EpdFontFamily::BOLD); renderer.drawCenteredText(UI_10_FONT_ID, centerY, tr(STR_FONT_INSTALLED), true, EpdFontFamily::BOLD);
const auto labels = mappedInput.mapLabels(tr(STR_BACK), "", "", ""); const auto labels = mappedInput.mapLabels(tr(STR_BACK), "", "", "");
+13 -4
View File
@@ -84,10 +84,19 @@ class FontDownloadActivity : public Activity {
bool fetchAndParseManifest(); bool fetchAndParseManifest();
void downloadFamily(ManifestFamily& family); void downloadFamily(ManifestFamily& family);
void downloadAll(); void downloadAll();
void updateAll();
static bool computeFileCrc32(const char* path, uint32_t& outCrc); static bool computeFileCrc32(const char* path, uint32_t& outCrc);
bool isDownloadAllSelected() const { return selectedIndex_ == 0 && !families_.empty(); } bool showDownloadAllRow() const;
int familyIndexFromList(int listIndex) const { return listIndex - 1; } bool showUpdateAllRow() const;
int listItemCount() const { return families_.empty() ? 0 : static_cast<int>(families_.size()) + 1; } int specialRowCount() const;
size_t totalUninstalledSize() 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); static std::string formatSize(size_t bytes);
}; };
@@ -116,8 +116,8 @@ void OtaUpdateActivity::render(RenderLock&&) {
static_cast<int>(updaterProgress * 100), 100); static_cast<int>(updaterProgress * 100), 100);
y += metrics.progressBarHeight + metrics.verticalSpacing; y += metrics.progressBarHeight + metrics.verticalSpacing;
renderer.drawCenteredText(UI_10_FONT_ID, y, // Percent label is drawn by BaseTheme::drawProgressBar; this slot is left intentionally empty
(std::to_string(static_cast<int>(updaterProgress * 100)) + "%").c_str()); // so the bytes line below stays at the same Y it was at when the activity drew its own percent.
y += height + metrics.verticalSpacing; y += height + metrics.verticalSpacing;
renderer.drawCenteredText( renderer.drawCenteredText(
UI_10_FONT_ID, y, UI_10_FONT_ID, y,
@@ -230,7 +230,8 @@ void SdFirmwareUpdateActivity::render(RenderLock&&) {
Rect{metrics.contentSidePadding, y, pageWidth - metrics.contentSidePadding * 2, metrics.progressBarHeight}, Rect{metrics.contentSidePadding, y, pageWidth - metrics.contentSidePadding * 2, metrics.progressBarHeight},
static_cast<int>(pct), 100); static_cast<int>(pct), 100);
y += metrics.progressBarHeight + metrics.verticalSpacing; 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; y += lineHeight + metrics.verticalSpacing;
renderer.drawCenteredText(UI_10_FONT_ID, y, tr(STR_FIRMWARE_UPDATE_DO_NOT_POWER_OFF)); renderer.drawCenteredText(UI_10_FONT_ID, y, tr(STR_FIRMWARE_UPDATE_DO_NOT_POWER_OFF));
} else if (state == State::SUCCESS) { } else if (state == State::SUCCESS) {
+2 -2
View File
@@ -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_CHECK_UPDATES, SettingAction::CheckForUpdates));
systemSettings.push_back(SettingInfo::Action(StrId::STR_SD_FIRMWARE_UPDATE, SettingAction::SdFirmwareUpdate)); systemSettings.push_back(SettingInfo::Action(StrId::STR_SD_FIRMWARE_UPDATE, SettingAction::SdFirmwareUpdate));
systemSettings.push_back(SettingInfo::Action(StrId::STR_LANGUAGE, SettingAction::Language)); 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, 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)); readerSettings.push_back(SettingInfo::Action(StrId::STR_CUSTOMISE_STATUS_BAR, SettingAction::CustomiseStatusBar));
// Update currentSettings pointer and count for the active category // Update currentSettings pointer and count for the active category
+10 -3
View File
@@ -97,7 +97,12 @@ void BmpViewerActivity::onEnter() {
} }
// 4. Prepare Rendering // 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<int>(siblingImages.size()) - 1);
const auto labels =
mappedInput.mapLabels(tr(STR_BACK), tr(STR_SET_SLEEP_COVER), (hasPrevious ? "<" : ""), (hasNext ? ">" : ""));
GUI.fillPopupProgress(renderer, popupRect, 50); GUI.fillPopupProgress(renderer, popupRect, 50);
@@ -185,7 +190,8 @@ void BmpViewerActivity::loop() {
return; return;
} }
if (mappedInput.wasReleased(MappedInputManager::Button::Up)) { if (mappedInput.wasReleased(MappedInputManager::Button::Left) ||
mappedInput.wasReleased(MappedInputManager::Button::Up)) {
if (siblingImages.size() > 1 && currentImageIndex > 0) { if (siblingImages.size() > 1 && currentImageIndex > 0) {
currentImageIndex--; currentImageIndex--;
std::string dirPath = FsHelpers::extractFolderPath(filePath); std::string dirPath = FsHelpers::extractFolderPath(filePath);
@@ -196,7 +202,8 @@ void BmpViewerActivity::loop() {
return; return;
} }
if (mappedInput.wasReleased(MappedInputManager::Button::Down)) { if (mappedInput.wasReleased(MappedInputManager::Button::Right) ||
mappedInput.wasReleased(MappedInputManager::Button::Down)) {
if (siblingImages.size() > 1 && currentImageIndex != -1 && if (siblingImages.size() > 1 && currentImageIndex != -1 &&
currentImageIndex < static_cast<int>(siblingImages.size()) - 1) { currentImageIndex < static_cast<int>(siblingImages.size()) - 1) {
currentImageIndex++; currentImageIndex++;
@@ -5,7 +5,6 @@
#include <I18n.h> #include <I18n.h>
#include <algorithm> #include <algorithm>
#include <cctype>
#include <string> #include <string>
#include <vector> #include <vector>
@@ -44,18 +43,6 @@ void drawScrollBar(const GfxRenderer& renderer, Rect rect, int itemCount, int pa
renderer.fillRect(barX, thumbY, barW, thumbH); 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<unsigned char>(label[0]))) {
label.erase(0, 1);
}
// Trim any extra left spaces.
while (!label.empty() && label[0] == ' ') {
label.erase(0, 1);
}
return label;
}
} // namespace } // namespace
int coverWidth = 0; int coverWidth = 0;
@@ -412,11 +399,11 @@ void RoundedRaffTheme::drawButtonHints(GfxRenderer& renderer, const char* btn1,
const bool backDisabled = (btn1 == nullptr || btn1[0] == '\0'); const bool backDisabled = (btn1 == nullptr || btn1[0] == '\0');
const int leftGroupX = sidePadding; const int leftGroupX = sidePadding;
const int rightGroupX = leftGroupX + groupWidth + groupGap; 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. // 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 selectText = (btn2 && btn2[0] != '\0') ? std::string(btn2) : "";
const std::string upText = (btn3 && btn3[0] != '\0') ? sanitizeButtonLabel(std::string(btn3)) : ""; const std::string upText = (btn3 && btn3[0] != '\0') ? std::string(btn3) : "";
const std::string downText = (btn4 && btn4[0] != '\0') ? sanitizeButtonLabel(std::string(btn4)) : ""; 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. // Ensure button hints always "win" visually even if other elements accidentally render into this area.
renderer.fillRect(leftGroupX, hintY, groupWidth, hintHeight, false); renderer.fillRect(leftGroupX, hintY, groupWidth, hintHeight, false);
@@ -16,7 +16,6 @@ Requirements:
import argparse import argparse
import re import re
from collections import Counter from collections import Counter
import pyphen
from pathlib import Path from pathlib import Path
import zipfile import zipfile
@@ -75,6 +74,8 @@ def generate_hyphenation_data(
min_prefix: Minimum characters allowed before the first hyphen (default: 2) min_prefix: Minimum characters allowed before the first hyphen (default: 2)
min_suffix: Minimum characters allowed after the last hyphen (default: 2) min_suffix: Minimum characters allowed after the last hyphen (default: 2)
""" """
import pyphen
print(f"Reading from: {input_file}") print(f"Reading from: {input_file}")
# Read the input file # Read the input file