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