Merge remote-tracking branch 'upstream/master' into release/1.3.0
This commit is contained in:
@@ -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."
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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.
|
||||
@@ -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
@@ -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
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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
@@ -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});
|
||||
},
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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,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),
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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: "Выявы"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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: "Суреттер"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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: "Автоперелистывание: "
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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)"
|
||||
|
||||
@@ -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-карти та виберіть його"
|
||||
|
||||
+1
-1
Submodule open-x4-sdk updated: 7d86603ad2...a64a3c29be
+12
-6
@@ -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()
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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), "", "", "");
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user