Compare commits

..
26 Commits
Author SHA1 Message Date
Zach Nelson 8959836e46 Restore lost block-quote on WARNING
Compile Release / build-release (push) Canceled after 0s
2026-05-15 14:30:38 -05:00
Chun Ming Lee fd79074d43 fix: handle fallbacks for advance table and prewarm (#1929)
## Summary
- Fixes #1928 by having the prewarm and advance table functions resolve
fallback styles
---

### AI Usage
Did you use AI tools to help write this code?  YES - Codex

---------

Co-authored-by: Uri Tauber <uritaube@gmail.com>
# Conflicts:
#	lib/EpdFont/SdCardFont.cpp
2026-05-15 11:44:20 -05:00
Zach Nelson b971f7bda4 correct broken internal anchor 2026-05-15 11:29:46 -05:00
Justin Mitchell c26e410c58 fix: Add documentation for USB-locked Xteink devices (#1990)
Document the Xteink Unlocker tool requirement for third-party purchased
xteink units that ship with USB flashing locked. Include warnings about
bricking risks when flashing unsupported firmwares (e.g. Papyrix) on
locked devices, as they may permanently lock the device with no recovery
path.
# Conflicts:
#	README.md
2026-05-15 11:00:41 -05:00
Zach Nelson 16e5e5f00b Update flasher links for X3 support 2026-05-15 10:31:52 -05:00
Zach Nelson 74e75746df Update to @Uri-Tauber's README.md 2026-05-15 10:17:54 -05:00
Zach Nelson a7586f20a1 fix: Prepare SD card font caches from txt reader (#1973)
## Summary

SD card font fixes:
- `TxtReaderActivity` needs to call `renderer.ensureSdCardFontReady` to
build the advance lookup table to support rendering with SD card fonts.
This revealed that `TxtReaderActivity` was inconsistently performing
layout with `getTextWidth`, when the renderer actually uses
`getTextAdvanceX`, which can lead to minor inconsistencies in alignment.
- Avoid allocating one big `allText` string in
`ParsedText::layoutAndExtractLines`. Instead, pass the vector of word
strings directly to `SdCardFont::buildAdvanceTable`, where the algorithm
just needs to iterate codepoints anyway.

---

### AI Usage

While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.

Did you use AI tools to help write this code? _**PARTIALLY**_

---------

Co-authored-by: Justin Mitchell <justin@jmitch.com>
# Conflicts:
#	lib/EpdFont/SdCardFont.cpp
2026-05-15 09:56:21 -05:00
Zach Nelson b145e4437c Merge remote-tracking branch 'upstream/master' into release/1.3.0 2026-05-12 12:37:43 -05:00
Danila Yudin 3c34a8310e docs: fix KOReader sync guide link (#1930)
## Summary

* Fix the README link to the KOReader Sync quick setup section in the
user guide.
* Update the fragment from the old 3.6.5 anchor to the current 3.6.7
anchor.

## Additional Context

The README pointed to `#365-koreader-sync-quick-setup`, but
`USER_GUIDE.md` defines this section as `3.6.7 KOReader Sync Quick
Setup`, with the matching anchor `#367-koreader-sync-quick-setup`.

Verified with `rg` that the README now points to the existing heading.
PlatformIO checks were not run because this is a Markdown-only link fix
and `pio` is not installed in this environment.
2026-05-12 12:23:50 -05:00
WuTofu bc6e090aa8 feat: separate into "Download All" and "Update All" in font manager (#1955)
## Summary

* **What is the goal of this PR?**  
Separate the font manager's combined `Download / Update All` action into
separate `Download All` and `Update All` rows

* **What changes are included?**  
- Updated multiple i18n translation files to add `STR_UPDATE_ALL` and
adjust `STR_DOWNLOAD_ALL` text.
- Added separate handlers: `downloadAll()` for fonts that have been
installed and `updateAll()` for fonts with updates.
  - `Download All` and `Update All` are only shown when applicable.

---

### AI Usage

While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.

Did you use AI tools to help write this code? _**PARTIALLY**_
2026-05-12 12:17:01 -05:00
Jan Ivanov 8d1b86a893 feat: add next / prev labels to bmp viewer (#1852)
## Summary

Bind `prev` / `next` functionality to the left and right buttons in BMP
Viewer and adds labels

<img width="718" height="953" alt="image"
src="https://github.com/user-attachments/assets/c6dac14e-14f5-4cbf-9298-772cfc479f33"
/>

## Additional Context

* Add any other information that might be helpful for the reviewer
(e.g., performance implications, potential risks,
  specific areas to focus on).

---

### AI Usage

While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.

Did you use AI tools to help write this code? _**NO**_
2026-05-12 15:06:35 +03:00
Zach Nelson 63d5094f2e Revert "feat: closest-pt size selection instead of ordinal slot" (#1949)
Reverts crosspoint-reader/crosspoint-reader#1912

This was meant to be more robust with partial SD card fonts, but causes
trouble for folks using custom font sizes. We need a better approach to
decouple numeric font sizes from S/M/L/XL settings.
2026-05-11 12:44:52 -05:00
Danila Yudin 24977048c3 docs: fix hyphenation updater script name (#1931)
## Summary

* Rename the hyphenation update script to match the documented
`update_hyphenation.sh` name.
* Update the hyphenation trie documentation command example to use the
renamed script.

## Additional Context

The prose in `docs/hyphenation-trie-format.md` referred to
`update_hyphenation.sh`, but the command example and file on disk used
`update_hypenation.sh`. This made the documented script name
inconsistent with the runnable command.

Verified with `rg` that no references to the misspelled script name
remain. Also ran `bash -n scripts/update_hyphenation.sh` to syntax-check
the renamed script without downloading trie files.
2026-05-11 11:23:05 -05:00
Danila Yudin 63e92ec74e fix: make script help paths lightweight (#1937)
## Summary

Consolidate the script help/startup fixes so standalone helper commands
can show usage without requiring runtime-only dependencies or generating
files.

## Details

Several helper scripts imported optional packages, evaluated newer
annotations, or started generation before users could inspect CLI usage.
On a fresh checkout, this made common help paths fail on missing
packages such as `cairosvg`, `Pillow`, `freetype-py`, `fontTools`,
`pyphen`, `pyserial`, or on Python 3.9 annotation evaluation. A few
generators also treated `--help` as a normal output argument and wrote
files instead of printing usage.

This change keeps runtime dependencies on the code paths that need them,
handles lightweight help/list commands first, and adds explicit `--help`
handling for generator scripts that previously started work.

## Validation

- `python3 <script> --help` across tracked Python scripts, excluding
`scripts/patch_jpegdec.py` because it is a PlatformIO pre-build hook
rather than a standalone CLI
- `python3 scripts/convert_icon.py` still exits with usage for missing
required arguments
- `python3 lib/EpdFont/scripts/fontconvert_sdcard.py --list-presets`
- `python3 -m py_compile` for the changed scripts
2026-05-11 11:21:13 -05:00
KymAndriyandKymAndriy accd50b593 fix: Ukrainian-translation (#1946)
## Summary

Micro fix Ukrainian translation

---

### AI Usage

While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.

Did you use AI tools to help write this code? _**NO**_

Co-authored-by: KymAndriy <test@notamail.ua>
2026-05-11 10:29:05 -05:00
26250c0b80 fix: Ukrainian translation (#1939)
## Summary

Update Ukrainian translation


### AI Usage

Did you use AI tools to help write this code? _** PARTIALLY **_

---------

Co-authored-by: Kym_Adnriy <kym_andr@gmail.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2026-05-11 09:53:22 -05:00
KemoNine 99ac1c5c89 fix: sd font download urls in docs (#1945)
## Summary

SD font binaries are published in their own repo, this updates the docs
to have the proper informatoin.

---

### AI Usage

While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.

Did you use AI tools to help write this code? **Yes, claude**
2026-05-11 10:21:13 -04:00
KemoNine 90874dae9f fix: sd font folder paths in documentation (#1944)
## Summary

The paths in the sd font documentation were never updated to match the
code's use of `/fonts` and `/.fonts`. This fixes the paths in the docs.

### AI Usage

While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.

Did you use AI tools to help write this code? **Yes, Claude**
2026-05-11 09:49:32 -04:00
WuTofu a0037ced2b feat: add font family deletion functionality (#1919)
## Summary

* **What is the goal of this PR?**

1. Adds font family deletion support to the font download activity so
users can remove installed font families directly from the download
list.
2. Address an issue where a font family manually deleted in the file
browser by user will show as "update" in the `FontDownloadActivity`
 

* **What changes are included?**
* Updated button hint label to show `Delete` when deletion is available.
  * Added confirmation before deleting a selected installed font family.
* Refreshed font registry in `fetchAndParseManifest` (for the second
goal above)

---

### AI Usage

While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.

Did you use AI tools to help write this code? _**PARTIALLY**_
2026-05-11 15:45:38 +02:00
Uri Tauber 20fee843c7 fix: Missing navigation button labels in Roundedraff theme (#1905) 2026-05-10 21:15:45 +03:00
Uri Tauber 181ed6c488 fix: gracefully resolve fonts missing variants (#1921) 2026-05-10 21:11:11 +03:00
Jack R 3ac1ab13a0 fix: distribute justifyExtra to non-breaking space tokens (#1783) 2026-05-10 21:10:51 +03:00
KemoNine dd06e71b66 fix: remove percent rendering from activities (#1901) 2026-05-10 21:10:23 +03:00
Vincent Politzer 4e5e2fe40a feat: focus reading (#1670)
## Summary

This PR introduces **Focus Reading**, a generic implementation of
artificial fixation points (similar to Bionic Reading) designed to
improve reading speed and focus by bolding the initial characters of
words. This is achieved by dynamically bolding characters during
indexing.

<img width="500" alt="Focus Reading on X3"
src="https://github.com/user-attachments/assets/94a632a5-82da-47be-957c-538b35bf84d9"
/>

### Implementation Details
#### Core Text Engine (`ParsedText`)
- Modified `ParsedText::addWord` to implement a custom bolding
algorithm. It uses a 45% ratio for bolding, with a minimum of 1
character and a maximum of 9.
- UTF-8 Safety: Integrated `utf8NextCodepoint` to ensure character
counting and string slicing occur at safe byte boundaries, preventing
corruption of multi-byte characters (e.g., accented letters or smart
quotes).
- Intelligent Tokenization: This correctly identifies and separates
"word" characters (letters, apostrophes, hyphens) from "non-word"
characters (numbers, brackets, smart quotes).
- Formatting Preservation: The logic ensures that punctuation is not
"stolen" for the bolding count and that existing styles (like italics or
underlines) are preserved across the bold/regular split.
- Processing at indexing stage reduces CPU load at render-time and
ensures layout/fit is unaffected.
- Split details are tracked with `wordIsFocusSuffix`. After splitting
and layout, suffixes are merged back into their preceding word entries
to prevent a doubling of RAM usage.

#### Settings and UI
- Version Management: Bumped `SECTION_FILE_VERSION` to `21`
- Global Settings: Added `focusReadingEnabled` to `CrossPointSettings`.
- User Interface: Added a new toggle in the "Reader" section of the
settings menu, positioned after the "Embedded Style" option.
- Localization: Added the `STR_FOCUS_READING` string

#### Plumbing
- Plumbed the `focusReadingEnabled` boolean through
`EpubReaderActivity`, `Section`, and `ChapterHtmlSlimParser` to ensure
the user's setting reaches the `ParsedText` constructor during chapter
indexing.

## Additional Context

### Files Changed
- `lib/Epub/Epub/ParsedText.h / .cpp`: Core fixation logic and UTF-8
tokenization.
- `lib/Epub/Epub/Section.h / .cpp`: Cache header updates and
invalidation logic.
- `lib/Epub/Epub/parsers/ChapterHtmlSlimParser.h / .cpp`: Plumbing the
setting to text blocks.
- `src/CrossPointSettings.h`: Data persistence for the new setting.
- `src/SettingsList.h`: UI toggle implementation.
- `src/activities/reader/EpubReaderActivity.cpp`: Handling settings
changes during reading sessions.
- `lib/I18n/translations/*.yaml`: UI strings.

---

### AI Usage

While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.

Did you use AI tools to help write this code? _**YES**_
2026-05-10 20:02:14 +02:00
Zach Nelson 2ff63884d6 fix: Restore performance in fontconvert_sdcard.py (#1924)
## Summary

#1910 caused a massive performance degradation in the way rasterized
glyph buffers were handled during pixel iteration. This change restores
the original performance characteristics so the font generation job
finishes in a reasonable amount of time.

---

### AI Usage

While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.

Did you use AI tools to help write this code? _**YES**_
2026-05-10 12:01:02 -05:00
Zach Nelson 74b8cac928 chore: Add verbose mode to build-sd-fonts.py (#1923)
## Summary

One of the recent font conversion script changes obliterated
performance. Add a verbose mode to build-sd-fonts.py to help diagnose
problems. Changed the CI process to use verbose mode.

---

### AI Usage

While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.

Did you use AI tools to help write this code? _**YES**_
2026-05-10 11:55:57 -05:00
72 changed files with 1172 additions and 432 deletions
+3 -3
View File
@@ -40,7 +40,7 @@ jobs:
echo "metadata=$(python3 -c 'from cpfont_version import FONTS_MANIFEST_VERSION; print(FONTS_MANIFEST_VERSION)')" >> "$GITHUB_OUTPUT" echo "metadata=$(python3 -c 'from cpfont_version import FONTS_MANIFEST_VERSION; print(FONTS_MANIFEST_VERSION)')" >> "$GITHUB_OUTPUT"
- name: Build SD card fonts - name: Build SD card fonts
run: python3 lib/EpdFont/scripts/build-sd-fonts.py --clean run: python3 lib/EpdFont/scripts/build-sd-fonts.py --clean --verbose -j 1
- name: Flatten output for release assets - name: Flatten output for release assets
run: | run: |
@@ -83,7 +83,7 @@ jobs:
--title "$TITLE" \ --title "$TITLE" \
--notes "Pre-built \`.cpfont\` font files for CrossPoint Reader. --notes "Pre-built \`.cpfont\` font files for CrossPoint Reader.
Download individual files or use **Settings > System > Download Fonts** on the device. Download individual files or use **Settings > System > Manage Fonts** on the device.
See [SD Card Fonts documentation](https://github.com/${{ github.repository }}/blob/main/docs/sd-card-fonts.md) for details." See [SD Card Fonts documentation](https://github.com/${{ github.repository }}/blob/main/docs/sd-card-fonts.md) for details."
@@ -103,4 +103,4 @@ jobs:
This is revision **${{ steps.tags.outputs.revision }}** — see [\`${{ steps.tags.outputs.versioned }}\`](https://github.com/${{ env.FONTS_REPO }}/releases/tag/${{ steps.tags.outputs.versioned }}) for the immutable copy. This is revision **${{ steps.tags.outputs.revision }}** — see [\`${{ steps.tags.outputs.versioned }}\`](https://github.com/${{ env.FONTS_REPO }}/releases/tag/${{ steps.tags.outputs.versioned }}) for the immutable copy.
Download individual files or use **Settings > System > Download Fonts** on the device." Download individual files or use **Settings > System > Manage fonts** on the device."
+166 -108
View File
@@ -1,122 +1,169 @@
# CrossPoint Reader # CrossPoint Reader
Firmware for the **Xteink X4** e-paper display reader (unaffiliated with Xteink). CrossPoint is open-source e-reader firmware - community-built, fully hackable, free forever. It's maintained by a growing community of developers and readers who believe your device should do what you want - not what a manufacturer decided for you.
Built using **PlatformIO** and targeting the **ESP32-C3** microcontroller.
CrossPoint Reader is a purpose-built firmware designed to be a drop-in, fully open-source replacement for the official **Now running on:** ESP32C3-based Xteink [X4](https://www.xteink.com/products/xteink-x4) and [X3](https://www.xteink.com/products/xteink-x3).
Xteink firmware. It aims to match or improve upon the standard EPUB reading experience.
![](./docs/images/cover.jpg) ![CrossPoint Reader running on Xteink device](./docs/images/cover.jpg)
## Motivation ## What can CrossPoint do?
E-paper devices are fantastic for reading, but most commercially available readers are closed systems with limited - **Reader engine**: EPUB 2/3 rendering with embedded-style option, image handling, hyphenation, kerning, chapter navigation, footnotes, go-to-percent, auto page turn, orientation control, focus reading, KOReader progress sync and more.
customisation. The **Xteink X4** is an affordable, e-paper device, however the official firmware remains closed.
CrossPoint exists partly as a fun side-project and partly to open up the ecosystem and truly unlock the device's
potential.
CrossPoint Reader aims to: - **Various formats**: native handling for `.epub`, `.xtc/.xtch`, `.txt`, and `.bmp`.
* Provide a **fully open-source alternative** to the official firmware.
* Offer a **document reader** capable of handling EPUB content on constrained hardware.
* Support **customisable font, layout, and display** options.
* Run purely on the **Xteink X4 hardware**.
This project is **not affiliated with Xteink**; it's built as a community project. - **Screenshots.**
## Features & Usage - **Custom fonts**: install your favorite fonts on the SD card.
- [x] EPUB parsing and rendering (EPUB 2 and EPUB 3) - **Tilt page turn (X3 only)**.
- [x] Image support within EPUB
- [x] Saved reading position
- [x] File explorer with file picker
- [x] Basic EPUB picker from root directory
- [x] Support nested folders
- [ ] EPUB picker with cover art
- [x] Custom sleep screen
- [x] Cover sleep screen
- [x] Wifi book upload
- [x] Wifi OTA updates
- [x] KOReader Sync integration for cross-device reading progress
- [x] Configurable font, layout, and display options
- [ ] User provided fonts
- [ ] Full UTF support
- [x] Screen rotation
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). - **Library workflow**: folder browser, hidden-file toggle, long-press delete, recent books, SD-cache management.
See [the user guide](./USER_GUIDE.md) for instructions on operating CrossPoint, including the - **Wireless workflows**:
[KOReader Sync quick setup](./USER_GUIDE.md#365-koreader-sync-quick-setup).
- File transfer web UI
- EPUB Optimizer
- Web settings UI/API (edit many device settings from browser)
- WebSocket fast uploads
- WebDAV handler
- AP mode (hotspot) and STA mode (join existing WiFi), both with QR helpers
- Calibre wireless connect flow
- OPDS browser with saved servers (up to 8), search, pagination, and direct download
- OTA update checks and installs from GitHub releases
For more details about the scope of the project, see the [SCOPE.md](SCOPE.md) document. - **Customization**: multiple themes (Classic, Lyra, Lyra Extended, RoundedRaff), sleep screen modes, front/side button remapping, status bar controls, power-button behavior, refresh cadence, and more.
## Installing - **Localization**: 22 UI languages and counting.
### Web (latest firmware) ### Coming soon:
1. Connect your Xteink X4 to your computer via USB-C and wake/unlock the device - RTL support — Arabic, Hebrew, and Farsi.
2. Go to https://xteink.dve.al/ and click "Flash CrossPoint firmware"
To revert back to the official firmware, you can flash the latest official firmware from https://xteink.dve.al/, or swap - Bookmarks.
back to the other partition using the "Swap boot partition" button here https://xteink.dve.al/debug.
### Web (specific firmware version) - Dictionary lookup — inline word lookup without leaving the reader.
1. Connect your Xteink X4 to your computer via USB-C - More themes.
2. Download the `firmware.bin` file from the release of your choice via the [releases page](https://github.com/crosspoint-reader/crosspoint-reader/releases)
3. Go to https://xteink.dve.al/ and flash the firmware file using the "OTA fast flash controls" section
To revert back to the official firmware, you can flash the latest official firmware from https://xteink.dve.al/, or swap - Much more! stay tuned.
back to the other partition using the "Swap boot partition" button here https://xteink.dve.al/debug.
### Command line (specific firmware version) ---
## USB-locked devices (Xteink Unlocker)
Some Xteink units purchased from third-party stores (e.g. AliExpress) ship with USB flashing locked from the factory.
If your device is locked, you will need to use the **Xteink Unlocker** tool available at
https://crosspointreader.com/#unlock-tool before you can flash CrossPoint.
**You do not need this tool if you bought your device directly from xteink.com.** Those units are not locked.
**Not sure if your device is locked?** Power it on, connect the USB-C cable, and try flashing via the web flasher first (see
[Install firmware](#install-firmware) below). If the browser's serial device picker does not show your device, try a different
USB port or browser before assuming the device is locked. Only reach for the unlocker if the device still doesn't appear.
> ### ⚠️ WARNING: READ THIS BEFORE USING THE UNLOCKER ⚠️
>
> **The only officially supported firmwares in the unlock tool are CrossPoint and CrossInk.**
>
> Flashing any other firmware on a USB-locked device may **permanently brick the device** or leave it **permanently
> stuck on that firmware with no recovery path**. Once USB flashing is re-locked, your only way back is via OTA, and if
> the firmware you flashed doesn't support OTA, **there is no way out**.
>
> **The Papyrix fork has removed OTA update support from its code.** If you flash Papyrix onto a
> USB-locked unit, you will have **zero update or recovery path** and will be stuck on it forever. **Do not flash
> Papyrix (or any other unsupported firmware) on a locked device.**
## Install firmware
### Web installer (recommended)
1. Connect your device to your computer via USB-C and wake/unlock the device
2. Go to https://crosspointreader.com/#flash-tools, select device (X3 or X4), and choose an official CrossPoint release.
### Web installer (specific version)
1. Connect your device to your computer via USB-C and wake/unlock the device
2. Download a `firmware.bin` from [Releases](https://github.com/crosspoint-reader/crosspoint-reader/releases), local build, or continuous integration artifact.
3. Go to https://crosspointreader.com/#flash-tools, select device (X3 or X4), click "Custom .bin" and upload a `firmware.bin`.
### Revert to Official Firmware
To revert to the official firmware, you can also flash the latest official firmware using https://crosspointreader.com/#flash-tools.
### Command line
1. Install [`esptool`](https://github.com/espressif/esptool):
1. Install [`esptool`](https://github.com/espressif/esptool) :
```bash ```bash
pip install esptool pip install esptool
``` ```
2. Download the `firmware.bin` file from the release of your choice via the [releases page](https://github.com/crosspoint-reader/crosspoint-reader/releases)
3. Connect your Xteink X4 to your computer via USB-C. 2. Download `firmware.bin` from the [releases page](https://github.com/crosspoint-reader/crosspoint-reader/releases).
4. Note the device location. On Linux, run `dmesg` after connecting. On MacOS, run : 3. Connect your device via USB-C.
4. Find the device port. On Linux, run `dmesg` after connecting. On macOS:
```bash ```bash
log stream --predicate 'subsystem == "com.apple.iokit"' --info log stream --predicate 'subsystem == "com.apple.iokit"' --info
``` ```
5. Flash the firmware :
5. Flash:
```bash ```bash
esptool.py --chip esp32c3 --port /dev/ttyACM0 --baud 921600 write_flash 0x10000 /path/to/firmware.bin esptool.py --chip esp32c3 --port /dev/ttyACM0 --baud 921600 write_flash 0x10000 /path/to/firmware.bin
``` ```
Change `/dev/ttyACM0` to the device for your system.
Adjust `/dev/ttyACM0` to match your system.
### Manual ### Manual
See [Development](#development) below. See [Development quick start](#development-quick-start) below.
## Development ---
## Documentation
- [User Guide](./USER_GUIDE.md)
- [Web server usage](./docs/webserver.md)
- [Web server endpoints](./docs/webserver-endpoints.md)
- [Project scope](./SCOPE.md)
- [Contributing docs](./docs/contributing/README.md)
---
## Development quick start
### Prerequisites ### Prerequisites
* **PlatformIO Core** (`pio`) or **VS Code + PlatformIO IDE** - [pioarduino](https://github.com/pioarduino/pioarduino) or VS Code + pioarduino plugin
* Python 3.8+ - Python 3.8+
* USB-C cable for flashing the ESP32-C3 - `clang-format` 21
* Xteink X4 - USB-C cable supporting data transfer
### Checking out the code ### Setup
CrossPoint uses PlatformIO for building and flashing the firmware. To get started, clone the repository: ```bash
```
git clone --recursive https://github.com/crosspoint-reader/crosspoint-reader git clone --recursive https://github.com/crosspoint-reader/crosspoint-reader
cd crosspoint-reader
# Or, if you've already cloned without --recursive: # if cloned without --recursive:
git submodule update --init --recursive git submodule update --init --recursive
``` ```
### Flashing your device ### Build / flash / monitor
Connect your Xteink X4 to your computer via USB-C and run the following command. ```bash
```sh
pio run --target upload pio run --target upload
``` ```
### Contributor pre-PR checks
```bash
./bin/clang-format-fix
pio check -e default
pio run -e default
```
### Debugging ### Debugging
After flashing the new features, its recommended to capture detailed logs from the serial port. After flashing the new features, its recommended to capture detailed logs from the serial port.
@@ -126,7 +173,9 @@ First, make sure all required Python packages are installed:
```python ```python
python3 -m pip install pyserial colorama matplotlib python3 -m pip install pyserial colorama matplotlib
``` ```
after that run the script:
After that run the script:
```sh ```sh
# For Linux # For Linux
# This was tested on Debian and should work on most Linux systems. # This was tested on Debian and should work on most Linux systems.
@@ -135,63 +184,72 @@ python3 scripts/debugging_monitor.py
# For macOS # For macOS
python3 scripts/debugging_monitor.py /dev/cu.usbmodem2101 python3 scripts/debugging_monitor.py /dev/cu.usbmodem2101
``` ```
Minor adjustments may be required for Windows. Minor adjustments may be required for Windows.
---
## Internals ## Internals
CrossPoint Reader is pretty aggressive about caching data down to the SD card to minimise RAM usage. The ESP32-C3 only CrossPoint Reader is pretty aggressive about caching data down to the SD card to minimise RAM usage. The ESP32-C3 only has ~380KB of usable RAM, so we have to be careful. A lot of the decisions made in the design of the firmware were based on this constraint.
has ~380KB of usable RAM, so we have to be careful. A lot of the decisions made in the design of the firmware were based
on this constraint.
### Data caching ### Data caching
The first time chapters of a book are loaded, they are cached to the SD card. Subsequent loads are served from the The first time chapters of a book are loaded, they are cached to the SD card. Subsequent loads are served from the
cache. This cache directory exists at `.crosspoint` on the SD card. The structure is as follows: cache. This cache directory exists at `.crosspoint` on the SD card. The structure is as follows:
```text
```
.crosspoint/ .crosspoint/
├── epub_12471232/ # Each EPUB is cached to a subdirectory named `epub_<hash>` ├── epub_<hash>/ # one directory per book, named by content hash
│ ├── progress.bin # Stores reading progress (chapter, page, etc.) │ ├── progress.bin # reading position (chapter, page, etc.)
│ ├── cover.bmp # Book cover image (once generated) │ ├── cover.bmp # generated cover image
│ ├── book.bin # Book metadata (title, author, spine, table of contents, etc.) │ ├── book.bin # metadata: title, author, spine, TOC
│ └── sections/ # All chapter data is stored in the sections subdirectory │ └── sections/ # per-chapter layout cache
│ ├── 0.bin # Chapter data (screen count, all text layout info, etc.) │ ├── 0.bin
│ ├── 1.bin # files are named by their index in the spine │ ├── 1.bin
│ └── ... │ └── ...
└── epub_189013891/
``` ```
Deleting the `.crosspoint` directory will clear the entire cache. Removing `/.crosspoint` clears all cached metadata and forces a full regeneration on next open. Note: the cache isn't cleared automatically when you delete a book, and moving a file to a new path resets its reading progress.
Due the way it's currently implemented, the cache is not automatically cleared when a book is deleted and moving a book
file will use a new cache directory, resetting the reading progress.
For more details on the internal file structures, see the [file formats document](./docs/file-formats.md). For more details on the internal file structures, see the [file formats document](./docs/file-formats.md).
---
## Contributing ## Contributing
Contributions are very welcome! Contributions are welcome. If you're new to the codebase, start with the [contributing docs](./docs/contributing/README.md). For things to work on, check the [ideas discussion board](https://github.com/crosspoint-reader/crosspoint-reader/discussions/categories/ideas) — leave a comment before starting so we don't duplicate effort.
If you are new to the codebase, start with the [contributing docs](./docs/contributing/README.md). Everyone here is a volunteer, so please be respectful and patient. For governance and community expectations, see [GOVERNANCE.md](./GOVERNANCE.md).
If you're looking for a way to help out, take a look at the [ideas discussion board](https://github.com/crosspoint-reader/crosspoint-reader/discussions/categories/ideas).
If there's something there you'd like to work on, leave a comment so that we can avoid duplicated effort.
Everyone here is a volunteer, so please be respectful and patient. For more details on our governance and community
principles, please see [GOVERNANCE.md](GOVERNANCE.md).
### To submit a contribution:
1. Fork the repo
2. Create a branch (`feature/dithering-improvement`)
3. Make changes
4. Submit a PR
--- ---
CrossPoint Reader is **not affiliated with Xteink or any manufacturer of the X4 hardware**. ## Community forks
Huge shoutout to [**diy-esp32-epub-reader** by atomic14](https://github.com/atomic14/diy-esp32-epub-reader), which was a project I took a lot of inspiration from as I One of the best things about open source is that anyone can take the code in a different direction. If you need something outside CrossPoint's [scope](./SCOPE.md), check out the community forks:
was making CrossPoint.
- [CrossInk](https://github.com/uxjulia/CrossInk) — Typography and reading tracking: Bionic Reading (bolds word stems to create fixation points), guide dots between words, improved paragraph indents, and replaces the default fonts with ChareInk/Lexend/Bitter.
- [papyrix-reader](https://github.com/bigbag/papyrix-reader) — Adds FB2 and MD format support. Actively maintained with Arabic script support. Custom themes via SD card.
- [crosspet](https://github.com/trilwu/crosspet) — A Vietnamese fork that adds a Tamagotchi-style virtual chicken that grows based on your reading milestones (pages read, streaks, care). Also: Flashcards, Weather, Pomodoro timer, and mini-games.
- [crosspoint-reader (jpirnay)](https://github.com/jpirnay/crosspoint-reader) — Faster integration of functionality. Tracks upstream PRs and integrates the good ones ahead of the official merge.
- [crosspoint-reader-cjk](https://github.com/aBER0724/crosspoint-reader-cjk) — Purpose-built for Chinese, Japanese, and Korean reading.
- [inx](https://github.com/obijuankenobiii/inx) — Completely reimagines the user interface with tabbed navigation.
- ~~[PlusPoint](https://github.com/ngxson/pluspoint-reader) — custom JS apps support.~~ (Unmaintained)
- [crosspoint-reader-papers3](https://github.com/juicecultus/crosspoint-reader-papers3) — Crosspoint port for M5Stack Paper S3.
**Note:** Many of these features will make their way into CrossPoint over time. We maintain a slower pace to ensure rock-solid stability and squash bugs before they reach your device.
Want to build your own device? Be sure to check out the [de-link](https://github.com/iandchasse/de-link) project.
---
CrossPoint Reader is **not affiliated with Xteink or any device manufacturer**.
Huge shoutout to [diy-esp32-epub-reader](https://github.com/atomic14/diy-esp32-epub-reader), which inspired this project.
+33
View File
@@ -0,0 +1,33 @@
# Focus Reading
Focus Reading is a reading aid that bolds the first portion of each word, guiding your eyes to natural fixation points and helping you read faster with less effort. Some readers — particularly those with ADHD — find it helps them stay engaged with the text and reduces mind-wandering. It is inspired by the Bionic Reading technique.
<img src="./images/focus-reading/focus-reading.jpg" height="500" alt="Comparison of the same page with and without Focus Reading enabled" />
*Left: Focus Reading off. Right: Focus Reading on. Both using Literata.*
## Enabling Focus Reading
1. Open **Settings > Reader**
2. Toggle **Focus Reading** on
Toggling the setting will trigger a re-index of your current book, the same as when changing font settings. Once indexing is complete, page turns proceed as normal. No changes are made to your EPUB files.
## Examples
<img src="./images/focus-reading/focus-reading-notoserif.jpg" height="500" alt="Focus Reading with Noto Serif font" />
*Focus Reading with Noto Serif font*
<img src="./images/focus-reading/focus-reading-merriweather.jpg" height="500" alt="Focus Reading with Merriweather font" />
*Focus Reading with Merriweather font*
<img src="./images/focus-reading/focus-reading-atkinson.jpg" height="500" alt="Focus Reading with Atkinson Hyperlegible Next font" />
*Focus Reading with Atkinson Hyperlegible Next font*
## Notes
- Focus Reading only applies to regular body text. Already-bold text (headings, emphasis) is left unchanged.
- The setting is per-device, not per-book — it applies to all books while enabled.
+1 -1
View File
@@ -49,5 +49,5 @@ A convenient script `update_hyphenation.sh` is used to update all languages.
To use it, run: To use it, run:
```sh ```sh
./scripts/update_hypenation.sh ./scripts/update_hyphenation.sh
``` ```
Binary file not shown.

After

Width:  |  Height:  |  Size: 216 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 211 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 207 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 260 KiB

+25 -15
View File
@@ -10,7 +10,7 @@ There are three ways to install fonts:
### Option 1: Download from device (recommended) ### Option 1: Download from device (recommended)
1. Connect your CrossPoint reader to WiFi 1. Connect your CrossPoint reader to WiFi
2. Go to **Settings > System > Download Fonts** 2. Go to **Settings > System > Manage Fonts**
3. Browse available font families and tap to download 3. Browse available font families and tap to download
4. Downloaded fonts appear immediately in **Settings > Reader > Font Family** 4. Downloaded fonts appear immediately in **Settings > Reader > Font Family**
@@ -24,28 +24,38 @@ There are three ways to install fonts:
### Option 3: Manual SD card copy ### Option 3: Manual SD card copy
1. Download font files from the 1. Download font files from the
[Releases page](https://github.com/crosspoint-reader/crosspoint-reader/releases/tag/sd-fonts) [crosspoint-fonts repository](https://github.com/crosspoint-reader/crosspoint-fonts)
2. Copy font family folders to `/.crosspoint/fonts/` on your SD card: 2. Copy font family folders to one of two locations on your SD card:
- `/.fonts/` — hidden directory (preferred; keeps the SD root tidy
when mounted on a desktop)
- `/fonts/` — visible directory (use this if your OS hides dot-files
and you'd rather see the folder in your file manager)
Both roots are always scanned at boot and the results are merged: a
family installed in `/fonts/` shows up even when `/.fonts/` also
exists, and vice versa. The two roots only collide if the same family
name appears in both — in that case the copy in `/.fonts/` wins and
the duplicate in `/fonts/` is ignored.
SD Card Root/ SD Card Root/
── .crosspoint/ ── .fonts/ ← Hidden root (preferred)
└── fonts/ └── Literata/
├── Bookerly-SD/ ├── Literata_12.cpfont
├── Bookerly-SD_12.cpfont ├── Literata_14.cpfont
├── Bookerly-SD_14.cpfont ├── Literata_16.cpfont
│ ├── Bookerly-SD_16.cpfont └── Literata_18.cpfont
│ └── Bookerly-SD_18.cpfont └── fonts/ ← Visible root (equally valid)
└── Merriweather/
├── Merriweather_12.cpfont
└── ... └── ...
3. Insert the SD card and power on your CrossPoint reader 3. Insert the SD card and power on your CrossPoint reader
## Available Pre-Built Fonts ## Available Pre-Built Fonts
| Font | Best For | Languages | The current list of pre-built fonts is maintained in the
|------|----------|-----------| [crosspoint-fonts repository](https://github.com/crosspoint-reader/crosspoint-fonts).
| Bookerly-SD | General reading | English, Western European |
| NotoSansExtended | Multi-script reading | European, Greek, Cyrillic, Georgian, Armenian, Ethiopic |
| NotoSansCJK | Chinese/Japanese/Korean | CJK + ASCII |
## Converting Custom Fonts ## Converting Custom Fonts
+121 -67
View File
@@ -9,16 +9,20 @@
#include <cstring> #include <cstring>
#include <memory> #include <memory>
#include "EpdFontFamily.h"
static_assert(sizeof(EpdGlyph) == 16, "EpdGlyph must be 16 bytes to match .cpfont file layout"); static_assert(sizeof(EpdGlyph) == 16, "EpdGlyph must be 16 bytes to match .cpfont file layout");
static_assert(sizeof(EpdUnicodeInterval) == 12, "EpdUnicodeInterval must be 12 bytes to match .cpfont file layout"); static_assert(sizeof(EpdUnicodeInterval) == 12, "EpdUnicodeInterval must be 12 bytes to match .cpfont file layout");
static_assert(sizeof(EpdKernClassEntry) == 3, "EpdKernClassEntry must be 3 bytes to match .cpfont file layout"); static_assert(sizeof(EpdKernClassEntry) == 3, "EpdKernClassEntry must be 3 bytes to match .cpfont file layout");
static_assert(sizeof(EpdLigaturePair) == 8, "EpdLigaturePair must be 8 bytes to match .cpfont file layout"); static_assert(sizeof(EpdLigaturePair) == 8, "EpdLigaturePair must be 8 bytes to match .cpfont file layout");
// FNV-1a hash for content-based font ID generation namespace {
static constexpr uint32_t FNV_OFFSET = 2166136261u;
static constexpr uint32_t FNV_PRIME = 16777619u;
static uint32_t fnv1a(const uint8_t* data, size_t len, uint32_t hash = FNV_OFFSET) { // FNV-1a hash for content-based font ID generation
constexpr uint32_t FNV_OFFSET = 2166136261u;
constexpr uint32_t FNV_PRIME = 16777619u;
uint32_t fnv1a(const uint8_t* data, size_t len, uint32_t hash = FNV_OFFSET) {
for (size_t i = 0; i < len; i++) { for (size_t i = 0; i < len; i++) {
hash ^= data[i]; hash ^= data[i];
hash *= FNV_PRIME; hash *= FNV_PRIME;
@@ -27,16 +31,44 @@ static uint32_t fnv1a(const uint8_t* data, size_t len, uint32_t hash = FNV_OFFSE
} }
// .cpfont magic bytes // .cpfont magic bytes
static constexpr char CPFONT_MAGIC[8] = {'C', 'P', 'F', 'O', 'N', 'T', '\0', '\0'}; constexpr char CPFONT_MAGIC[8] = {'C', 'P', 'F', 'O', 'N', 'T', '\0', '\0'};
// CPFONT_VERSION is defined as a #define in SdCardFont.h so it can be // CPFONT_VERSION is defined as a #define in SdCardFont.h so it can be
// stringified into FONT_MANIFEST_URL. // stringified into FONT_MANIFEST_URL.
static constexpr uint32_t HEADER_SIZE = 32; constexpr uint32_t HEADER_SIZE = 32;
static constexpr uint32_t STYLE_TOC_ENTRY_SIZE = 32; constexpr uint32_t STYLE_TOC_ENTRY_SIZE = 32;
// Helper to read little-endian values from byte buffer // Helper to read little-endian values from byte buffer
static inline uint16_t readU16(const uint8_t* p) { return p[0] | (p[1] << 8); } inline uint16_t readU16(const uint8_t* p) { return p[0] | (p[1] << 8); }
static inline int16_t readI16(const uint8_t* p) { return static_cast<int16_t>(p[0] | (p[1] << 8)); } inline int16_t readI16(const uint8_t* p) { return static_cast<int16_t>(p[0] | (p[1] << 8)); }
static inline uint32_t readU32(const uint8_t* p) { return p[0] | (p[1] << 8) | (p[2] << 16) | (p[3] << 24); } inline uint32_t readU32(const uint8_t* p) { return p[0] | (p[1] << 8) | (p[2] << 16) | (p[3] << 24); }
// Walks a null-terminated UTF-8 string and appends each unique codepoint to
// codepoints[0..cpCount-1] via O(n²) dedup. Returns true if the buffer
// reached maxCount (cap hit), false if all codepoints fit.
bool collectUniqueCodepoints(const char* text, uint32_t* codepoints, uint32_t& cpCount, uint32_t maxCount) {
const unsigned char* p = reinterpret_cast<const unsigned char*>(text);
while (*p) {
uint32_t cp = utf8NextCodepoint(&p);
if (cp == 0) break;
bool found = false;
for (uint32_t i = 0; i < cpCount; i++) {
if (codepoints[i] == cp) {
found = true;
break;
}
}
if (!found) {
if (cpCount >= maxCount) return true;
codepoints[cpCount++] = cp;
}
}
return false;
}
const char* asCStr(const std::string& s) { return s.c_str(); }
const char* asCStr(const char* s) { return s; }
} // namespace
SdCardFont::~SdCardFont() { freeAll(); } SdCardFont::~SdCardFont() { freeAll(); }
@@ -587,6 +619,8 @@ int32_t SdCardFont::findGlobalGlyphIndex(const PerStyle& s, uint32_t codepoint)
int SdCardFont::prewarm(const char* utf8Text, uint8_t styleMask, bool metadataOnly) { int SdCardFont::prewarm(const char* utf8Text, uint8_t styleMask, bool metadataOnly) {
if (!loaded_) return -1; if (!loaded_) return -1;
styleMask = resolveStyleMask(styleMask);
if (styleMask == 0) return 0;
unsigned long startMs = millis(); unsigned long startMs = millis();
@@ -1015,62 +1049,10 @@ uint16_t SdCardFont::getAdvance(uint32_t codepoint, uint8_t style) const {
return 0; return 0;
} }
int SdCardFont::buildAdvanceTable(const char* utf8Text, uint8_t styleMask) { // Given a sorted array of unique codepoints, resolve glyph indices per style,
if (!loaded_) return -1; // batch-read advanceX from SD, and merge into the persistent advance table.
// Caller owns the codepoints buffer.
// Note: advance table is preserved across calls. We only fetch codepoints int SdCardFont::fetchAdvancesForCodepoints(uint32_t* codepoints, uint32_t cpCount, uint8_t styleMask) {
// not already present, then merge them in. Use clearPersistentCache() to
// wipe the table when the font/size/family changes.
unsigned long startMs = millis();
// Step 1: Extract unique codepoints, capped at MAX_UNIQUE_CODEPOINTS.
// The dedup buffer is sized to the cap, not total chars — a large EPUB section
// may contain 50K+ characters but real text has far fewer unique codepoints.
// 4096 × 4 bytes = 16KB temporary; bounded regardless of input size.
static constexpr uint32_t MAX_UNIQUE_CODEPOINTS = 4096;
uint32_t* codepoints = new (std::nothrow) uint32_t[MAX_UNIQUE_CODEPOINTS];
if (!codepoints) {
LOG_ERR("SDCF", "buildAdvanceTable: failed to allocate codepoint buffer (%u bytes)", MAX_UNIQUE_CODEPOINTS * 4);
return -1;
}
uint32_t cpCount = 0;
bool hitCap = false;
// Second pass: collect unique codepoints via O(n²) dedup.
// Bounded by uniqueCount × totalChars comparisons. For 2000 unique from 2291 total,
// worst case ~4.6M comparisons of uint32_t — ~30ms on 160MHz RISC-V, acceptable
// for one-time section indexing.
const unsigned char* p = reinterpret_cast<const unsigned char*>(utf8Text);
while (*p) {
uint32_t cp = utf8NextCodepoint(&p);
if (cp == 0) break;
bool found = false;
for (uint32_t i = 0; i < cpCount; i++) {
if (codepoints[i] == cp) {
found = true;
break;
}
}
if (!found) {
if (cpCount >= MAX_UNIQUE_CODEPOINTS) {
hitCap = true;
break;
}
codepoints[cpCount++] = cp;
}
}
if (hitCap) {
LOG_ERR("SDCF", "buildAdvanceTable: unique codepoint cap (%u) hit, layout may be approximate",
MAX_UNIQUE_CODEPOINTS);
}
// Sort for ordered glyph index mapping and final table output
std::sort(codepoints, codepoints + cpCount);
// Step 2: For each requested style, fetch any codepoints not yet cached and
// merge them into the persistent advance table.
int totalMissed = 0; int totalMissed = 0;
for (uint8_t si = 0; si < MAX_STYLES; si++) { for (uint8_t si = 0; si < MAX_STYLES; si++) {
if (!(styleMask & (1 << si)) || !styles_[si].present) continue; if (!(styleMask & (1 << si)) || !styles_[si].present) continue;
@@ -1165,12 +1147,55 @@ int SdCardFont::buildAdvanceTable(const char* utf8Text, uint8_t styleMask) {
ADVANCE_CACHE_LIMIT); ADVANCE_CACHE_LIMIT);
} }
delete[] codepoints; return totalMissed;
}
template <typename Iter>
int SdCardFont::buildAdvanceTableRange(Iter begin, Iter end, bool includeSpace, bool includeHyphen, uint8_t styleMask) {
if (!loaded_) return -1;
styleMask = resolveStyleMask(styleMask);
if (styleMask == 0) return 0;
unsigned long startMs = millis();
// +2 reserved slots for space and hyphen injected after the main scan.
static constexpr uint32_t MAX_UNIQUE_CODEPOINTS = 4096;
uint32_t* codepoints = new (std::nothrow) uint32_t[MAX_UNIQUE_CODEPOINTS + 2];
if (!codepoints) {
LOG_ERR("SDCF", "buildAdvanceTable: failed to allocate codepoint buffer (%u bytes)", MAX_UNIQUE_CODEPOINTS * 4);
return -1;
}
uint32_t cpCount = 0;
bool hitCap = false;
for (auto it = begin; it != end && !hitCap; ++it) {
hitCap = collectUniqueCodepoints(asCStr(*it), codepoints, cpCount, MAX_UNIQUE_CODEPOINTS);
}
if (includeSpace && std::none_of(codepoints, codepoints + cpCount, [](uint32_t c) { return c == ' '; }))
codepoints[cpCount++] = ' ';
if (includeHyphen && std::none_of(codepoints, codepoints + cpCount, [](uint32_t c) { return c == '-'; }))
codepoints[cpCount++] = '-';
if (hitCap) {
LOG_ERR("SDCF", "buildAdvanceTable: unique codepoint cap (%u) hit, layout may be approximate",
MAX_UNIQUE_CODEPOINTS);
}
std::sort(codepoints, codepoints + cpCount);
int totalMissed = fetchAdvancesForCodepoints(codepoints, cpCount, styleMask);
delete[] codepoints;
stats_.prewarmTotalMs = millis() - startMs; stats_.prewarmTotalMs = millis() - startMs;
return totalMissed; return totalMissed;
} }
int SdCardFont::buildAdvanceTable(const char* utf8Text, uint8_t styleMask) {
return buildAdvanceTableRange(&utf8Text, &utf8Text + 1, false, false, styleMask);
}
int SdCardFont::buildAdvanceTable(const std::vector<std::string>& words, bool includeHyphen, uint8_t styleMask) {
return buildAdvanceTableRange(words.begin(), words.end(), words.size() > 1, includeHyphen, styleMask);
}
// --- Stats --- // --- Stats ---
void SdCardFont::logStats(const char* label) { void SdCardFont::logStats(const char* label) {
@@ -1190,6 +1215,35 @@ EpdFont* SdCardFont::getEpdFont(uint8_t style) {
bool SdCardFont::hasStyle(uint8_t style) const { return styles_[style & (MAX_STYLES - 1)].present; } bool SdCardFont::hasStyle(uint8_t style) const { return styles_[style & (MAX_STYLES - 1)].present; }
uint8_t SdCardFont::resolveStyle(uint8_t style) const {
static const uint8_t kFallbacks[MAX_STYLES][MAX_STYLES] = {
// REGULAR: REGULAR -> BOLD -> ITALIC -> BOLD_ITALIC
{EpdFontFamily::REGULAR, EpdFontFamily::BOLD, EpdFontFamily::ITALIC, EpdFontFamily::BOLD_ITALIC},
// BOLD: BOLD -> REGULAR -> BOLD_ITALIC -> ITALIC
{EpdFontFamily::BOLD, EpdFontFamily::REGULAR, EpdFontFamily::BOLD_ITALIC, EpdFontFamily::ITALIC},
// ITALIC: ITALIC -> REGULAR -> BOLD_ITALIC -> BOLD
{EpdFontFamily::ITALIC, EpdFontFamily::REGULAR, EpdFontFamily::BOLD_ITALIC, EpdFontFamily::BOLD},
// BOLD_ITALIC: BOLD_ITALIC -> BOLD -> ITALIC -> REGULAR
{EpdFontFamily::BOLD_ITALIC, EpdFontFamily::BOLD, EpdFontFamily::ITALIC, EpdFontFamily::REGULAR},
};
const uint8_t styleBits = style & (MAX_STYLES - 1);
for (uint8_t candidate : kFallbacks[styleBits]) {
if (styles_[candidate].present) return candidate;
}
return EpdFontFamily::REGULAR;
}
uint8_t SdCardFont::resolveStyleMask(uint8_t styleMask) const {
uint8_t resolvedMask = 0;
for (uint8_t si = 0; si < MAX_STYLES; si++) {
if (styleMask & (1 << si)) {
resolvedMask |= static_cast<uint8_t>(1u << resolveStyle(si));
}
}
return resolvedMask;
}
// --- On-demand glyph loading (overflow buffer) --- // --- On-demand glyph loading (overflow buffer) ---
const EpdGlyph* SdCardFont::onGlyphMiss(void* ctx, uint32_t codepoint) { const EpdGlyph* SdCardFont::onGlyphMiss(void* ctx, uint32_t codepoint) {
+14 -1
View File
@@ -1,6 +1,8 @@
#pragma once #pragma once
#include <cstdint> #include <cstdint>
#include <string>
#include <vector>
#include "EpdFont.h" #include "EpdFont.h"
#include "EpdFontData.h" #include "EpdFontData.h"
@@ -43,10 +45,11 @@ class SdCardFont {
int prewarm(const char* utf8Text, uint8_t styleMask = 0x0F, bool metadataOnly = false); int prewarm(const char* utf8Text, uint8_t styleMask = 0x0F, bool metadataOnly = false);
// Build a compact advance-only table for layout measurement. // Build a compact advance-only table for layout measurement.
// Extracts ALL unique codepoints from utf8Text (no MAX_PAGE_GLYPHS cap), // Extracts ALL unique codepoints from words (no MAX_PAGE_GLYPHS cap),
// batch-reads advanceX from SD, stores in a sorted per-style table. // batch-reads advanceX from SD, stores in a sorted per-style table.
// Returns number of codepoints not found in font coverage. // Returns number of codepoints not found in font coverage.
int buildAdvanceTable(const char* utf8Text, uint8_t styleMask = 0x0F); int buildAdvanceTable(const char* utf8Text, uint8_t styleMask = 0x0F);
int buildAdvanceTable(const std::vector<std::string>& words, bool includeHyphen, uint8_t styleMask = 0x0F);
// Look up advanceX for a codepoint from the advance table. // Look up advanceX for a codepoint from the advance table.
// Returns the 12.4 fixed-point advance, or 0 if not found. // Returns the 12.4 fixed-point advance, or 0 if not found.
@@ -71,6 +74,13 @@ class SdCardFont {
// Returns true if the given style is present in this font file. // Returns true if the given style is present in this font file.
bool hasStyle(uint8_t style) const; bool hasStyle(uint8_t style) const;
// Resolve requested style bits to the closest present style.
uint8_t resolveStyle(uint8_t style) const;
// Resolve every requested style bit through fallback and return the actual
// styles that need cache/advance preparation.
uint8_t resolveStyleMask(uint8_t styleMask) const;
// Number of styles present in this font file. // Number of styles present in this font file.
uint8_t styleCount() const { return styleCount_; } uint8_t styleCount() const { return styleCount_; }
@@ -229,6 +239,9 @@ class SdCardFont {
void applyKernLigaturePointers(PerStyle& s, EpdFontData& data) const; void applyKernLigaturePointers(PerStyle& s, EpdFontData& data) const;
void applyGlyphMissCallback(uint8_t styleIdx); void applyGlyphMissCallback(uint8_t styleIdx);
int32_t findGlobalGlyphIndex(const PerStyle& s, uint32_t codepoint) const; int32_t findGlobalGlyphIndex(const PerStyle& s, uint32_t codepoint) const;
int fetchAdvancesForCodepoints(uint32_t* codepoints, uint32_t cpCount, uint8_t styleMask);
template <typename Iter>
int buildAdvanceTableRange(Iter begin, Iter end, bool includeSpace, bool includeHyphen, uint8_t styleMask);
int prewarmStyle(uint8_t styleIdx, const uint32_t* codepoints, uint32_t cpCount, bool metadataOnly); int prewarmStyle(uint8_t styleIdx, const uint32_t* codepoints, uint32_t cpCount, bool metadataOnly);
// Global helpers // Global helpers
+12 -8
View File
@@ -28,21 +28,25 @@ int SdCardFontManager::computeFontId(uint32_t contentHash, const char* familyNam
return id != 0 ? id : 1; // 0 is reserved as "not found" sentinel return id != 0 ? id : 1; // 0 is reserved as "not found" sentinel
} }
bool SdCardFontManager::loadFamily(const SdCardFontFamilyInfo& family, GfxRenderer& renderer, uint8_t targetPtSize) { bool SdCardFontManager::loadFamily(const SdCardFontFamilyInfo& family, GfxRenderer& renderer, uint8_t fontSizeEnum) {
// Unload any previously loaded family first // Unload any previously loaded family first
if (!loadedFamilyName_.empty()) { if (!loadedFamilyName_.empty()) {
unloadAll(renderer); unloadAll(renderer);
} }
// Pick the single file whose size is closest to targetPtSize. Loading // Select by ordinal position: sort available sizes, then map the font size
// only one size bounds resident memory (intervals + kern/ligature tables // enum (SMALL=0 .. EXTRA_LARGE=3) to the corresponding slot. When the
// per style) to one file's worth, vs. N_sizes × per-file overhead. // family has fewer sizes than 4, clamp to the last available size.
const SdCardFontFileInfo* selected = family.pickClosestSize(targetPtSize); auto sizes = family.availableSizes();
if (!selected) { if (sizes.empty()) {
LOG_ERR("SDMGR", "Family %s has no files to load", family.name.c_str()); LOG_ERR("SDMGR", "Family %s has no files to load", family.name.c_str());
return false; return false;
} }
uint8_t idx = fontSizeEnum;
if (idx >= sizes.size()) idx = sizes.size() - 1;
const SdCardFontFileInfo* selected = family.findFile(sizes[idx]);
auto* font = new (std::nothrow) SdCardFont(); auto* font = new (std::nothrow) SdCardFont();
if (!font) { if (!font) {
LOG_ERR("SDMGR", "Failed to allocate SdCardFont for %s", selected->path.c_str()); LOG_ERR("SDMGR", "Failed to allocate SdCardFont for %s", selected->path.c_str());
@@ -66,8 +70,8 @@ bool SdCardFontManager::loadFamily(const SdCardFontFamilyInfo& family, GfxRender
renderer.registerSdCardFont(fontId, font); renderer.registerSdCardFont(fontId, font);
loaded_.push_back({font, fontId, selected->pointSize}); loaded_.push_back({font, fontId, selected->pointSize});
LOG_DBG("SDMGR", "Loaded %s size=%u id=%d styles=%u (target=%u)", selected->path.c_str(), selected->pointSize, fontId, LOG_DBG("SDMGR", "Loaded %s size=%u id=%d styles=%u (sizeEnum=%u)", selected->path.c_str(), selected->pointSize,
font->styleCount(), targetPtSize); fontId, font->styleCount(), fontSizeEnum);
EpdFontFamily fontFamily(font->getEpdFont(0), font->getEpdFont(1), font->getEpdFont(2), font->getEpdFont(3)); EpdFontFamily fontFamily(font->getEpdFont(0), font->getEpdFont(1), font->getEpdFont(2), font->getEpdFont(3));
renderer.insertFont(fontId, fontFamily); renderer.insertFont(fontId, fontFamily);
+5 -9
View File
@@ -15,16 +15,12 @@ class SdCardFontManager {
SdCardFontManager(const SdCardFontManager&) = delete; SdCardFontManager(const SdCardFontManager&) = delete;
SdCardFontManager& operator=(const SdCardFontManager&) = delete; SdCardFontManager& operator=(const SdCardFontManager&) = delete;
// Load the single size whose pointSize is closest to targetPtSize. Only one // Load the font file matching fontSizeEnum (SMALL=0 .. EXTRA_LARGE=3) by
// .cpfont file is loaded; other sizes remain on disk. This keeps resident // ordinal position in the family's sorted size list. Only one .cpfont file
// interval + kern/ligature tables to one size's worth of memory. // is loaded; other sizes remain on disk. This keeps resident interval +
// // kern/ligature tables to one size's worth of memory.
// Closest-pt selection is robust against families that don't ship the
// canonical {12,14,16,18}: a family with only [10,14,18] still resolves
// any reasonable target, where ordinal slot-mapping by SMALL..EXTRA_LARGE
// would mis-select.
// Returns true on success. // Returns true on success.
bool loadFamily(const SdCardFontFamilyInfo& family, GfxRenderer& renderer, uint8_t targetPtSize); bool loadFamily(const SdCardFontFamilyInfo& family, GfxRenderer& renderer, uint8_t fontSizeEnum);
// Unload everything, unregister from renderer. // Unload everything, unregister from renderer.
void unloadAll(GfxRenderer& renderer); void unloadAll(GfxRenderer& renderer);
-17
View File
@@ -4,8 +4,6 @@
#include <Logging.h> #include <Logging.h>
#include <algorithm> #include <algorithm>
#include <climits>
#include <cstdlib>
#include <cstring> #include <cstring>
// --- SdCardFontFamilyInfo helpers --- // --- SdCardFontFamilyInfo helpers ---
@@ -40,21 +38,6 @@ std::vector<uint8_t> SdCardFontFamilyInfo::availableSizes() const {
return sizes; return sizes;
} }
const SdCardFontFileInfo* SdCardFontFamilyInfo::pickClosestSize(uint8_t targetPtSize) const {
const SdCardFontFileInfo* selected = nullptr;
int bestDiff = INT_MAX;
for (const auto& f : files) {
int diff = std::abs(static_cast<int>(f.pointSize) - static_cast<int>(targetPtSize));
// Strict < ensures the first scan wins on ties; then tie-break by smaller
// pointSize to make the choice independent of filesystem enumeration order.
if (diff < bestDiff || (diff == bestDiff && selected && f.pointSize < selected->pointSize)) {
bestDiff = diff;
selected = &f;
}
}
return selected;
}
// --- SdCardFontRegistry --- // --- SdCardFontRegistry ---
bool SdCardFontRegistry::parseFilename(const char* filename, uint8_t& size, uint8_t& style) { bool SdCardFontRegistry::parseFilename(const char* filename, uint8_t& size, uint8_t& style) {
-9
View File
@@ -20,15 +20,6 @@ struct SdCardFontFamilyInfo {
const SdCardFontFileInfo* findFile(uint8_t size, uint8_t style = 0) const; const SdCardFontFileInfo* findFile(uint8_t size, uint8_t style = 0) const;
bool hasSize(uint8_t size) const; bool hasSize(uint8_t size) const;
std::vector<uint8_t> availableSizes() const; std::vector<uint8_t> availableSizes() const;
// Pick the file whose pointSize is closest to targetPtSize. On ties (equal
// distance) prefers the smaller pointSize so behaviour is deterministic
// across SD card layouts. Returns nullptr when files is empty.
//
// Robust against families that don't ship the canonical {12,14,16,18} set:
// a family with only [10,14,18] resolves target 12 → 10 (or 14 on tie),
// target 16 → 14 or 18, etc., instead of mis-indexing by ordinal slot.
const SdCardFontFileInfo* pickClosestSize(uint8_t targetPtSize) const;
}; };
class SdCardFontRegistry { class SdCardFontRegistry {
+75 -14
View File
@@ -17,6 +17,12 @@ Usage:
# Generate only specific families # Generate only specific families
python3 build-sd-fonts.py --only Literata,IBMPlexMono python3 build-sd-fonts.py --only Literata,IBMPlexMono
# Stream child process output for debugging
python3 build-sd-fonts.py --verbose
# Override the per-family timeout (default: 600s)
python3 build-sd-fonts.py --timeout 1200
""" """
import argparse import argparse
@@ -25,6 +31,8 @@ import shutil
import subprocess import subprocess
import sys import sys
import tempfile import tempfile
import threading
import time
import urllib.request import urllib.request
from concurrent.futures import ProcessPoolExecutor, as_completed from concurrent.futures import ProcessPoolExecutor, as_completed
from pathlib import Path from pathlib import Path
@@ -141,7 +149,16 @@ def resolve_font_path(style_spec: dict, family_name: str, style_name: str) -> Pa
return resolved return resolved
def build_family(family: dict, output_base: Path) -> tuple[str, bool, str]: def _stream_pipe(pipe, prefix: str, dest: list[str]):
"""Read lines from a pipe, print with prefix, and accumulate into dest."""
for line in pipe:
dest.append(line)
print(f" [{prefix}] {line}", end="", flush=True)
def build_family(
family: dict, output_base: Path, verbose: bool = False, timeout: int = 600
) -> tuple[str, bool, str]:
"""Build a single font family. Returns (name, success, message).""" """Build a single font family. Returns (name, success, message)."""
name = family["name"] name = family["name"]
output_dir = output_base / name output_dir = output_base / name
@@ -185,18 +202,52 @@ def build_family(family: dict, output_base: Path) -> tuple[str, bool, str]:
cmd.append("--force-autohint") cmd.append("--force-autohint")
# Run fontconvert_sdcard.py # Run fontconvert_sdcard.py
start = time.monotonic()
try: try:
result = subprocess.run( if verbose:
cmd, proc = subprocess.Popen(
capture_output=True, cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True
text=True, )
timeout=600, stdout_lines: list[str] = []
) stderr_lines: list[str] = []
if result.returncode != 0: t_out = threading.Thread(
return name, False, result.stderr.strip() or f"Exit code {result.returncode}" target=_stream_pipe, args=(proc.stdout, name, stdout_lines)
return name, True, "" )
except subprocess.TimeoutExpired: t_err = threading.Thread(
return name, False, "Timed out after 600s" target=_stream_pipe, args=(proc.stderr, f"{name}/err", stderr_lines)
)
t_out.start()
t_err.start()
try:
proc.wait(timeout=timeout)
except subprocess.TimeoutExpired:
proc.kill()
proc.wait()
elapsed = time.monotonic() - start
return name, False, f"Timed out after {elapsed:.0f}s"
finally:
t_out.join()
t_err.join()
if proc.returncode != 0:
err = "".join(stderr_lines).strip()
return name, False, err or f"Exit code {proc.returncode}"
return name, True, ""
else:
result = subprocess.run(
cmd, capture_output=True, text=True, timeout=timeout,
)
if result.returncode != 0:
return name, False, result.stderr.strip() or f"Exit code {result.returncode}"
return name, True, ""
except subprocess.TimeoutExpired as e:
elapsed = time.monotonic() - start
tail = ""
captured = getattr(e, "stderr", None) or getattr(e, "stdout", None)
if captured:
lines = captured.strip().splitlines()
tail = "\n Last output:\n" + "\n".join(f" | {l}" for l in lines[-20:])
return name, False, f"Timed out after {elapsed:.0f}s{tail}"
except Exception as e: except Exception as e:
return name, False, str(e) return name, False, str(e)
@@ -252,6 +303,14 @@ def main():
help="Max parallel jobs (default: number of families)" help="Max parallel jobs (default: number of families)"
) )
parser.add_argument("--clean", action="store_true", help="Clean output directory before building") parser.add_argument("--clean", action="store_true", help="Clean output directory before building")
parser.add_argument(
"--verbose", "-v", action="store_true",
help="Stream child process output in real time (useful for debugging timeouts)"
)
parser.add_argument(
"--timeout", type=int, default=600,
help="Per-family timeout in seconds (default: 600)"
)
args = parser.parse_args() args = parser.parse_args()
if args.manifest and not args.base_url: if args.manifest and not args.base_url:
@@ -303,12 +362,14 @@ def main():
# Build phase (parallel) # Build phase (parallel)
max_workers = args.jobs or len(families) max_workers = args.jobs or len(families)
print(f"\n=== Building {len(families)} families ({max_workers} parallel jobs) ===\n") verbose = args.verbose
timeout = args.timeout
print(f"\n=== Building {len(families)} families ({max_workers} parallel jobs, timeout {timeout}s) ===\n")
failed = [] failed = []
with ProcessPoolExecutor(max_workers=max_workers) as executor: with ProcessPoolExecutor(max_workers=max_workers) as executor:
futures = { futures = {
executor.submit(build_family, family, output_base): family["name"] executor.submit(build_family, family, output_base, verbose, timeout): family["name"]
for family in families for family in families
} }
for future in as_completed(futures): for future in as_completed(futures):
+3 -2
View File
@@ -1,12 +1,10 @@
#!python3 #!python3
import freetype
import zlib import zlib
import sys import sys
import re import re
import math import math
import argparse import argparse
from collections import namedtuple from collections import namedtuple
from fontTools.ttLib import TTFont
# Force UTF-8 stdout so that `python fontconvert.py … > foo.h` on Windows # Force UTF-8 stdout so that `python fontconvert.py … > foo.h` on Windows
# (default cp1252) doesn't emit UTF-16 LE / replacement chars in the generated # (default cp1252) doesn't emit UTF-16 LE / replacement chars in the generated
@@ -27,6 +25,9 @@ parser.add_argument("--force-autohint", dest="force_autohint", action="store_tru
parser.add_argument("--pnum", dest="pnum", action="store_true", help="Use proportional numerals (pnum OpenType feature) instead of default tabular figures. Reduces visual gaps between digits in running prose.") parser.add_argument("--pnum", dest="pnum", action="store_true", help="Use proportional numerals (pnum OpenType feature) instead of default tabular figures. Reduces visual gaps between digits in running prose.")
args = parser.parse_args() args = parser.parse_args()
import freetype
from fontTools.ttLib import TTFont
GlyphProps = namedtuple("GlyphProps", ["width", "height", "advance_x", "left", "top", "data_length", "data_offset", "code_point"]) GlyphProps = namedtuple("GlyphProps", ["width", "height", "advance_x", "left", "top", "data_length", "data_offset", "code_point"])
font_stack = [freetype.Face(f) for f in args.fontstack] font_stack = [freetype.Face(f) for f in args.fontstack]
+19 -7
View File
@@ -22,7 +22,8 @@ Usage:
""" """
import freetype from __future__ import annotations
import struct import struct
import sys import sys
import os import os
@@ -31,8 +32,6 @@ import math
import argparse import argparse
from collections import namedtuple from collections import namedtuple
from fontTools.ttLib import TTFont
from cpfont_version import CPFONT_VERSION from cpfont_version import CPFONT_VERSION
# --- Unicode interval presets --- # --- Unicode interval presets ---
@@ -252,6 +251,8 @@ def extract_kerning_fonttools(font_path, codepoints, ppem):
codepoints. Values are scaled from font design units to integer codepoints. Values are scaled from font design units to integer
pixels at ppem. pixels at ppem.
""" """
from fontTools.ttLib import TTFont
font = TTFont(font_path) font = TTFont(font_path)
units_per_em = font['head'].unitsPerEm units_per_em = font['head'].unitsPerEm
cmap = font.getBestCmap() or {} cmap = font.getBestCmap() or {}
@@ -402,6 +403,8 @@ def extract_ligatures_fonttools(font_path, codepoints):
Returns list of (packed_pair, ligature_codepoint) for the given codepoints. Returns list of (packed_pair, ligature_codepoint) for the given codepoints.
Multi-character ligatures are decomposed into chained pairs. Multi-character ligatures are decomposed into chained pairs.
""" """
from fontTools.ttLib import TTFont
font = TTFont(font_path) font = TTFont(font_path)
cmap = font.getBestCmap() or {} cmap = font.getBestCmap() or {}
@@ -514,6 +517,8 @@ def extract_ligatures_fonttools(font_path, codepoints):
def rasterize_font_style(fontfile, size, intervals, style_id=0, force_autohint=False): def rasterize_font_style(fontfile, size, intervals, style_id=0, force_autohint=False):
"""Rasterize all glyphs for one font style. Returns StyleRasterData.""" """Rasterize all glyphs for one font style. Returns StyleRasterData."""
import freetype
style_names = {0: "regular", 1: "bold", 2: "italic", 3: "bolditalic"} style_names = {0: "regular", 1: "bold", 2: "italic", 3: "bolditalic"}
style_label = style_names.get(style_id, str(style_id)) style_label = style_names.get(style_id, str(style_id))
@@ -535,14 +540,16 @@ def rasterize_font_style(fontfile, size, intervals, style_id=0, force_autohint=F
return face return face
return None return None
# Validate intervals: remove codepoints not present in the font # Validate intervals: remove codepoints not present in the font.
# Only check glyph existence via get_char_index — do NOT call
# load_glyph here, as that triggers FT_LOAD_RENDER at the target
# DPI and doubles total rasterization time for no benefit.
print(f" [{style_label}] Validating intervals against font...", file=sys.stderr) print(f" [{style_label}] Validating intervals against font...", file=sys.stderr)
validated_intervals = [] validated_intervals = []
for i_start, i_end in intervals: for i_start, i_end in intervals:
start = i_start start = i_start
for code_point in range(i_start, i_end + 1): for code_point in range(i_start, i_end + 1):
f = load_glyph(code_point) if face.get_char_index(code_point) == 0:
if f is None:
if start < code_point: if start < code_point:
validated_intervals.append((start, code_point - 1)) validated_intervals.append((start, code_point - 1))
start = code_point + 1 start = code_point + 1
@@ -575,13 +582,18 @@ def rasterize_font_style(fontfile, size, intervals, style_id=0, force_autohint=F
# pitch == width and a top-down layout — that holds in the common # pitch == width and a top-down layout — that holds in the common
# case but breaks on padded or flipped bitmaps and corrupts the # case but breaks on padded or flipped bitmaps and corrupts the
# output. Walk by (row, col) using the real pitch instead. # output. Walk by (row, col) using the real pitch instead.
#
# Cache bitmap.buffer in a local — ctypes struct field access
# creates a new Python wrapper object each time, so re-evaluating
# it per pixel is catastrophically slow.
pixels4g = [] pixels4g = []
px = 0 px = 0
buf = bitmap.buffer
abs_pitch = abs(bitmap.pitch) abs_pitch = abs(bitmap.pitch)
for y in range(bitmap.rows): for y in range(bitmap.rows):
row_offset = y * abs_pitch if bitmap.pitch >= 0 else (bitmap.rows - 1 - y) * abs_pitch row_offset = y * abs_pitch if bitmap.pitch >= 0 else (bitmap.rows - 1 - y) * abs_pitch
for x in range(bitmap.width): for x in range(bitmap.width):
v = bitmap.buffer[row_offset + x] v = buf[row_offset + x]
if x % 2 == 0: if x % 2 == 0:
px = (v >> 4) px = (v >> 4)
else: else:
@@ -234,6 +234,9 @@ def main():
if len(sys.argv) < 2: if len(sys.argv) < 2:
print(f"Usage: {sys.argv[0]} <font_headers_directory>", file=sys.stderr) print(f"Usage: {sys.argv[0]} <font_headers_directory>", file=sys.stderr)
sys.exit(1) sys.exit(1)
if sys.argv[1] in ("-h", "--help"):
print(f"Usage: {sys.argv[0]} <font_headers_directory>")
sys.exit(0)
font_dir = sys.argv[1] font_dir = sys.argv[1]
if not os.path.isdir(font_dir): if not os.path.isdir(font_dir):
+228 -23
View File
@@ -74,21 +74,170 @@ uint16_t measureWordWidth(const GfxRenderer& renderer, const int fontId, const s
return renderer.getTextAdvanceX(fontId, sanitized.c_str(), style); return renderer.getTextAdvanceX(fontId, sanitized.c_str(), style);
} }
// Checks if a UTF-8 codepoint should be counted as part of a word for Focus Reading
bool isWordCharacter(uint32_t cp) {
// ASCII range (Catches 95%+ of characters immediately)
if (cp < 128) {
// Bitwise trick: (cp | 0x20) converts uppercase ASCII to lowercase.
// This checks for A-Z and a-z mathematically, avoiding memory lookups and <cctype>
return ((cp | 0x20) >= 'a' && (cp | 0x20) <= 'z') || cp == '\'';
}
// General Punctuation Block, Currency, Math, Arrows, & Symbols (0x2000 - 0x2BFF)
if (cp >= 0x2000 && cp <= 0x2BFF) {
// Explicitly allow smart quotes, reject all other general punctuation (em-dashes, etc.)
return cp == 0x2018 || cp == 0x2019;
}
// Latin-1 Punctuation Block (0x00A1 - 0x00BF)
if (cp >= 0x00A1 && cp <= 0x00BF) {
// Allow ordinal indicators and micro sign, reject the rest (¡, ¿, «, », etc.)
return cp == 0x00AA || cp == 0x00B5 || cp == 0x00BA;
}
// Rejects Two-em dash, Three-em dash, Double oblique hyphen, etc.
if (cp >= 0x2E00 && cp <= 0x2E7F) return false;
// Rejects Modifier Minus (0x02D7), Small Hyphen (0xFE63), and Fullwidth Hyphen (0xFF0D)
if (cp == 0x02D7 || cp == 0xFE63 || cp == 0xFF0D) return false;
// Assume all other Unicode ranges (accented letters, Cyrillic, Greek, etc.) are valid
return true;
}
} // namespace } // namespace
void ParsedText::addWord(std::string word, const EpdFontFamily::Style fontStyle, const bool underline, void ParsedText::addWord(std::string word, const EpdFontFamily::Style fontStyle, const bool underline,
const bool attachToPrevious) { const bool attachToPrevious) {
if (word.empty()) return; if (word.empty()) return;
words.push_back(std::move(word)); EpdFontFamily::Style baseStyle = fontStyle;
EpdFontFamily::Style combinedStyle = fontStyle;
if (underline) { if (underline) {
combinedStyle = static_cast<EpdFontFamily::Style>(combinedStyle | EpdFontFamily::UNDERLINE); baseStyle = static_cast<EpdFontFamily::Style>(baseStyle | EpdFontFamily::UNDERLINE);
} }
wordStyles.push_back(combinedStyle);
wordContinues.push_back(attachToPrevious);
}
// Already-bold text should stay fully bold; focus splitting would make its suffix regular later.
if (!this->focusReadingEnabled || (baseStyle & EpdFontFamily::BOLD) != 0) {
words.push_back(std::move(word));
wordStyles.push_back(baseStyle);
wordContinues.push_back(attachToPrevious);
wordIsFocusSuffix.push_back(false);
return;
}
// --- FOCUS READING LOGIC BELOW ---
// Pre-reserve capacity to prevent mid-word heap reallocations.
size_t maxPossibleNewTokens = word.length();
size_t requiredSize = words.size() + maxPossibleNewTokens;
if (words.capacity() < requiredSize) {
// Emulate standard geometric growth (doubling) to ensure we don't reallocate on every word.
size_t newCapacity = words.capacity() * 2;
// Ensure the doubled capacity is actually enough for this specific word
if (newCapacity < requiredSize) {
newCapacity = requiredSize;
}
// Set a sensible minimum starting size so the first few words don't trigger tiny reallocations
if (newCapacity < 16) {
newCapacity = 16;
}
words.reserve(newCapacity);
wordStyles.reserve(newCapacity);
wordContinues.reserve(newCapacity);
wordIsFocusSuffix.reserve(newCapacity);
}
// Lambda helper to process and push individual sub-segments of the string
// Use std::string_view to avoid heap allocations when slicing
auto processSegment = [&](std::string_view segment, bool isWord, bool attach) {
if (!isWord) {
// Punctuation and Numbers stay regular
words.emplace_back(segment);
wordStyles.push_back(baseStyle);
wordContinues.push_back(attach);
wordIsFocusSuffix.push_back(false);
} else {
size_t charCount = 0;
const unsigned char* countPtr = reinterpret_cast<const unsigned char*>(segment.data());
const unsigned char* countEnd = countPtr + segment.length();
while (countPtr < countEnd) {
utf8NextCodepoint(&countPtr);
charCount++;
}
// Target 45% for 1-bold at 4 chars and 3-bold at 7 chars with floor truncation
constexpr size_t FOCUS_READING_PERCENT = 45;
size_t targetBoldChars = (charCount * FOCUS_READING_PERCENT) / 100;
targetBoldChars = std::clamp<size_t>(targetBoldChars, 1, 9);
if (targetBoldChars >= charCount) {
// Whole segment is bold - no suffix split needed
words.emplace_back(segment);
wordStyles.push_back(static_cast<EpdFontFamily::Style>(baseStyle | EpdFontFamily::BOLD));
wordContinues.push_back(attach);
wordIsFocusSuffix.push_back(false);
} else {
countPtr = reinterpret_cast<const unsigned char*>(segment.data());
for (size_t i = 0; i < targetBoldChars; ++i) {
utf8NextCodepoint(&countPtr);
}
size_t splitByteOffset = countPtr - reinterpret_cast<const unsigned char*>(segment.data());
// Bold prefix
words.emplace_back(segment.substr(0, splitByteOffset));
wordStyles.push_back(static_cast<EpdFontFamily::Style>(baseStyle | EpdFontFamily::BOLD));
wordContinues.push_back(attach);
wordIsFocusSuffix.push_back(false);
// Regular suffix - marked so extractLine can merge it back into single TextBlock entry
words.emplace_back(segment.substr(splitByteOffset));
wordStyles.push_back(baseStyle);
wordContinues.push_back(true);
wordIsFocusSuffix.push_back(true);
}
}
};
// Tokenize the string by alternating states (Word vs. Non-Word)
const unsigned char* ptr = reinterpret_cast<const unsigned char*>(word.c_str());
const unsigned char* end = ptr + word.length();
const unsigned char* segmentStart = ptr;
uint32_t firstCp = utf8NextCodepoint(&ptr); // Consume the first char to determine initial state
bool inWordSegment = isWordCharacter(firstCp);
bool isFirstSegment = true;
while (ptr < end) {
const unsigned char* currentCpStart = ptr;
uint32_t cp = utf8NextCodepoint(&ptr);
bool isWordChar = isWordCharacter(cp);
// Whenever the character type flips, slice off the segment we just completed and process it
if (isWordChar != inWordSegment) {
size_t segmentLen = currentCpStart - segmentStart;
std::string_view segment(reinterpret_cast<const char*>(segmentStart), segmentLen);
// Only the very first segment inherits the original attachToPrevious flag.
// Every subsequent segment MUST attach=true so it glues seamlessly to the prefix.
processSegment(segment, inWordSegment, isFirstSegment ? attachToPrevious : true);
// Setup for the next segment
segmentStart = currentCpStart;
inWordSegment = isWordChar;
isFirstSegment = false;
}
}
// Process the final remaining segment
size_t segmentLen = end - segmentStart;
std::string_view segment(reinterpret_cast<const char*>(segmentStart), segmentLen);
processSegment(segment, inWordSegment, isFirstSegment ? attachToPrevious : true);
}
// Consumes data to minimize memory usage // Consumes data to minimize memory usage
void ParsedText::layoutAndExtractLines(const GfxRenderer& renderer, const int fontId, const uint16_t viewportWidth, void ParsedText::layoutAndExtractLines(const GfxRenderer& renderer, const int fontId, const uint16_t viewportWidth,
const std::function<void(std::shared_ptr<TextBlock>)>& processLine, const std::function<void(std::shared_ptr<TextBlock>)>& processLine,
@@ -106,20 +255,6 @@ void ParsedText::layoutAndExtractLines(const GfxRenderer& renderer, const int fo
// (advanceX only, no bitmaps) for all unique codepoints in this paragraph so // (advanceX only, no bitmaps) for all unique codepoints in this paragraph so
// that calculateWordWidths() can measure text without on-demand SD I/O. // that calculateWordWidths() can measure text without on-demand SD I/O.
if (renderer.isSdCardFont(fontId)) { if (renderer.isSdCardFont(fontId)) {
// Reserve upfront so the joined text allocates exactly once. Without this,
// paragraphs with many words trigger a chain of vector-like reallocations
// inside std::string during layout — visible in prewarm timings for SD fonts.
size_t totalSize = hyphenationEnabled ? 1 : 0;
if (!words.empty()) totalSize += words.size() - 1; // inter-word spaces
for (const auto& w : words) totalSize += w.size();
std::string allText;
allText.reserve(totalSize);
for (size_t i = 0; i < words.size(); i++) {
if (i > 0) allText += ' ';
allText += words[i];
}
if (hyphenationEnabled) allText += '-';
// Style mask: only ask the SD font to load advances for styles actually // Style mask: only ask the SD font to load advances for styles actually
// used in this paragraph. Style index is the low two bits (regular/bold/ // used in this paragraph. Style index is the low two bits (regular/bold/
// italic/bold-italic); the underline bit is irrelevant to advance metrics. // italic/bold-italic); the underline bit is irrelevant to advance metrics.
@@ -128,7 +263,7 @@ void ParsedText::layoutAndExtractLines(const GfxRenderer& renderer, const int fo
styleMask |= static_cast<uint8_t>(1u << (static_cast<uint8_t>(s) & 0x03)); styleMask |= static_cast<uint8_t>(1u << (static_cast<uint8_t>(s) & 0x03));
} }
if (styleMask == 0) styleMask = 0x01; // defensive: regular only if (styleMask == 0) styleMask = 0x01; // defensive: regular only
renderer.ensureSdCardFontReady(fontId, allText.c_str(), styleMask); renderer.ensureSdCardFontReady(fontId, words, hyphenationEnabled, styleMask);
} }
const int pageWidth = viewportWidth; const int pageWidth = viewportWidth;
@@ -153,6 +288,7 @@ void ParsedText::layoutAndExtractLines(const GfxRenderer& renderer, const int fo
words.erase(words.begin(), words.begin() + consumed); words.erase(words.begin(), words.begin() + consumed);
wordStyles.erase(wordStyles.begin(), wordStyles.begin() + consumed); wordStyles.erase(wordStyles.begin(), wordStyles.begin() + consumed);
wordContinues.erase(wordContinues.begin(), wordContinues.begin() + consumed); wordContinues.erase(wordContinues.begin(), wordContinues.begin() + consumed);
wordIsFocusSuffix.erase(wordIsFocusSuffix.begin(), wordIsFocusSuffix.begin() + consumed);
} }
} }
@@ -436,6 +572,8 @@ bool ParsedText::hyphenateWordAtIndex(const size_t wordIndex, const int availabl
// Insert the remainder word (with matching style and continuation flag) directly after the prefix. // Insert the remainder word (with matching style and continuation flag) directly after the prefix.
words.insert(words.begin() + wordIndex + 1, remainder); words.insert(words.begin() + wordIndex + 1, remainder);
wordStyles.insert(wordStyles.begin() + wordIndex + 1, style); wordStyles.insert(wordStyles.begin() + wordIndex + 1, style);
// The hyphen remainder is not a focus suffix - it starts fresh on the next line.
wordIsFocusSuffix.insert(wordIsFocusSuffix.begin() + wordIndex + 1, false);
// Continuation flag handling after splitting a word into prefix + remainder. // Continuation flag handling after splitting a word into prefix + remainder.
// //
@@ -500,6 +638,11 @@ void ParsedText::extractLine(const size_t breakIndex, const int pageWidth, const
renderer.getSpaceAdvance(fontId, lastCodepoint(words[lastBreakAt + wordIdx - 1]), renderer.getSpaceAdvance(fontId, lastCodepoint(words[lastBreakAt + wordIdx - 1]),
firstCodepoint(words[lastBreakAt + wordIdx]), wordStyles[lastBreakAt + wordIdx - 1]); firstCodepoint(words[lastBreakAt + wordIdx]), wordStyles[lastBreakAt + wordIdx - 1]);
} else if (wordIdx > 0 && continuesVec[lastBreakAt + wordIdx]) { } else if (wordIdx > 0 && continuesVec[lastBreakAt + wordIdx]) {
// Non-breaking space tokens (" " with continues=true) are visible, stretchable spaces —
// count them as justifiable gaps so justifyExtra is distributed to them too.
if (words[lastBreakAt + wordIdx] == " ") {
actualGapCount++;
}
// Cross-boundary kerning for continuation words (e.g. nonbreaking spaces, attached punctuation) // Cross-boundary kerning for continuation words (e.g. nonbreaking spaces, attached punctuation)
totalNaturalGaps += totalNaturalGaps +=
renderer.getKerning(fontId, lastCodepoint(words[lastBreakAt + wordIdx - 1]), renderer.getKerning(fontId, lastCodepoint(words[lastBreakAt + wordIdx - 1]),
@@ -541,6 +684,11 @@ void ParsedText::extractLine(const size_t breakIndex, const int pageWidth, const
advance += advance +=
renderer.getKerning(fontId, lastCodepoint(words[lastBreakAt + wordIdx]), renderer.getKerning(fontId, lastCodepoint(words[lastBreakAt + wordIdx]),
firstCodepoint(words[lastBreakAt + wordIdx + 1]), wordStyles[lastBreakAt + wordIdx]); firstCodepoint(words[lastBreakAt + wordIdx + 1]), wordStyles[lastBreakAt + wordIdx]);
// Non-breaking space tokens are stretchable — expand them during justification like normal spaces.
if (words[lastBreakAt + wordIdx] == " " && continuesVec[lastBreakAt + wordIdx] &&
blockStyle.alignment == CssTextAlign::Justify && !isLastLine) {
advance += justifyExtra;
}
xpos += advance; xpos += advance;
} else { } else {
int gap = 0; int gap = 0;
@@ -567,6 +715,63 @@ void ParsedText::extractLine(const size_t breakIndex, const int pageWidth, const
} }
} }
processLine( // Fast path: when no word on this line was split for focus reading, skip the merge work
std::make_shared<TextBlock>(std::move(lineWords), std::move(lineXPos), std::move(lineWordStyles), blockStyle)); // entirely and pass empty boundary/suffixX vectors. TextBlock pays zero per-word RAM cost
// for these annotations when the vectors are empty.
bool lineHasFocusSplit = false;
for (size_t i = 0; i < lineWordCount; i++) {
if (wordIsFocusSuffix[lastBreakAt + i]) {
lineHasFocusSplit = true;
break;
}
}
if (!lineHasFocusSplit) {
processLine(std::make_shared<TextBlock>(std::move(lineWords), std::move(lineXPos), std::move(lineWordStyles),
std::vector<uint8_t>{}, std::vector<uint16_t>{}, blockStyle));
return;
}
// Slow path: merge focus suffix tokens back into their preceding word entry so each
// original word occupies one TextBlock slot. Splits are recorded as per-word annotations
// applied at render time, cutting the token count significantly when the feature is active.
std::vector<std::string> outWords;
std::vector<int16_t> outXPos;
std::vector<EpdFontFamily::Style> outStyles;
std::vector<uint8_t> outBoundaries;
std::vector<uint16_t> outSuffixX;
outWords.reserve(lineWordCount);
outXPos.reserve(lineWordCount);
outStyles.reserve(lineWordCount);
outBoundaries.reserve(lineWordCount);
outSuffixX.reserve(lineWordCount);
for (size_t i = 0; i < lineWordCount; i++) {
if (wordIsFocusSuffix[lastBreakAt + i] && !outWords.empty()) {
// Focus suffix: merge string into the preceding bold-prefix entry.
outWords.back() += lineWords[i];
} else {
// Normal word: check for a following focus suffix to record the byte boundary.
uint8_t boundary = 0;
uint16_t suffixX = 0;
if (i + 1 < lineWordCount && wordIsFocusSuffix[lastBreakAt + i + 1]) {
boundary = static_cast<uint8_t>(std::min(lineWords[i].size(), size_t{255}));
// Suffix x offset = layout-time advance of the bold prefix, already known from xpos table.
suffixX = static_cast<uint16_t>(lineXPos[i + 1] - lineXPos[i]);
}
outWords.push_back(std::move(lineWords[i]));
outXPos.push_back(lineXPos[i]);
// For focus entries with a suffix, strip BOLD from the stored style.
// Render re-applies it to the prefix portion only, via the boundary field.
const EpdFontFamily::Style storedStyle =
boundary > 0 ? static_cast<EpdFontFamily::Style>(lineWordStyles[i] & ~EpdFontFamily::BOLD)
: lineWordStyles[i];
outStyles.push_back(storedStyle);
outBoundaries.push_back(boundary);
outSuffixX.push_back(suffixX);
}
}
processLine(std::make_shared<TextBlock>(std::move(outWords), std::move(outXPos), std::move(outStyles),
std::move(outBoundaries), std::move(outSuffixX), blockStyle));
} }
+8 -3
View File
@@ -15,10 +15,12 @@ class GfxRenderer;
class ParsedText { class ParsedText {
std::vector<std::string> words; std::vector<std::string> words;
std::vector<EpdFontFamily::Style> wordStyles; std::vector<EpdFontFamily::Style> wordStyles;
std::vector<bool> wordContinues; // true = word attaches to previous (no space before it) std::vector<bool> wordContinues; // true = word attaches to previous (no space before it)
std::vector<bool> wordIsFocusSuffix; // true = token is the regular tail of a focus bold-prefix split
BlockStyle blockStyle; BlockStyle blockStyle;
bool extraParagraphSpacing; bool extraParagraphSpacing;
bool hyphenationEnabled; bool hyphenationEnabled;
bool focusReadingEnabled;
void applyParagraphIndent(); void applyParagraphIndent();
std::vector<size_t> computeLineBreaks(const GfxRenderer& renderer, int fontId, int pageWidth, std::vector<size_t> computeLineBreaks(const GfxRenderer& renderer, int fontId, int pageWidth,
@@ -35,8 +37,11 @@ class ParsedText {
public: public:
explicit ParsedText(const bool extraParagraphSpacing, const bool hyphenationEnabled = false, explicit ParsedText(const bool extraParagraphSpacing, const bool hyphenationEnabled = false,
const BlockStyle& blockStyle = BlockStyle()) const bool focusReadingEnabled = false, const BlockStyle& blockStyle = BlockStyle())
: blockStyle(blockStyle), extraParagraphSpacing(extraParagraphSpacing), hyphenationEnabled(hyphenationEnabled) {} : blockStyle(blockStyle),
extraParagraphSpacing(extraParagraphSpacing),
hyphenationEnabled(hyphenationEnabled),
focusReadingEnabled(focusReadingEnabled) {}
~ParsedText() = default; ~ParsedText() = default;
void addWord(std::string word, EpdFontFamily::Style fontStyle, bool underline = false, bool attachToPrevious = false); void addWord(std::string word, EpdFontFamily::Style fontStyle, bool underline = false, bool attachToPrevious = false);
+15 -11
View File
@@ -13,8 +13,8 @@ namespace {
constexpr uint8_t SECTION_FILE_VERSION = 23; constexpr uint8_t SECTION_FILE_VERSION = 23;
constexpr uint32_t HEADER_SIZE = sizeof(uint8_t) + sizeof(int) + sizeof(float) + sizeof(bool) + sizeof(uint8_t) + constexpr uint32_t HEADER_SIZE = sizeof(uint8_t) + sizeof(int) + sizeof(float) + sizeof(bool) + sizeof(uint8_t) +
sizeof(uint16_t) + sizeof(uint16_t) + sizeof(uint16_t) + sizeof(bool) + sizeof(bool) + sizeof(uint16_t) + sizeof(uint16_t) + sizeof(uint16_t) + sizeof(bool) + sizeof(bool) +
sizeof(uint8_t) + sizeof(uint32_t) + sizeof(uint32_t) + sizeof(uint32_t) + sizeof(uint8_t) + sizeof(bool) + sizeof(uint32_t) + sizeof(uint32_t) +
sizeof(uint32_t); sizeof(uint32_t) + sizeof(uint32_t);
struct PageLutEntry { struct PageLutEntry {
uint32_t fileOffset; uint32_t fileOffset;
@@ -43,7 +43,8 @@ uint32_t Section::onPageComplete(std::unique_ptr<Page> page) {
void Section::writeSectionFileHeader(const int fontId, const float lineCompression, const bool extraParagraphSpacing, void Section::writeSectionFileHeader(const int fontId, const float lineCompression, const bool extraParagraphSpacing,
const uint8_t paragraphAlignment, const uint16_t viewportWidth, const uint8_t paragraphAlignment, const uint16_t viewportWidth,
const uint16_t viewportHeight, const bool hyphenationEnabled, const uint16_t viewportHeight, const bool hyphenationEnabled,
const bool embeddedStyle, const uint8_t imageRendering) { const bool embeddedStyle, const uint8_t imageRendering,
const bool focusReadingEnabled) {
if (!file) { if (!file) {
LOG_DBG("SCT", "File not open for writing header"); LOG_DBG("SCT", "File not open for writing header");
return; return;
@@ -51,8 +52,8 @@ void Section::writeSectionFileHeader(const int fontId, const float lineCompressi
static_assert(HEADER_SIZE == sizeof(SECTION_FILE_VERSION) + sizeof(fontId) + sizeof(lineCompression) + static_assert(HEADER_SIZE == sizeof(SECTION_FILE_VERSION) + sizeof(fontId) + sizeof(lineCompression) +
sizeof(extraParagraphSpacing) + sizeof(paragraphAlignment) + sizeof(viewportWidth) + sizeof(extraParagraphSpacing) + sizeof(paragraphAlignment) + sizeof(viewportWidth) +
sizeof(viewportHeight) + sizeof(pageCount) + sizeof(hyphenationEnabled) + sizeof(viewportHeight) + sizeof(pageCount) + sizeof(hyphenationEnabled) +
sizeof(embeddedStyle) + sizeof(imageRendering) + sizeof(uint32_t) + sizeof(embeddedStyle) + sizeof(imageRendering) + sizeof(focusReadingEnabled) +
sizeof(uint32_t) + sizeof(uint32_t) + sizeof(uint32_t), sizeof(uint32_t) + sizeof(uint32_t) + sizeof(uint32_t) + sizeof(uint32_t),
"Header size mismatch"); "Header size mismatch");
serialization::writePod(file, SECTION_FILE_VERSION); serialization::writePod(file, SECTION_FILE_VERSION);
serialization::writePod(file, fontId); serialization::writePod(file, fontId);
@@ -64,6 +65,7 @@ void Section::writeSectionFileHeader(const int fontId, const float lineCompressi
serialization::writePod(file, hyphenationEnabled); serialization::writePod(file, hyphenationEnabled);
serialization::writePod(file, embeddedStyle); serialization::writePod(file, embeddedStyle);
serialization::writePod(file, imageRendering); serialization::writePod(file, imageRendering);
serialization::writePod(file, focusReadingEnabled);
serialization::writePod(file, pageCount); // Placeholder for page count (will be initially 0, patched later) serialization::writePod(file, pageCount); // Placeholder for page count (will be initially 0, patched later)
serialization::writePod(file, static_cast<uint32_t>(0)); // Placeholder for LUT offset (patched later) serialization::writePod(file, static_cast<uint32_t>(0)); // Placeholder for LUT offset (patched later)
serialization::writePod(file, static_cast<uint32_t>(0)); // Placeholder for anchor map offset (patched later) serialization::writePod(file, static_cast<uint32_t>(0)); // Placeholder for anchor map offset (patched later)
@@ -74,7 +76,7 @@ void Section::writeSectionFileHeader(const int fontId, const float lineCompressi
bool Section::loadSectionFile(const int fontId, const float lineCompression, const bool extraParagraphSpacing, bool Section::loadSectionFile(const int fontId, const float lineCompression, const bool extraParagraphSpacing,
const uint8_t paragraphAlignment, const uint16_t viewportWidth, const uint8_t paragraphAlignment, const uint16_t viewportWidth,
const uint16_t viewportHeight, const bool hyphenationEnabled, const bool embeddedStyle, const uint16_t viewportHeight, const bool hyphenationEnabled, const bool embeddedStyle,
const uint8_t imageRendering) { const uint8_t imageRendering, const bool focusReadingEnabled) {
if (!Storage.openFileForRead("SCT", filePath, file)) { if (!Storage.openFileForRead("SCT", filePath, file)) {
return false; return false;
} }
@@ -99,6 +101,7 @@ bool Section::loadSectionFile(const int fontId, const float lineCompression, con
bool fileHyphenationEnabled; bool fileHyphenationEnabled;
bool fileEmbeddedStyle; bool fileEmbeddedStyle;
uint8_t fileImageRendering; uint8_t fileImageRendering;
bool fileFocusReadingEnabled;
serialization::readPod(file, fileFontId); serialization::readPod(file, fileFontId);
serialization::readPod(file, fileLineCompression); serialization::readPod(file, fileLineCompression);
serialization::readPod(file, fileExtraParagraphSpacing); serialization::readPod(file, fileExtraParagraphSpacing);
@@ -108,13 +111,13 @@ bool Section::loadSectionFile(const int fontId, const float lineCompression, con
serialization::readPod(file, fileHyphenationEnabled); serialization::readPod(file, fileHyphenationEnabled);
serialization::readPod(file, fileEmbeddedStyle); serialization::readPod(file, fileEmbeddedStyle);
serialization::readPod(file, fileImageRendering); serialization::readPod(file, fileImageRendering);
serialization::readPod(file, fileFocusReadingEnabled);
if (fontId != fileFontId || lineCompression != fileLineCompression || if (fontId != fileFontId || lineCompression != fileLineCompression ||
extraParagraphSpacing != fileExtraParagraphSpacing || paragraphAlignment != fileParagraphAlignment || extraParagraphSpacing != fileExtraParagraphSpacing || paragraphAlignment != fileParagraphAlignment ||
viewportWidth != fileViewportWidth || viewportHeight != fileViewportHeight || viewportWidth != fileViewportWidth || viewportHeight != fileViewportHeight ||
hyphenationEnabled != fileHyphenationEnabled || embeddedStyle != fileEmbeddedStyle || hyphenationEnabled != fileHyphenationEnabled || embeddedStyle != fileEmbeddedStyle ||
imageRendering != fileImageRendering) { imageRendering != fileImageRendering || focusReadingEnabled != fileFocusReadingEnabled) {
// Explicit close() required: member variable persists beyond function scope
file.close(); file.close();
LOG_ERR("SCT", "Deserialization failed: Parameters do not match"); LOG_ERR("SCT", "Deserialization failed: Parameters do not match");
clearCache(); clearCache();
@@ -148,7 +151,8 @@ bool Section::clearCache() const {
bool Section::createSectionFile(const int fontId, const float lineCompression, const bool extraParagraphSpacing, bool Section::createSectionFile(const int fontId, const float lineCompression, const bool extraParagraphSpacing,
const uint8_t paragraphAlignment, const uint16_t viewportWidth, const uint8_t paragraphAlignment, const uint16_t viewportWidth,
const uint16_t viewportHeight, const bool hyphenationEnabled, const bool embeddedStyle, const uint16_t viewportHeight, const bool hyphenationEnabled, const bool embeddedStyle,
const uint8_t imageRendering, const std::function<void()>& popupFn) { const uint8_t imageRendering, const bool focusReadingEnabled,
const std::function<void()>& popupFn) {
const auto localPath = epub->getSpineItem(spineIndex).href; const auto localPath = epub->getSpineItem(spineIndex).href;
const auto tmpHtmlPath = epub->getCachePath() + "/.tmp_" + std::to_string(spineIndex) + ".html"; const auto tmpHtmlPath = epub->getCachePath() + "/.tmp_" + std::to_string(spineIndex) + ".html";
@@ -199,7 +203,7 @@ bool Section::createSectionFile(const int fontId, const float lineCompression, c
return false; return false;
} }
writeSectionFileHeader(fontId, lineCompression, extraParagraphSpacing, paragraphAlignment, viewportWidth, writeSectionFileHeader(fontId, lineCompression, extraParagraphSpacing, paragraphAlignment, viewportWidth,
viewportHeight, hyphenationEnabled, embeddedStyle, imageRendering); viewportHeight, hyphenationEnabled, embeddedStyle, imageRendering, focusReadingEnabled);
std::vector<PageLutEntry> lut = {}; std::vector<PageLutEntry> lut = {};
// Derive the content base directory and image cache path prefix for the parser // Derive the content base directory and image cache path prefix for the parser
@@ -219,7 +223,7 @@ bool Section::createSectionFile(const int fontId, const float lineCompression, c
ChapterHtmlSlimParser visitor( ChapterHtmlSlimParser visitor(
epub, tmpHtmlPath, renderer, fontId, lineCompression, extraParagraphSpacing, paragraphAlignment, viewportWidth, epub, tmpHtmlPath, renderer, fontId, lineCompression, extraParagraphSpacing, paragraphAlignment, viewportWidth,
viewportHeight, hyphenationEnabled, viewportHeight, hyphenationEnabled, focusReadingEnabled,
[this, &lut](std::unique_ptr<Page> page, const uint16_t paragraphIndex, const uint16_t listItemIndex) { [this, &lut](std::unique_ptr<Page> page, const uint16_t paragraphIndex, const uint16_t listItemIndex) {
lut.push_back({this->onPageComplete(std::move(page)), paragraphIndex, listItemIndex}); lut.push_back({this->onPageComplete(std::move(page)), paragraphIndex, listItemIndex});
}, },
+4 -3
View File
@@ -18,7 +18,7 @@ class Section {
void writeSectionFileHeader(int fontId, float lineCompression, bool extraParagraphSpacing, uint8_t paragraphAlignment, void writeSectionFileHeader(int fontId, float lineCompression, bool extraParagraphSpacing, uint8_t paragraphAlignment,
uint16_t viewportWidth, uint16_t viewportHeight, bool hyphenationEnabled, uint16_t viewportWidth, uint16_t viewportHeight, bool hyphenationEnabled,
bool embeddedStyle, uint8_t imageRendering); bool embeddedStyle, uint8_t imageRendering, bool focusReadingEnabled);
uint32_t onPageComplete(std::unique_ptr<Page> page); uint32_t onPageComplete(std::unique_ptr<Page> page);
public: public:
@@ -33,11 +33,12 @@ class Section {
~Section() = default; ~Section() = default;
bool loadSectionFile(int fontId, float lineCompression, bool extraParagraphSpacing, uint8_t paragraphAlignment, bool loadSectionFile(int fontId, float lineCompression, bool extraParagraphSpacing, uint8_t paragraphAlignment,
uint16_t viewportWidth, uint16_t viewportHeight, bool hyphenationEnabled, bool embeddedStyle, uint16_t viewportWidth, uint16_t viewportHeight, bool hyphenationEnabled, bool embeddedStyle,
uint8_t imageRendering); uint8_t imageRendering, bool focusReadingEnabled);
bool clearCache() const; bool clearCache() const;
bool createSectionFile(int fontId, float lineCompression, bool extraParagraphSpacing, uint8_t paragraphAlignment, bool createSectionFile(int fontId, float lineCompression, bool extraParagraphSpacing, uint8_t paragraphAlignment,
uint16_t viewportWidth, uint16_t viewportHeight, bool hyphenationEnabled, bool embeddedStyle, uint16_t viewportWidth, uint16_t viewportHeight, bool hyphenationEnabled, bool embeddedStyle,
uint8_t imageRendering, const std::function<void()>& popupFn = nullptr); uint8_t imageRendering, bool focusReadingEnabled,
const std::function<void()>& popupFn = nullptr);
std::unique_ptr<Page> loadPageFromSectionFile(); std::unique_ptr<Page> loadPageFromSectionFile();
// Look up the page number for an anchor id from the section cache file. // Look up the page number for an anchor id from the section cache file.
+62 -10
View File
@@ -4,18 +4,44 @@
#include <Logging.h> #include <Logging.h>
#include <Serialization.h> #include <Serialization.h>
#include <cstring>
void TextBlock::render(const GfxRenderer& renderer, const int fontId, const int x, const int y) const { void TextBlock::render(const GfxRenderer& renderer, const int fontId, const int x, const int y) const {
// Validate iterator bounds before rendering // Focus annotations are optional: empty vectors mean no word in this block has a split.
if (words.size() != wordXpos.size() || words.size() != wordStyles.size()) { // When present, they must be sized in lockstep with words[].
LOG_ERR("TXB", "Render skipped: size mismatch (words=%u, xpos=%u, styles=%u)\n", (uint32_t)words.size(), const bool hasFocus = !wordFocusBoundary.empty();
(uint32_t)wordXpos.size(), (uint32_t)wordStyles.size()); if (words.size() != wordXpos.size() || words.size() != wordStyles.size() ||
(hasFocus && (words.size() != wordFocusBoundary.size() || words.size() != wordFocusSuffixX.size()))) {
LOG_ERR("TXB", "Render skipped: size mismatch (words=%u, xpos=%u, styles=%u, boundary=%u, suffixX=%u)\n",
(uint32_t)words.size(), (uint32_t)wordXpos.size(), (uint32_t)wordStyles.size(),
(uint32_t)wordFocusBoundary.size(), (uint32_t)wordFocusSuffixX.size());
return; return;
} }
for (size_t i = 0; i < words.size(); i++) { for (size_t i = 0; i < words.size(); i++) {
const int wordX = wordXpos[i] + x; const int wordX = wordXpos[i] + x;
const EpdFontFamily::Style currentStyle = wordStyles[i]; const EpdFontFamily::Style currentStyle = wordStyles[i];
renderer.drawText(fontId, wordX, y, words[i].c_str(), true, currentStyle); const uint8_t boundary = hasFocus ? wordFocusBoundary[i] : 0;
if (boundary > 0) {
// Focus split: draw bold prefix, then the regular suffix at a pre-computed x offset.
// The bold prefix is bounded to 9 codepoints by the clamp on targetBoldChars in
// ParsedText::addWord; 9 UTF-8 codepoints occupy at most 9 * 4 = 36 bytes, +1 for null = 37.
// suffixX is computed at cache-creation time to avoid font metric lookups at render time.
static constexpr size_t MAX_FOCUS_PREFIX_BYTES = 9 * 4 + 1;
char boldBuf[40];
static_assert(sizeof(boldBuf) >= MAX_FOCUS_PREFIX_BYTES,
"boldBuf too small for max focus prefix (9 codepoints * 4 UTF-8 bytes + null)");
const auto boldStyle = static_cast<EpdFontFamily::Style>(currentStyle | EpdFontFamily::BOLD);
const size_t boldLen = std::min<size_t>({static_cast<size_t>(boundary), words[i].size(), sizeof(boldBuf) - 1});
memcpy(boldBuf, words[i].c_str(), boldLen);
boldBuf[boldLen] = '\0';
renderer.drawText(fontId, wordX, y, boldBuf, true, boldStyle);
const int suffixX = wordX + wordFocusSuffixX[i];
renderer.drawText(fontId, suffixX, y, words[i].c_str() + boldLen, true, currentStyle);
} else {
renderer.drawText(fontId, wordX, y, words[i].c_str(), true, currentStyle);
}
if ((currentStyle & EpdFontFamily::UNDERLINE) != 0) { if ((currentStyle & EpdFontFamily::UNDERLINE) != 0) {
const std::string& w = words[i]; const std::string& w = words[i];
@@ -42,9 +68,15 @@ void TextBlock::render(const GfxRenderer& renderer, const int fontId, const int
} }
bool TextBlock::serialize(FsFile& file) const { bool TextBlock::serialize(FsFile& file) const {
if (words.size() != wordXpos.size() || words.size() != wordStyles.size()) { // Focus annotations are optional; vectors are either empty (no splits in this block)
LOG_ERR("TXB", "Serialization failed: size mismatch (words=%u, xpos=%u, styles=%u)\n", words.size(), // or sized in lockstep with words[].
wordXpos.size(), wordStyles.size()); const bool hasFocus = !wordFocusBoundary.empty();
if (words.size() != wordXpos.size() || words.size() != wordStyles.size() ||
(hasFocus && (words.size() != wordFocusBoundary.size() || words.size() != wordFocusSuffixX.size()))) {
LOG_ERR("TXB", "Serialization failed: size mismatch (words=%u, xpos=%u, styles=%u, boundary=%u, suffixX=%u)\n",
static_cast<uint32_t>(words.size()), static_cast<uint32_t>(wordXpos.size()),
static_cast<uint32_t>(wordStyles.size()), static_cast<uint32_t>(wordFocusBoundary.size()),
static_cast<uint32_t>(wordFocusSuffixX.size()));
return false; return false;
} }
@@ -53,6 +85,13 @@ bool TextBlock::serialize(FsFile& file) const {
for (const auto& w : words) serialization::writeString(file, w); for (const auto& w : words) serialization::writeString(file, w);
for (auto x : wordXpos) serialization::writePod(file, x); for (auto x : wordXpos) serialization::writePod(file, x);
for (auto s : wordStyles) serialization::writePod(file, s); for (auto s : wordStyles) serialization::writePod(file, s);
// Focus block: 1-byte presence flag, followed by per-word vectors only when present.
// Saves 3 bytes/word when focus reading is disabled or no word on this line was split.
serialization::writePod(file, static_cast<uint8_t>(hasFocus ? 1 : 0));
if (hasFocus) {
for (auto b : wordFocusBoundary) serialization::writePod(file, b);
for (auto sx : wordFocusSuffixX) serialization::writePod(file, sx);
}
// Style (alignment + margins/padding/indent) // Style (alignment + margins/padding/indent)
serialization::writePod(file, blockStyle.alignment); serialization::writePod(file, blockStyle.alignment);
@@ -76,6 +115,8 @@ std::unique_ptr<TextBlock> TextBlock::deserialize(FsFile& file) {
std::vector<std::string> words; std::vector<std::string> words;
std::vector<int16_t> wordXpos; std::vector<int16_t> wordXpos;
std::vector<EpdFontFamily::Style> wordStyles; std::vector<EpdFontFamily::Style> wordStyles;
std::vector<uint8_t> wordFocusBoundary;
std::vector<uint16_t> wordFocusSuffixX;
BlockStyle blockStyle; BlockStyle blockStyle;
// Word count // Word count
@@ -94,6 +135,16 @@ std::unique_ptr<TextBlock> TextBlock::deserialize(FsFile& file) {
for (auto& w : words) serialization::readString(file, w); for (auto& w : words) serialization::readString(file, w);
for (auto& x : wordXpos) serialization::readPod(file, x); for (auto& x : wordXpos) serialization::readPod(file, x);
for (auto& s : wordStyles) serialization::readPod(file, s); for (auto& s : wordStyles) serialization::readPod(file, s);
// Focus block: presence flag, then vectors only if present. Empty vectors when absent
// signal "no splits in this block" to render() (zero per-word RAM cost).
uint8_t hasFocus;
serialization::readPod(file, hasFocus);
if (hasFocus) {
wordFocusBoundary.resize(wc);
wordFocusSuffixX.resize(wc);
for (auto& b : wordFocusBoundary) serialization::readPod(file, b);
for (auto& sx : wordFocusSuffixX) serialization::readPod(file, sx);
}
// Style (alignment + margins/padding/indent) // Style (alignment + margins/padding/indent)
serialization::readPod(file, blockStyle.alignment); serialization::readPod(file, blockStyle.alignment);
@@ -109,6 +160,7 @@ std::unique_ptr<TextBlock> TextBlock::deserialize(FsFile& file) {
serialization::readPod(file, blockStyle.textIndent); serialization::readPod(file, blockStyle.textIndent);
serialization::readPod(file, blockStyle.textIndentDefined); serialization::readPod(file, blockStyle.textIndentDefined);
return std::unique_ptr<TextBlock>( return std::unique_ptr<TextBlock>(new TextBlock(std::move(words), std::move(wordXpos), std::move(wordStyles),
new TextBlock(std::move(words), std::move(wordXpos), std::move(wordStyles), blockStyle)); std::move(wordFocusBoundary), std::move(wordFocusSuffixX),
blockStyle));
} }
+15 -1
View File
@@ -15,14 +15,28 @@ class TextBlock final : public Block {
std::vector<std::string> words; std::vector<std::string> words;
std::vector<int16_t> wordXpos; std::vector<int16_t> wordXpos;
std::vector<EpdFontFamily::Style> wordStyles; std::vector<EpdFontFamily::Style> wordStyles;
// Per-word focus boundary: N > 0 means the first N bytes of words[i] are rendered bold,
// the remainder in the base style. 0 means no split (whole word uses wordStyles[i]).
// N encodes the bold PREFIX length only — bounded to 9 codepoints (≤36 UTF-8 bytes) by
// FOCUS_READING_PERCENT's 1..9 clamp in ParsedText::addWord, so it always fits in uint8_t.
// Vector is empty when no focus splits exist anywhere in the block (zero per-word RAM cost
// when focus reading is disabled, or on lines that happen to contain no splittable words).
std::vector<uint8_t> wordFocusBoundary;
// Pre-computed pixel offset from word start to the regular suffix, stored when boundary > 0.
// Eliminates getTextAdvanceX from the render path. 0 when boundary == 0.
// Empty in lockstep with wordFocusBoundary.
std::vector<uint16_t> wordFocusSuffixX;
BlockStyle blockStyle; BlockStyle blockStyle;
public: public:
explicit TextBlock(std::vector<std::string> words, std::vector<int16_t> word_xpos, explicit TextBlock(std::vector<std::string> words, std::vector<int16_t> word_xpos,
std::vector<EpdFontFamily::Style> word_styles, const BlockStyle& blockStyle = BlockStyle()) std::vector<EpdFontFamily::Style> word_styles, std::vector<uint8_t> focus_boundary,
std::vector<uint16_t> focus_suffix_x, const BlockStyle& blockStyle = BlockStyle())
: words(std::move(words)), : words(std::move(words)),
wordXpos(std::move(word_xpos)), wordXpos(std::move(word_xpos)),
wordStyles(std::move(word_styles)), wordStyles(std::move(word_styles)),
wordFocusBoundary(std::move(focus_boundary)),
wordFocusSuffixX(std::move(focus_suffix_x)),
blockStyle(blockStyle) {} blockStyle(blockStyle) {}
~TextBlock() override = default; ~TextBlock() override = default;
void setBlockStyle(const BlockStyle& blockStyle) { this->blockStyle = blockStyle; } void setBlockStyle(const BlockStyle& blockStyle) { this->blockStyle = blockStyle; }
@@ -141,7 +141,7 @@ void ChapterHtmlSlimParser::startNewTextBlock(const BlockStyle& blockStyle) {
anchorData.push_back({std::move(pendingAnchorId), static_cast<uint16_t>(completedPageCount)}); anchorData.push_back({std::move(pendingAnchorId), static_cast<uint16_t>(completedPageCount)});
pendingAnchorId.clear(); pendingAnchorId.clear();
} }
currentTextBlock.reset(new ParsedText(extraParagraphSpacing, hyphenationEnabled, blockStyle)); currentTextBlock.reset(new ParsedText(extraParagraphSpacing, hyphenationEnabled, focusReadingEnabled, blockStyle));
wordsExtractedInBlock = 0; wordsExtractedInBlock = 0;
} }
@@ -47,6 +47,7 @@ class ChapterHtmlSlimParser {
uint16_t viewportWidth; uint16_t viewportWidth;
uint16_t viewportHeight; uint16_t viewportHeight;
bool hyphenationEnabled; bool hyphenationEnabled;
bool focusReadingEnabled;
const CssParser* cssParser; const CssParser* cssParser;
bool embeddedStyle; bool embeddedStyle;
uint8_t imageRendering; uint8_t imageRendering;
@@ -101,6 +102,7 @@ class ChapterHtmlSlimParser {
const int fontId, const float lineCompression, const bool extraParagraphSpacing, const int fontId, const float lineCompression, const bool extraParagraphSpacing,
const uint8_t paragraphAlignment, const uint16_t viewportWidth, const uint8_t paragraphAlignment, const uint16_t viewportWidth,
const uint16_t viewportHeight, const bool hyphenationEnabled, const uint16_t viewportHeight, const bool hyphenationEnabled,
const bool focusReadingEnabled,
const std::function<void(std::unique_ptr<Page>, uint16_t, uint16_t)>& completePageFn, const std::function<void(std::unique_ptr<Page>, uint16_t, uint16_t)>& completePageFn,
const bool embeddedStyle, const std::string& contentBase, const bool embeddedStyle, const std::string& contentBase,
const std::string& imageBasePath, const uint8_t imageRendering = 0, const std::string& imageBasePath, const uint8_t imageRendering = 0,
@@ -116,6 +118,7 @@ class ChapterHtmlSlimParser {
viewportWidth(viewportWidth), viewportWidth(viewportWidth),
viewportHeight(viewportHeight), viewportHeight(viewportHeight),
hyphenationEnabled(hyphenationEnabled), hyphenationEnabled(hyphenationEnabled),
focusReadingEnabled(focusReadingEnabled),
completePageFn(completePageFn), completePageFn(completePageFn),
popupFn(popupFn), popupFn(popupFn),
cssParser(cssParser), cssParser(cssParser),
+30 -4
View File
@@ -10,6 +10,19 @@
#include "FontCacheManager.h" #include "FontCacheManager.h"
namespace {
const char* resolveVisualText(const char* text, std::string& visualBuffer, int paragraphLevel);
/**
* Resolves the requested style to the best available style in the given SD card font.
* Falls back gracefully when the font lacks the requested variant.
*/
uint8_t resolveSdCardStyle(const SdCardFont& font, const EpdFontFamily::Style style) {
return font.resolveStyle(static_cast<uint8_t>(style));
}
} // namespace
const uint8_t* GfxRenderer::getGlyphBitmap(const EpdFontData* fontData, const EpdGlyph* glyph) const { const uint8_t* GfxRenderer::getGlyphBitmap(const EpdFontData* fontData, const EpdGlyph* glyph) const {
if (fontData->groups != nullptr) { if (fontData->groups != nullptr) {
auto* fd = fontCacheManager_ ? fontCacheManager_->getDecompressor() : nullptr; auto* fd = fontCacheManager_ ? fontCacheManager_->getDecompressor() : nullptr;
@@ -40,11 +53,22 @@ const uint8_t* GfxRenderer::getGlyphBitmap(const EpdFontData* fontData, const Ep
void GfxRenderer::ensureSdCardFontReady(int fontId, const char* utf8Text, uint8_t styleMask) const { void GfxRenderer::ensureSdCardFontReady(int fontId, const char* utf8Text, uint8_t styleMask) const {
auto it = sdCardFonts_.find(fontId); auto it = sdCardFonts_.find(fontId);
if (it != sdCardFonts_.end()) {
int missed = it->second->buildAdvanceTable(utf8Text, styleMask);
if (missed > 0) {
LOG_DBG("GFX", "ensureSdCardFontReady: %d glyph(s) not found", missed);
}
}
}
void GfxRenderer::ensureSdCardFontReady(int fontId, const std::vector<std::string>& words, bool includeHyphen,
uint8_t styleMask) const {
auto it = sdCardFonts_.find(fontId);
if (it != sdCardFonts_.end()) { if (it != sdCardFonts_.end()) {
// Augment the persistent advance-only table for layout measurement. // Augment the persistent advance-only table for layout measurement.
// The table survives across paragraphs/sections (capped per font), so // The table survives across paragraphs/sections (capped per font), so
// repeated indexing of the same SD font amortizes glyph-metric SD reads. // repeated indexing of the same SD font amortizes glyph-metric SD reads.
int missed = it->second->buildAdvanceTable(utf8Text, styleMask); int missed = it->second->buildAdvanceTable(words, includeHyphen, styleMask);
if (missed > 0) { if (missed > 0) {
LOG_DBG("GFX", "ensureSdCardFontReady: %d glyph(s) not found", missed); LOG_DBG("GFX", "ensureSdCardFontReady: %d glyph(s) not found", missed);
} }
@@ -1074,7 +1098,8 @@ int GfxRenderer::getSpaceWidth(const int fontId, const EpdFontFamily::Style styl
// Advance table fast-path for SD card fonts during layout // Advance table fast-path for SD card fonts during layout
auto sdIt = sdCardFonts_.find(fontId); auto sdIt = sdCardFonts_.find(fontId);
if (sdIt != sdCardFonts_.end() && sdIt->second->hasAdvanceTable()) { if (sdIt != sdCardFonts_.end() && sdIt->second->hasAdvanceTable()) {
return fp4::toPixel(sdIt->second->getAdvance(' ', static_cast<uint8_t>(style))); const uint8_t resolvedStyle = resolveSdCardStyle(*sdIt->second, style);
return fp4::toPixel(sdIt->second->getAdvance(' ', resolvedStyle));
} }
const auto fontIt = fontMap.find(fontId); const auto fontIt = fontMap.find(fontId);
@@ -1094,7 +1119,8 @@ int GfxRenderer::getSpaceAdvance(const int fontId, const uint32_t leftCp, const
// so we return just the space advance without kerning. // so we return just the space advance without kerning.
auto sdIt = sdCardFonts_.find(fontId); auto sdIt = sdCardFonts_.find(fontId);
if (sdIt != sdCardFonts_.end() && sdIt->second->hasAdvanceTable()) { if (sdIt != sdCardFonts_.end() && sdIt->second->hasAdvanceTable()) {
return fp4::toPixel(sdIt->second->getAdvance(' ', static_cast<uint8_t>(style))); const uint8_t resolvedStyle = resolveSdCardStyle(*sdIt->second, style);
return fp4::toPixel(sdIt->second->getAdvance(' ', resolvedStyle));
} }
const auto fontIt = fontMap.find(fontId); const auto fontIt = fontMap.find(fontId);
@@ -1124,7 +1150,7 @@ int GfxRenderer::getTextAdvanceX(const int fontId, const char* text, EpdFontFami
auto sdIt = sdCardFonts_.find(fontId); auto sdIt = sdCardFonts_.find(fontId);
if (sdIt != sdCardFonts_.end() && sdIt->second->hasAdvanceTable()) { if (sdIt != sdCardFonts_.end() && sdIt->second->hasAdvanceTable()) {
int32_t widthFP = 0; int32_t widthFP = 0;
const uint8_t styleIdx = static_cast<uint8_t>(style); const uint8_t styleIdx = resolveSdCardStyle(*sdIt->second, style);
while (uint32_t cp = utf8NextCodepoint(reinterpret_cast<const uint8_t**>(&text))) { while (uint32_t cp = utf8NextCodepoint(reinterpret_cast<const uint8_t**>(&text))) {
widthFP += sdIt->second->getAdvance(cp, styleIdx); widthFP += sdIt->second->getAdvance(cp, styleIdx);
} }
+2
View File
@@ -94,6 +94,8 @@ class GfxRenderer {
// (which holds a const GfxRenderer&) before measuring word widths. Safe to call on non-SD fonts (no-op). // (which holds a const GfxRenderer&) before measuring word widths. Safe to call on non-SD fonts (no-op).
// styleMask: bitmask of styles to prepare (bit 0=regular, 1=bold, 2=italic, 3=bold-italic). // styleMask: bitmask of styles to prepare (bit 0=regular, 1=bold, 2=italic, 3=bold-italic).
void ensureSdCardFontReady(int fontId, const char* utf8Text, uint8_t styleMask = 0x0F) const; void ensureSdCardFontReady(int fontId, const char* utf8Text, uint8_t styleMask = 0x0F) const;
void ensureSdCardFontReady(int fontId, const std::vector<std::string>& words, bool includeHyphen,
uint8_t styleMask = 0x0F) const;
// Orientation control (affects logical width/height and coordinate transforms) // Orientation control (affects logical width/height and coordinate transforms)
void setOrientation(const Orientation o) { orientation = o; } void setOrientation(const Orientation o) { orientation = o; }
+1
View File
@@ -259,6 +259,7 @@ STR_SECTION_PREFIX: "Раздзел"
STR_UPLOAD: "Адправіць" STR_UPLOAD: "Адправіць"
STR_BOOK_S_STYLE: "Стыль кнігі" STR_BOOK_S_STYLE: "Стыль кнігі"
STR_EMBEDDED_STYLE: "Убудаваны стыль" STR_EMBEDDED_STYLE: "Убудаваны стыль"
STR_FOCUS_READING: "Фокуснае чытанне"
STR_OPDS_SERVER_URL: "URL OPDS сервера" STR_OPDS_SERVER_URL: "URL OPDS сервера"
STR_SCREENSHOT_BUTTON: "Зрабіць здымак экрана" STR_SCREENSHOT_BUTTON: "Зрабіць здымак экрана"
STR_IMAGES: "Выявы" STR_IMAGES: "Выявы"
+1
View File
@@ -287,6 +287,7 @@ STR_SECTION_PREFIX: "Secció "
STR_UPLOAD: "Puja" STR_UPLOAD: "Puja"
STR_BOOK_S_STYLE: "Estil del llibre" STR_BOOK_S_STYLE: "Estil del llibre"
STR_EMBEDDED_STYLE: "Estil incrustat" STR_EMBEDDED_STYLE: "Estil incrustat"
STR_FOCUS_READING: "Lectura enfocada"
STR_OPDS_SERVER_URL: "URL del servidor OPDS" STR_OPDS_SERVER_URL: "URL del servidor OPDS"
STR_FOOTNOTES: "Notes al peu" STR_FOOTNOTES: "Notes al peu"
STR_NO_FOOTNOTES: "No hi ha notes al peu en aquesta pàgina" STR_NO_FOOTNOTES: "No hi ha notes al peu en aquesta pàgina"
+1
View File
@@ -264,6 +264,7 @@ STR_SECTION_PREFIX: "Sekce"
STR_UPLOAD: "Nahrát" STR_UPLOAD: "Nahrát"
STR_BOOK_S_STYLE: "Styl knihy" STR_BOOK_S_STYLE: "Styl knihy"
STR_EMBEDDED_STYLE: "Vložený styl" STR_EMBEDDED_STYLE: "Vložený styl"
STR_FOCUS_READING: "Soustředěné čtení"
STR_OPDS_SERVER_URL: "URL serveru OPDS" STR_OPDS_SERVER_URL: "URL serveru OPDS"
STR_SCREENSHOT_BUTTON: "Udělat snímek obrazovky" STR_SCREENSHOT_BUTTON: "Udělat snímek obrazovky"
STR_TILT_PAGE_TURN: "Otáčení stránek nakloněním" STR_TILT_PAGE_TURN: "Otáčení stránek nakloněním"
+1
View File
@@ -287,6 +287,7 @@ STR_SECTION_PREFIX: "Afsnit "
STR_UPLOAD: "Upload" STR_UPLOAD: "Upload"
STR_BOOK_S_STYLE: "Bogens stil" STR_BOOK_S_STYLE: "Bogens stil"
STR_EMBEDDED_STYLE: "Indlejret stil" STR_EMBEDDED_STYLE: "Indlejret stil"
STR_FOCUS_READING: "Fokuslæsning"
STR_OPDS_SERVER_URL: "OPDS Server URL" STR_OPDS_SERVER_URL: "OPDS Server URL"
STR_FOOTNOTES: "Fodnoter" STR_FOOTNOTES: "Fodnoter"
STR_NO_FOOTNOTES: "Ingen fodnoter på denne side" STR_NO_FOOTNOTES: "Ingen fodnoter på denne side"
+1
View File
@@ -287,6 +287,7 @@ STR_SECTION_PREFIX: "Sectie "
STR_UPLOAD: "Uploaden" STR_UPLOAD: "Uploaden"
STR_BOOK_S_STYLE: "Stijl van boek" STR_BOOK_S_STYLE: "Stijl van boek"
STR_EMBEDDED_STYLE: "Ingebedde stijl" STR_EMBEDDED_STYLE: "Ingebedde stijl"
STR_FOCUS_READING: "Gefocust lezen"
STR_OPDS_SERVER_URL: "OPDS-server URL" STR_OPDS_SERVER_URL: "OPDS-server URL"
STR_FOOTNOTES: "Voetnoten" STR_FOOTNOTES: "Voetnoten"
STR_NO_FOOTNOTES: "Geen voetnoten op deze pagina" STR_NO_FOOTNOTES: "Geen voetnoten op deze pagina"
+5 -3
View File
@@ -296,6 +296,7 @@ STR_SECTION_PREFIX: "Section "
STR_UPLOAD: "Upload" STR_UPLOAD: "Upload"
STR_BOOK_S_STYLE: "Book's Style" STR_BOOK_S_STYLE: "Book's Style"
STR_EMBEDDED_STYLE: "Embedded Style" STR_EMBEDDED_STYLE: "Embedded Style"
STR_FOCUS_READING: "Focus Reading"
STR_OPDS_SERVER_URL: "OPDS Server URL" STR_OPDS_SERVER_URL: "OPDS Server URL"
STR_SET_SLEEP_COVER: "Set Cover" STR_SET_SLEEP_COVER: "Set Cover"
STR_FOOTNOTES: "Footnotes" STR_FOOTNOTES: "Footnotes"
@@ -310,8 +311,8 @@ STR_DELETE_CONFIRM: "Delete this server?"
STR_OPDS_SERVERS: "OPDS Servers" STR_OPDS_SERVERS: "OPDS Servers"
STR_AUTO_TURN_ENABLED: "Auto Turn Enabled: " STR_AUTO_TURN_ENABLED: "Auto Turn Enabled: "
STR_AUTO_TURN_PAGES_PER_MIN: "Auto Turn (Pages Per Minute)" STR_AUTO_TURN_PAGES_PER_MIN: "Auto Turn (Pages Per Minute)"
STR_DOWNLOAD_FONTS: "Download Fonts" STR_MANAGE_FONTS: "Manage Fonts"
STR_FONT_DOWNLOAD: "Font Download" STR_FONT_BROWSER: "Font Browser"
STR_LOADING_FONT_LIST: "Loading font list..." STR_LOADING_FONT_LIST: "Loading font list..."
STR_NO_FONTS_AVAILABLE: "No fonts available" STR_NO_FONTS_AVAILABLE: "No fonts available"
STR_FONT_INSTALLED: "Font installed!" STR_FONT_INSTALLED: "Font installed!"
@@ -322,7 +323,8 @@ STR_SD_CARD_FULL: "Insufficient SD card space"
STR_FILES_LABEL: "Files: " STR_FILES_LABEL: "Files: "
STR_SIZE_LABEL: "Size: " STR_SIZE_LABEL: "Size: "
STR_REDOWNLOAD: "Re-download" STR_REDOWNLOAD: "Re-download"
STR_DOWNLOAD_ALL: "Download / Update All" STR_DOWNLOAD_ALL: "Download All"
STR_UPDATE_ALL: "Update All"
STR_ALL_FONTS_INSTALLED: "All fonts installed!" STR_ALL_FONTS_INSTALLED: "All fonts installed!"
STR_UPDATE_AVAILABLE: "Update" STR_UPDATE_AVAILABLE: "Update"
STR_CRASH_TITLE: "System Crash" STR_CRASH_TITLE: "System Crash"
+1
View File
@@ -262,6 +262,7 @@ STR_SECTION_PREFIX: "Osio "
STR_UPLOAD: "Lähetä" STR_UPLOAD: "Lähetä"
STR_BOOK_S_STYLE: "Kirjan tyyli" STR_BOOK_S_STYLE: "Kirjan tyyli"
STR_EMBEDDED_STYLE: "Upotettu tyyli" STR_EMBEDDED_STYLE: "Upotettu tyyli"
STR_FOCUS_READING: "Keskittynyt lukeminen"
STR_OPDS_SERVER_URL: "OPDS-palvelimen osoite" STR_OPDS_SERVER_URL: "OPDS-palvelimen osoite"
STR_SCREENSHOT_BUTTON: "Ota kuvakaappaus" STR_SCREENSHOT_BUTTON: "Ota kuvakaappaus"
STR_TILT_PAGE_TURN: "Sivunkääntö kallistamalla" STR_TILT_PAGE_TURN: "Sivunkääntö kallistamalla"
+1
View File
@@ -288,6 +288,7 @@ STR_SECTION_PREFIX: "Section "
STR_UPLOAD: "Envoyer" STR_UPLOAD: "Envoyer"
STR_BOOK_S_STYLE: "Style du livre" STR_BOOK_S_STYLE: "Style du livre"
STR_EMBEDDED_STYLE: "Style intégré" STR_EMBEDDED_STYLE: "Style intégré"
STR_FOCUS_READING: "Lecture focalisée"
STR_OPDS_SERVER_URL: "URL serveur OPDS" STR_OPDS_SERVER_URL: "URL serveur OPDS"
STR_FOOTNOTES: "Notes de bas de page" STR_FOOTNOTES: "Notes de bas de page"
STR_NO_FOOTNOTES: "Aucune note sur cette page" STR_NO_FOOTNOTES: "Aucune note sur cette page"
+1
View File
@@ -289,6 +289,7 @@ STR_SECTION_PREFIX: "Abschnitt"
STR_UPLOAD: "Hochladen" STR_UPLOAD: "Hochladen"
STR_BOOK_S_STYLE: "Buch-Stil" STR_BOOK_S_STYLE: "Buch-Stil"
STR_EMBEDDED_STYLE: "Eingebetteter Stil" STR_EMBEDDED_STYLE: "Eingebetteter Stil"
STR_FOCUS_READING: "Fokus-Lesen"
STR_OPDS_SERVER_URL: "OPDS-Server-URL" STR_OPDS_SERVER_URL: "OPDS-Server-URL"
STR_SCREENSHOT_BUTTON: "Screenshot aufnehmen" STR_SCREENSHOT_BUTTON: "Screenshot aufnehmen"
STR_FOOTNOTES: "Fußnoten" STR_FOOTNOTES: "Fußnoten"
+1
View File
@@ -284,6 +284,7 @@ STR_SECTION_PREFIX: "Szakasz "
STR_UPLOAD: "Feltöltés" STR_UPLOAD: "Feltöltés"
STR_BOOK_S_STYLE: "Könyv stílusa" STR_BOOK_S_STYLE: "Könyv stílusa"
STR_EMBEDDED_STYLE: "Beágyazott stílus" STR_EMBEDDED_STYLE: "Beágyazott stílus"
STR_FOCUS_READING: "Fókuszált olvasás"
STR_OPDS_SERVER_URL: "OPDS szerver URL" STR_OPDS_SERVER_URL: "OPDS szerver URL"
STR_FOOTNOTES: "Lábjegyzetek" STR_FOOTNOTES: "Lábjegyzetek"
STR_NO_FOOTNOTES: "Nincsenek lábjegyzetek ezen az oldalon" STR_NO_FOOTNOTES: "Nincsenek lábjegyzetek ezen az oldalon"
+1
View File
@@ -288,6 +288,7 @@ STR_SECTION_PREFIX: "Sezione "
STR_UPLOAD: "Carica" STR_UPLOAD: "Carica"
STR_BOOK_S_STYLE: "Stile libro" STR_BOOK_S_STYLE: "Stile libro"
STR_EMBEDDED_STYLE: "Stile integrato dell'epub" STR_EMBEDDED_STYLE: "Stile integrato dell'epub"
STR_FOCUS_READING: "Lettura focalizzata"
STR_OPDS_SERVER_URL: "Server OPDS" STR_OPDS_SERVER_URL: "Server OPDS"
STR_FOOTNOTES: "Note a piè pagina" STR_FOOTNOTES: "Note a piè pagina"
STR_NO_FOOTNOTES: "Nessuna nota in questa pagina" STR_NO_FOOTNOTES: "Nessuna nota in questa pagina"
+1
View File
@@ -258,6 +258,7 @@ STR_SECTION_PREFIX: "Бөлім "
STR_UPLOAD: "Жүктеп салу" STR_UPLOAD: "Жүктеп салу"
STR_BOOK_S_STYLE: "Кітап стилі" STR_BOOK_S_STYLE: "Кітап стилі"
STR_EMBEDDED_STYLE: "Кірістірілген стиль" STR_EMBEDDED_STYLE: "Кірістірілген стиль"
STR_FOCUS_READING: "Зейінді оқу"
STR_OPDS_SERVER_URL: "OPDS сервері URL" STR_OPDS_SERVER_URL: "OPDS сервері URL"
STR_NO_FILES_FOUND: "Файлдар табылмады" STR_NO_FILES_FOUND: "Файлдар табылмады"
STR_IMAGES: "Суреттер" STR_IMAGES: "Суреттер"
+1
View File
@@ -284,6 +284,7 @@ STR_SECTION_PREFIX: "Dalis "
STR_UPLOAD: "Įkelti" STR_UPLOAD: "Įkelti"
STR_BOOK_S_STYLE: "Knygos stilius" STR_BOOK_S_STYLE: "Knygos stilius"
STR_EMBEDDED_STYLE: "Integruotas stilius" STR_EMBEDDED_STYLE: "Integruotas stilius"
STR_FOCUS_READING: "Sufokusuotas skaitymas"
STR_OPDS_SERVER_URL: "OPDS URL" STR_OPDS_SERVER_URL: "OPDS URL"
STR_FOOTNOTES: "Išnašos" STR_FOOTNOTES: "Išnašos"
STR_NO_FOOTNOTES: "Šiame psl. išnašų nėra" STR_NO_FOOTNOTES: "Šiame psl. išnašų nėra"
+3 -1
View File
@@ -296,6 +296,7 @@ STR_SECTION_PREFIX: "Sekcja "
STR_UPLOAD: "Wyślij" STR_UPLOAD: "Wyślij"
STR_BOOK_S_STYLE: "Styl książki" STR_BOOK_S_STYLE: "Styl książki"
STR_EMBEDDED_STYLE: "Style wbudowane w EPUB" STR_EMBEDDED_STYLE: "Style wbudowane w EPUB"
STR_FOCUS_READING: "Czytanie skupione"
STR_OPDS_SERVER_URL: "URL serwera OPDS" STR_OPDS_SERVER_URL: "URL serwera OPDS"
STR_SET_SLEEP_COVER: "Ustaw okładkę" STR_SET_SLEEP_COVER: "Ustaw okładkę"
STR_FOOTNOTES: "Przypisy" STR_FOOTNOTES: "Przypisy"
@@ -322,7 +323,8 @@ STR_SD_CARD_FULL: "Za mało pamięci na karcie SD"
STR_FILES_LABEL: "Pliki: " STR_FILES_LABEL: "Pliki: "
STR_SIZE_LABEL: "Rozmiar: " STR_SIZE_LABEL: "Rozmiar: "
STR_REDOWNLOAD: "Re-download" STR_REDOWNLOAD: "Re-download"
STR_DOWNLOAD_ALL: "Pobierz / Uaktualnij wszystkie" STR_DOWNLOAD_ALL: "Pobierz wszystkie"
STR_UPDATE_ALL: "Uaktualnij wszystkie"
STR_ALL_FONTS_INSTALLED: "Wszystkie czcionki zainstalowane!" STR_ALL_FONTS_INSTALLED: "Wszystkie czcionki zainstalowane!"
STR_UPDATE_AVAILABLE: "Uaktualnij" STR_UPDATE_AVAILABLE: "Uaktualnij"
STR_CRASH_TITLE: "Awaria systemu" STR_CRASH_TITLE: "Awaria systemu"
+1
View File
@@ -264,6 +264,7 @@ STR_SECTION_PREFIX: "Seção"
STR_UPLOAD: "Enviar" STR_UPLOAD: "Enviar"
STR_BOOK_S_STYLE: "Estilo do livro" STR_BOOK_S_STYLE: "Estilo do livro"
STR_EMBEDDED_STYLE: "Estilo embutido" STR_EMBEDDED_STYLE: "Estilo embutido"
STR_FOCUS_READING: "Leitura focada"
STR_OPDS_SERVER_URL: "URL do servidor OPDS" STR_OPDS_SERVER_URL: "URL do servidor OPDS"
STR_SCREENSHOT_BUTTON: "Capturar tela" STR_SCREENSHOT_BUTTON: "Capturar tela"
STR_TILT_PAGE_TURN: "Virar página por inclinação" STR_TILT_PAGE_TURN: "Virar página por inclinação"
+1
View File
@@ -287,6 +287,7 @@ STR_SECTION_PREFIX: "Secţiune "
STR_UPLOAD: "Încărcare" STR_UPLOAD: "Încărcare"
STR_BOOK_S_STYLE: "Stilul cărţii" STR_BOOK_S_STYLE: "Stilul cărţii"
STR_EMBEDDED_STYLE: "Stil încorporat" STR_EMBEDDED_STYLE: "Stil încorporat"
STR_FOCUS_READING: "Lectură concentrată"
STR_OPDS_SERVER_URL: "URL server OPDS" STR_OPDS_SERVER_URL: "URL server OPDS"
STR_FOOTNOTES: "Note de subsol" STR_FOOTNOTES: "Note de subsol"
STR_NO_FOOTNOTES: "Nicio notă de subsol" STR_NO_FOOTNOTES: "Nicio notă de subsol"
+1
View File
@@ -291,6 +291,7 @@ STR_SECTION_PREFIX: "Раздел "
STR_UPLOAD: "Отправить" STR_UPLOAD: "Отправить"
STR_BOOK_S_STYLE: "Стиль книги" STR_BOOK_S_STYLE: "Стиль книги"
STR_EMBEDDED_STYLE: "Встроенный стиль" STR_EMBEDDED_STYLE: "Встроенный стиль"
STR_FOCUS_READING: "Фокусное чтение"
STR_OPDS_SERVER_URL: "URL OPDS сервера" STR_OPDS_SERVER_URL: "URL OPDS сервера"
STR_SCREENSHOT_BUTTON: "Сделать снимок экрана" STR_SCREENSHOT_BUTTON: "Сделать снимок экрана"
STR_AUTO_TURN_ENABLED: "Автоперелистывание: " STR_AUTO_TURN_ENABLED: "Автоперелистывание: "
+1
View File
@@ -284,6 +284,7 @@ STR_SECTION_PREFIX: "Razdelek "
STR_UPLOAD: "Naloži" STR_UPLOAD: "Naloži"
STR_BOOK_S_STYLE: "Slog knjige" STR_BOOK_S_STYLE: "Slog knjige"
STR_EMBEDDED_STYLE: "Vgrajen slog" STR_EMBEDDED_STYLE: "Vgrajen slog"
STR_FOCUS_READING: "Fokusirano branje"
STR_OPDS_SERVER_URL: "URL OPDS strežnika" STR_OPDS_SERVER_URL: "URL OPDS strežnika"
STR_FOOTNOTES: "Opombe" STR_FOOTNOTES: "Opombe"
STR_NO_FOOTNOTES: "Na tej strani ni opomb" STR_NO_FOOTNOTES: "Na tej strani ni opomb"
+1
View File
@@ -288,6 +288,7 @@ STR_SECTION_PREFIX: "Secc.:"
STR_UPLOAD: "Subir" STR_UPLOAD: "Subir"
STR_BOOK_S_STYLE: "Estilo del libro" STR_BOOK_S_STYLE: "Estilo del libro"
STR_EMBEDDED_STYLE: "Estilo integrado" STR_EMBEDDED_STYLE: "Estilo integrado"
STR_FOCUS_READING: "Lectura enfocada"
STR_OPDS_SERVER_URL: "URL del servidor OPDS" STR_OPDS_SERVER_URL: "URL del servidor OPDS"
STR_FOOTNOTES: "Pie de página" STR_FOOTNOTES: "Pie de página"
STR_NO_FOOTNOTES: "No hay notas al pie de esta página" STR_NO_FOOTNOTES: "No hay notas al pie de esta página"
+5 -3
View File
@@ -296,6 +296,7 @@ STR_SECTION_PREFIX: "Sektion"
STR_UPLOAD: "Uppladdning" STR_UPLOAD: "Uppladdning"
STR_BOOK_S_STYLE: "Bokstil" STR_BOOK_S_STYLE: "Bokstil"
STR_EMBEDDED_STYLE: "Inbäddad stil" STR_EMBEDDED_STYLE: "Inbäddad stil"
STR_FOCUS_READING: "Fokusläsning"
STR_OPDS_SERVER_URL: "OPDS-serveradress" STR_OPDS_SERVER_URL: "OPDS-serveradress"
STR_SET_SLEEP_COVER: "Ställ in omslag" STR_SET_SLEEP_COVER: "Ställ in omslag"
STR_FOOTNOTES: "Fotnoter" STR_FOOTNOTES: "Fotnoter"
@@ -310,8 +311,8 @@ STR_DELETE_CONFIRM: "Vill du ta bort den här servern?"
STR_OPDS_SERVERS: "OPDS-servrar" STR_OPDS_SERVERS: "OPDS-servrar"
STR_AUTO_TURN_ENABLED: "Automatisk vändning aktiverad: " STR_AUTO_TURN_ENABLED: "Automatisk vändning aktiverad: "
STR_AUTO_TURN_PAGES_PER_MIN: "Automatisk vändning (sidor per minut)" STR_AUTO_TURN_PAGES_PER_MIN: "Automatisk vändning (sidor per minut)"
STR_DOWNLOAD_FONTS: "Ladda ner teckensnitt" STR_MANAGE_FONTS: "Hantera teckensnitt"
STR_FONT_DOWNLOAD: "Nedladdning av teckensnitt" STR_FONT_BROWSER: "Teckensnittsbläddrare"
STR_LOADING_FONT_LIST: "Laddar teckensnittslista..." STR_LOADING_FONT_LIST: "Laddar teckensnittslista..."
STR_NO_FONTS_AVAILABLE: "Inga teckensnitt tillgängliga" STR_NO_FONTS_AVAILABLE: "Inga teckensnitt tillgängliga"
STR_FONT_INSTALLED: "Teckensnitt installerat!" STR_FONT_INSTALLED: "Teckensnitt installerat!"
@@ -322,7 +323,8 @@ STR_SD_CARD_FULL: "Otillräckligt utrymme på SD-kortet"
STR_FILES_LABEL: "Filer: " STR_FILES_LABEL: "Filer: "
STR_SIZE_LABEL: "Storlek: " STR_SIZE_LABEL: "Storlek: "
STR_REDOWNLOAD: "Ladda ner igen" STR_REDOWNLOAD: "Ladda ner igen"
STR_DOWNLOAD_ALL: "Ladda ner / Uppdatera alla" STR_DOWNLOAD_ALL: "Ladda ner alla"
STR_UPDATE_ALL: "Uppdatera alla"
STR_ALL_FONTS_INSTALLED: "Alla teckensnitt installerade!" STR_ALL_FONTS_INSTALLED: "Alla teckensnitt installerade!"
STR_UPDATE_AVAILABLE: "Uppdatering" STR_UPDATE_AVAILABLE: "Uppdatering"
STR_CRASH_TITLE: "Systemkrasch" STR_CRASH_TITLE: "Systemkrasch"
+1
View File
@@ -262,6 +262,7 @@ STR_SECTION_PREFIX: "Bölüm "
STR_UPLOAD: "Yükle" STR_UPLOAD: "Yükle"
STR_BOOK_S_STYLE: "Kitabın Stili" STR_BOOK_S_STYLE: "Kitabın Stili"
STR_EMBEDDED_STYLE: "Gömülü Stil" STR_EMBEDDED_STYLE: "Gömülü Stil"
STR_FOCUS_READING: "Odaklanmış Okuma"
STR_OPDS_SERVER_URL: "OPDS Sunucu Adresi" STR_OPDS_SERVER_URL: "OPDS Sunucu Adresi"
STR_AUTO_TURN_ENABLED: "Otomatik Çevirme Etkin: " STR_AUTO_TURN_ENABLED: "Otomatik Çevirme Etkin: "
STR_AUTO_TURN_PAGES_PER_MIN: "Otomatik Çevirme (Dakikada Sayfa)" STR_AUTO_TURN_PAGES_PER_MIN: "Otomatik Çevirme (Dakikada Sayfa)"
+36 -1
View File
@@ -87,7 +87,7 @@ STR_TIME_TO_SLEEP: "Перехід в режим сну"
STR_SHOW_HIDDEN_FILES: "Показати приховані файли" STR_SHOW_HIDDEN_FILES: "Показати приховані файли"
STR_REFRESH_FREQ: "Частота оновлення екрану" STR_REFRESH_FREQ: "Частота оновлення екрану"
STR_KOREADER_SYNC: "Синхронізація KOReader" STR_KOREADER_SYNC: "Синхронізація KOReader"
STR_CHECK_UPDATES: "Перевірити оновлення" STR_CHECK_UPDATES: "Перевірити оновлення системи"
STR_LANGUAGE: "Мова" STR_LANGUAGE: "Мова"
STR_CLEAR_READING_CACHE: "Очистити кеш книг" STR_CLEAR_READING_CACHE: "Очистити кеш книг"
STR_USERNAME: "Ім'я користувача" STR_USERNAME: "Ім'я користувача"
@@ -171,6 +171,7 @@ STR_NO_UPDATE: "Оновлень немає"
STR_UPDATE_FAILED: "Оновлення не вдалося" STR_UPDATE_FAILED: "Оновлення не вдалося"
STR_UPDATE_COMPLETE: "Оновлення завершено" STR_UPDATE_COMPLETE: "Оновлення завершено"
STR_POWER_ON_HINT: "Натисніть і утримуйте кнопку живлення, щоб увімкнути" STR_POWER_ON_HINT: "Натисніть і утримуйте кнопку живлення, щоб увімкнути"
STR_RESTARTING_HINT: "Перезавантаження... Якщо пристрій не перезавантажується, утримуйте кнопку живлення кілька секунд."
STR_NO_ENTRIES: "Записів не знайдено" STR_NO_ENTRIES: "Записів не знайдено"
STR_DOWNLOADING: "Завантаження..." STR_DOWNLOADING: "Завантаження..."
STR_DOWNLOAD_FAILED: "Завантаження не вдалося" STR_DOWNLOAD_FAILED: "Завантаження не вдалося"
@@ -228,6 +229,9 @@ STR_EXAMPLE_BOOK: "Назва книги"
STR_PREVIEW: "Перегляд" STR_PREVIEW: "Перегляд"
STR_TITLE: "Назва" STR_TITLE: "Назва"
STR_BATTERY: "Акумулятор" STR_BATTERY: "Акумулятор"
STR_XTC_STATUS_BAR: "XTC Рядок прогресу"
STR_BOTTOM: "Низ"
STR_TOP: "Верх"
STR_UI_THEME: "Тема інтерфейсу" STR_UI_THEME: "Тема інтерфейсу"
STR_THEME_CLASSIC: "Класична" STR_THEME_CLASSIC: "Класична"
STR_THEME_LYRA: "Lyra" STR_THEME_LYRA: "Lyra"
@@ -292,7 +296,9 @@ STR_SECTION_PREFIX: "Розділ "
STR_UPLOAD: "Завантажити" STR_UPLOAD: "Завантажити"
STR_BOOK_S_STYLE: "Стиль книги" STR_BOOK_S_STYLE: "Стиль книги"
STR_EMBEDDED_STYLE: "Вбудований стиль" STR_EMBEDDED_STYLE: "Вбудований стиль"
STR_FOCUS_READING: "Фокусне читання"
STR_OPDS_SERVER_URL: "URL сервера OPDS" STR_OPDS_SERVER_URL: "URL сервера OPDS"
STR_SET_SLEEP_COVER: "Як обкл."
STR_FOOTNOTES: "Примітки" STR_FOOTNOTES: "Примітки"
STR_NO_FOOTNOTES: "На цій сторінці немає приміток" STR_NO_FOOTNOTES: "На цій сторінці немає приміток"
STR_LINK: "[посилання]" STR_LINK: "[посилання]"
@@ -305,6 +311,22 @@ STR_DELETE_CONFIRM: "Видалити цей сервер?"
STR_OPDS_SERVERS: "Сервери OPDS" STR_OPDS_SERVERS: "Сервери OPDS"
STR_AUTO_TURN_ENABLED: "Автоперегортання увімк: " STR_AUTO_TURN_ENABLED: "Автоперегортання увімк: "
STR_AUTO_TURN_PAGES_PER_MIN: "Автоперегортання (ст/хв)" STR_AUTO_TURN_PAGES_PER_MIN: "Автоперегортання (ст/хв)"
STR_MANAGE_FONTS: "Керування шрифтами"
STR_FONT_BROWSER: "Перегляд шрифтів"
STR_LOADING_FONT_LIST: "Оновлення списку шрифтів..."
STR_NO_FONTS_AVAILABLE: "Шрифти недоступні"
STR_FONT_INSTALLED: "Шрифт встановлено!"
STR_FONT_INSTALL_FAILED: "Не вдалося встановити шрифт"
STR_INSTALLED: "Встановлено"
STR_CONFIRM_DOWNLOAD_PROMPT: "Завантажити?"
STR_SD_CARD_FULL: "Недостатньо місця на SD-карті"
STR_FILES_LABEL: "Файли: "
STR_SIZE_LABEL: "Розмір: "
STR_REDOWNLOAD: "Завантажити повторно"
STR_DOWNLOAD_ALL: "Завантажити все"
STR_UPDATE_ALL: "Оновити все"
STR_ALL_FONTS_INSTALLED: "Всі шрифти встановлено!"
STR_UPDATE_AVAILABLE: "Оновити"
STR_CRASH_TITLE: "Збій Системи" STR_CRASH_TITLE: "Збій Системи"
STR_CRASH_DESCRIPTION: "Дані про збій збережено в crash_report.txt. Додайте цей файл до вашого звіту про помилку." STR_CRASH_DESCRIPTION: "Дані про збій збережено в crash_report.txt. Додайте цей файл до вашого звіту про помилку."
STR_CRASH_REASON: "Причина збою:" STR_CRASH_REASON: "Причина збою:"
@@ -325,3 +347,16 @@ STR_KB_HINT_SECONDARY_CHAR: "Затисніть ВИБРАТИ для додат
STR_KB_HINT_UPPER_SECONDARY: "Затисніть ВИБРАТИ для ВЕЛИКИХ літер / символів" STR_KB_HINT_UPPER_SECONDARY: "Затисніть ВИБРАТИ для ВЕЛИКИХ літер / символів"
STR_KB_HINT_LOWER_SECONDARY: "Затисніть ВИБРАТИ для малих літер / символів" STR_KB_HINT_LOWER_SECONDARY: "Затисніть ВИБРАТИ для малих літер / символів"
STR_KB_HINT_URL_SNIPPETS: "Натисніть URL для вибору шаблонів" STR_KB_HINT_URL_SNIPPETS: "Натисніть URL для вибору шаблонів"
STR_SD_FIRMWARE_UPDATE: "Оновлення системи з SD-карти"
STR_SELECT_FIRMWARE_FILE: "Оберіть файл оновлення (.bin)"
STR_NO_BIN_FILES: "Не знайдено .bin файлів"
STR_VALIDATING_FIRMWARE: "Перевірка цілісності файлу..."
STR_INVALID_FIRMWARE: "Невірний файл оновлення системи"
STR_FIRMWARE_TOO_LARGE: "Файл оновлення системи не вміщується в розділ пам'яті"
STR_FIRMWARE_TOO_SMALL: "Файл оновлення системи занадто малий"
STR_FIRMWARE_UPDATE_PROMPT: "Оновити систему?"
STR_FIRMWARE_FILE_OPEN_FAILED: "Неможливо відкрити файл"
STR_FIRMWARE_WRITE_FAILED: "Помилка запису прошивки"
STR_FIRMWARE_UPDATE_DO_NOT_POWER_OFF: "Не вимикайте пристрій!"
STR_RECOVERY_MODE: "Режим відновлення"
STR_RECOVERY_MODE_HINT: "Помістіть firmware.bin у корінь SD-карти та виберіть його"
+12 -6
View File
@@ -1,18 +1,21 @@
import sys
import os import os
from PIL import Image
import cairosvg
import io import io
import sys
threshold = 128 threshold = 128
USAGE = 'Usage: python scripts/convert_icon.py input.png|input.svg output_name width height'
def svg_to_png_bytes(svg_path, width, height): def svg_to_png_bytes(svg_path, width, height):
import cairosvg
with open(svg_path, 'rb') as f: with open(svg_path, 'rb') as f:
svg_data = f.read() svg_data = f.read()
png_bytes = cairosvg.svg2png(bytestring=svg_data, output_width=width, output_height=height) png_bytes = cairosvg.svg2png(bytestring=svg_data, output_width=width, output_height=height)
return png_bytes return png_bytes
def load_image(path, width, height): def load_image(path, width, height):
from PIL import Image
ext = os.path.splitext(path)[1].lower() ext = os.path.splitext(path)[1].lower()
if ext == '.svg': if ext == '.svg':
png_bytes = svg_to_png_bytes(path, width, height) png_bytes = svg_to_png_bytes(path, width, height)
@@ -58,8 +61,11 @@ def image_to_c_array(img, array_name):
return c return c
def main(): def main():
if len(sys.argv) < 5: if any(arg in ('-h', '--help') for arg in sys.argv[1:]):
print('Usage: python convert_image.py input.png output_name width height') print(USAGE)
sys.exit(0)
if len(sys.argv) != 5:
print(USAGE)
sys.exit(1) sys.exit(1)
input_path, output_name, width, height = sys.argv[1:5] input_path, output_name, width, height = sys.argv[1:5]
array_name = output_name.capitalize() + 'Icon' array_name = output_name.capitalize() + 'Icon'
@@ -77,4 +83,4 @@ def main():
print(f'Wrote {output_path}') print(f'Wrote {output_path}')
if __name__ == '__main__': if __name__ == '__main__':
main() main()
+38 -28
View File
@@ -35,6 +35,43 @@ import threading
from collections import deque from collections import deque
from datetime import datetime from datetime import datetime
DEFAULT_BAUDRATE = 115200
def build_arg_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(
description="ESP32 Serial Monitor with Memory Graph - Real-time monitoring, graphing, and command interface"
)
parser.add_argument(
"port",
nargs="?",
default=None,
help="Serial port (leave empty for autodetection)",
)
parser.add_argument(
"--baud",
type=int,
default=DEFAULT_BAUDRATE,
help=f"Baud rate (default: {DEFAULT_BAUDRATE})",
)
parser.add_argument(
"--filter",
type=str,
default="",
help="Only display lines containing this keyword (case-insensitive)",
)
parser.add_argument(
"--suppress",
type=str,
default="",
help="Suppress lines containing this keyword (case-insensitive)",
)
return parser
if any(arg in ("-h", "--help") for arg in sys.argv[1:]):
build_arg_parser().parse_args()
# Try to import potentially missing packages # Try to import potentially missing packages
PACKAGE_MAPPING: dict[str, str] = { PACKAGE_MAPPING: dict[str, str] = {
"serial": "pyserial", "serial": "pyserial",
@@ -401,34 +438,7 @@ def main() -> None:
- Screenshot capture capability - Screenshot capture capability
- Graceful shutdown on Ctrl-C or window close - Graceful shutdown on Ctrl-C or window close
""" """
parser = argparse.ArgumentParser( parser = build_arg_parser()
description="ESP32 Serial Monitor with Memory Graph - Real-time monitoring, graphing, and command interface"
)
default_baudrate = 115200
parser.add_argument(
"port",
nargs="?",
default=None,
help="Serial port (leave empty for autodetection)",
)
parser.add_argument(
"--baud",
type=int,
default=default_baudrate,
help=f"Baud rate (default: {default_baudrate})",
)
parser.add_argument(
"--filter",
type=str,
default="",
help="Only display lines containing this keyword (case-insensitive)",
)
parser.add_argument(
"--suppress",
type=str,
default="",
help="Suppress lines containing this keyword (case-insensitive)",
)
args = parser.parse_args() args = parser.parse_args()
port = args.port port = args.port
if port is None: if port is None:
+2
View File
@@ -16,6 +16,8 @@ The input directory may be flat (all .cpfont files in one dir) or nested
convention <FamilyName>_<size>.cpfont. convention <FamilyName>_<size>.cpfont.
""" """
from __future__ import annotations
import argparse import argparse
import json import json
import os import os
@@ -137,10 +137,15 @@ Also includes:
import io import io
import os import os
import sys
import zipfile import zipfile
import uuid import uuid
from datetime import datetime from datetime import datetime
if any(arg in ("-h", "--help") for arg in sys.argv[1:]):
print(__doc__.strip())
sys.exit(0)
try: try:
from PIL import Image, ImageDraw, ImageFont from PIL import Image, ImageDraw, ImageFont
except ImportError: except ImportError:
+3
View File
@@ -326,4 +326,7 @@ def main():
if __name__ == '__main__': if __name__ == '__main__':
if any(arg in ("-h", "--help") for arg in sys.argv[1:]):
print(__doc__.strip())
sys.exit(0)
main() main()
+5
View File
@@ -11,9 +11,14 @@ Creates EPUBs with annotated JPEG and PNG images to verify:
""" """
import os import os
import sys
import zipfile import zipfile
from pathlib import Path from pathlib import Path
if any(arg in ("-h", "--help") for arg in sys.argv[1:]):
print(__doc__.strip())
sys.exit(0)
try: try:
from PIL import Image, ImageDraw, ImageFont from PIL import Image, ImageDraw, ImageFont
except ImportError: except ImportError:
+2
View File
@@ -213,6 +213,8 @@ class CrossPointSettings {
uint8_t fadingFix = 0; uint8_t fadingFix = 0;
// Use book's embedded CSS styles for EPUB rendering (1 = enabled, 0 = disabled) // Use book's embedded CSS styles for EPUB rendering (1 = enabled, 0 = disabled)
uint8_t embeddedStyle = 1; uint8_t embeddedStyle = 1;
// Focus Reading - emphasizes the first part of words with bold
uint8_t focusReadingEnabled = 0;
// SD card font family name (empty = use built-in fontFamily) // SD card font family name (empty = use built-in fontFamily)
char sdFontFamilyName[32] = ""; char sdFontFamilyName[32] = "";
// Show hidden files/directories (starting with '.') in the file browser (0 = hidden, 1 = show) // Show hidden files/directories (starting with '.') in the file browser (0 = hidden, 1 = show)
+14 -17
View File
@@ -5,15 +5,10 @@
#include "CrossPointSettings.h" #include "CrossPointSettings.h"
// Map fontSize enum (SMALL=0, MEDIUM=1, LARGE=2, EXTRA_LARGE=3) to the point static uint8_t fontSizeEnumFromSettings() {
// sizes shipped with the built-in fonts. Used to drive closest-pt selection
// in the SD card font registry (see SdCardFontFamilyInfo::pickClosestSize).
static constexpr uint8_t FONT_SIZE_TO_PT[CrossPointSettings::FONT_SIZE_COUNT] = {12, 14, 16, 18};
static uint8_t targetPtSizeFromSettings() {
uint8_t e = SETTINGS.fontSize; uint8_t e = SETTINGS.fontSize;
if (e >= CrossPointSettings::FONT_SIZE_COUNT) e = 1; // default to MEDIUM if (e >= CrossPointSettings::FONT_SIZE_COUNT) e = 1; // default to MEDIUM
return FONT_SIZE_TO_PT[e]; return e;
} }
void SdCardFontSystem::begin(GfxRenderer& renderer) { void SdCardFontSystem::begin(GfxRenderer& renderer) {
@@ -30,7 +25,7 @@ void SdCardFontSystem::begin(GfxRenderer& renderer) {
if (SETTINGS.sdFontFamilyName[0] != '\0') { if (SETTINGS.sdFontFamilyName[0] != '\0') {
const auto* family = registry_.findFamily(SETTINGS.sdFontFamilyName); const auto* family = registry_.findFamily(SETTINGS.sdFontFamilyName);
if (family) { if (family) {
if (manager_.loadFamily(*family, renderer, targetPtSizeFromSettings())) { if (manager_.loadFamily(*family, renderer, fontSizeEnumFromSettings())) {
LOG_DBG("SDFS", "Loaded SD card font family: %s", SETTINGS.sdFontFamilyName); LOG_DBG("SDFS", "Loaded SD card font family: %s", SETTINGS.sdFontFamilyName);
} else { } else {
LOG_ERR("SDFS", "Failed to load SD font family: %s (clearing)", SETTINGS.sdFontFamilyName); LOG_ERR("SDFS", "Failed to load SD font family: %s (clearing)", SETTINGS.sdFontFamilyName);
@@ -58,7 +53,7 @@ void SdCardFontSystem::ensureLoaded(GfxRenderer& renderer) {
const char* wantedFamily = SETTINGS.sdFontFamilyName; const char* wantedFamily = SETTINGS.sdFontFamilyName;
const std::string& currentFamily = manager_.currentFamilyName(); const std::string& currentFamily = manager_.currentFamilyName();
const uint8_t targetPt = targetPtSizeFromSettings(); const uint8_t sizeEnum = fontSizeEnumFromSettings();
if (wantedFamily[0] == '\0') { if (wantedFamily[0] == '\0') {
if (!currentFamily.empty()) { if (!currentFamily.empty()) {
@@ -67,8 +62,8 @@ void SdCardFontSystem::ensureLoaded(GfxRenderer& renderer) {
return; return;
} }
// Reload if family changed OR if the user-selected size now resolves to a // Reload if family changed OR if the user-selected size maps to a
// different on-disk file than what's currently loaded OR if the registry was // different file than what's currently loaded OR if the registry was
// just rediscovered (file may have been replaced on disk). // just rediscovered (file may have been replaced on disk).
bool familyMatches = (currentFamily == wantedFamily); bool familyMatches = (currentFamily == wantedFamily);
if (familyMatches) { if (familyMatches) {
@@ -79,11 +74,13 @@ void SdCardFontSystem::ensureLoaded(GfxRenderer& renderer) {
SETTINGS.sdFontFamilyName[0] = '\0'; SETTINGS.sdFontFamilyName[0] = '\0';
return; return;
} }
const auto* best = family->pickClosestSize(targetPt); auto sizes = family->availableSizes();
const uint8_t bestPt = best ? best->pointSize : 0; uint8_t idx = sizeEnum;
if (!registryWasDirty && bestPt == manager_.currentPointSize()) return; if (idx >= sizes.size()) idx = sizes.size() - 1;
LOG_DBG("SDFS", "Reloading %s: size %u -> %u (target %u)%s", wantedFamily, manager_.currentPointSize(), bestPt, uint8_t wantedPt = sizes.empty() ? 0 : sizes[idx];
targetPt, registryWasDirty ? " [registry dirty]" : ""); if (!registryWasDirty && wantedPt == manager_.currentPointSize()) return;
LOG_DBG("SDFS", "Reloading %s: size %u -> %u (enum %u)%s", wantedFamily, manager_.currentPointSize(), wantedPt,
sizeEnum, registryWasDirty ? " [registry dirty]" : "");
} }
if (!currentFamily.empty()) { if (!currentFamily.empty()) {
@@ -92,7 +89,7 @@ void SdCardFontSystem::ensureLoaded(GfxRenderer& renderer) {
const auto* family = registry_.findFamily(wantedFamily); const auto* family = registry_.findFamily(wantedFamily);
if (family) { if (family) {
if (manager_.loadFamily(*family, renderer, targetPt)) { if (manager_.loadFamily(*family, renderer, sizeEnum)) {
LOG_DBG("SDFS", "Loaded SD font family: %s", wantedFamily); LOG_DBG("SDFS", "Loaded SD font family: %s", wantedFamily);
} else { } else {
LOG_ERR("SDFS", "Failed to load SD font family: %s (clearing)", wantedFamily); LOG_ERR("SDFS", "Failed to load SD font family: %s (clearing)", wantedFamily);
+2
View File
@@ -145,6 +145,8 @@ inline std::vector<SettingInfo> getSettingsList(const SdCardFontRegistry* regist
"paragraphAlignment", StrId::STR_CAT_READER), "paragraphAlignment", StrId::STR_CAT_READER),
SettingInfo::Toggle(StrId::STR_EMBEDDED_STYLE, &CrossPointSettings::embeddedStyle, "embeddedStyle", SettingInfo::Toggle(StrId::STR_EMBEDDED_STYLE, &CrossPointSettings::embeddedStyle, "embeddedStyle",
StrId::STR_CAT_READER), StrId::STR_CAT_READER),
SettingInfo::Toggle(StrId::STR_FOCUS_READING, &CrossPointSettings::focusReadingEnabled, "focusReadingEnabled",
StrId::STR_CAT_READER),
SettingInfo::Toggle(StrId::STR_HYPHENATION, &CrossPointSettings::hyphenationEnabled, "hyphenationEnabled", SettingInfo::Toggle(StrId::STR_HYPHENATION, &CrossPointSettings::hyphenationEnabled, "hyphenationEnabled",
StrId::STR_CAT_READER), StrId::STR_CAT_READER),
SettingInfo::Enum(StrId::STR_ORIENTATION, &CrossPointSettings::orientation, SettingInfo::Enum(StrId::STR_ORIENTATION, &CrossPointSettings::orientation,
+4 -4
View File
@@ -609,7 +609,7 @@ void EpubReaderActivity::render(RenderLock&& lock) {
if (!section->loadSectionFile(SETTINGS.getReaderFontId(), SETTINGS.getReaderLineCompression(), if (!section->loadSectionFile(SETTINGS.getReaderFontId(), SETTINGS.getReaderLineCompression(),
SETTINGS.extraParagraphSpacing, SETTINGS.paragraphAlignment, viewportWidth, SETTINGS.extraParagraphSpacing, SETTINGS.paragraphAlignment, viewportWidth,
viewportHeight, SETTINGS.hyphenationEnabled, SETTINGS.embeddedStyle, viewportHeight, SETTINGS.hyphenationEnabled, SETTINGS.embeddedStyle,
SETTINGS.imageRendering)) { SETTINGS.imageRendering, SETTINGS.focusReadingEnabled)) {
LOG_DBG("ERS", "Cache not found, building..."); LOG_DBG("ERS", "Cache not found, building...");
GUI.drawPopup(renderer, tr(STR_INDEXING)); GUI.drawPopup(renderer, tr(STR_INDEXING));
@@ -619,7 +619,7 @@ void EpubReaderActivity::render(RenderLock&& lock) {
if (!section->createSectionFile(SETTINGS.getReaderFontId(), SETTINGS.getReaderLineCompression(), if (!section->createSectionFile(SETTINGS.getReaderFontId(), SETTINGS.getReaderLineCompression(),
SETTINGS.extraParagraphSpacing, SETTINGS.paragraphAlignment, viewportWidth, SETTINGS.extraParagraphSpacing, SETTINGS.paragraphAlignment, viewportWidth,
viewportHeight, SETTINGS.hyphenationEnabled, SETTINGS.embeddedStyle, viewportHeight, SETTINGS.hyphenationEnabled, SETTINGS.embeddedStyle,
SETTINGS.imageRendering, popupFn)) { SETTINGS.imageRendering, SETTINGS.focusReadingEnabled, popupFn)) {
LOG_ERR("ERS", "Failed to persist page data to SD"); LOG_ERR("ERS", "Failed to persist page data to SD");
section.reset(); section.reset();
showPendingSyncSaveError(); showPendingSyncSaveError();
@@ -750,7 +750,7 @@ void EpubReaderActivity::silentIndexNextChapterIfNeeded(const uint16_t viewportW
if (nextSection.loadSectionFile(SETTINGS.getReaderFontId(), SETTINGS.getReaderLineCompression(), if (nextSection.loadSectionFile(SETTINGS.getReaderFontId(), SETTINGS.getReaderLineCompression(),
SETTINGS.extraParagraphSpacing, SETTINGS.paragraphAlignment, viewportWidth, SETTINGS.extraParagraphSpacing, SETTINGS.paragraphAlignment, viewportWidth,
viewportHeight, SETTINGS.hyphenationEnabled, SETTINGS.embeddedStyle, viewportHeight, SETTINGS.hyphenationEnabled, SETTINGS.embeddedStyle,
SETTINGS.imageRendering)) { SETTINGS.imageRendering, SETTINGS.focusReadingEnabled)) {
return; return;
} }
@@ -758,7 +758,7 @@ void EpubReaderActivity::silentIndexNextChapterIfNeeded(const uint16_t viewportW
if (!nextSection.createSectionFile(SETTINGS.getReaderFontId(), SETTINGS.getReaderLineCompression(), if (!nextSection.createSectionFile(SETTINGS.getReaderFontId(), SETTINGS.getReaderLineCompression(),
SETTINGS.extraParagraphSpacing, SETTINGS.paragraphAlignment, viewportWidth, SETTINGS.extraParagraphSpacing, SETTINGS.paragraphAlignment, viewportWidth,
viewportHeight, SETTINGS.hyphenationEnabled, SETTINGS.embeddedStyle, viewportHeight, SETTINGS.hyphenationEnabled, SETTINGS.embeddedStyle,
SETTINGS.imageRendering)) { SETTINGS.imageRendering, SETTINGS.focusReadingEnabled)) {
LOG_ERR("ERS", "Failed silent indexing for chapter: %d", nextSpineIndex); LOG_ERR("ERS", "Failed silent indexing for chapter: %d", nextSpineIndex);
} }
} }
+16 -4
View File
@@ -192,6 +192,17 @@ bool TxtReaderActivity::loadPageAtOffset(size_t offset, std::vector<std::string>
} }
buffer[chunkSize] = '\0'; buffer[chunkSize] = '\0';
// Prime the SD card font's advance table with this chunk's codepoints.
// Without this, every getTextAdvanceX() call in the wrap loop below triggers
// on-demand glyph loads through the 8-slot overflow ring buffer, which
// thrashes for any text with more than 8 unique chars (i.e. all English),
// floods the heap with short-lived bitmap allocations, and eventually
// corrupts FreeRTOS state. The advance table persists across calls per
// font, so the cost amortizes to ~ASCII-size after the first chunk.
if (renderer.isSdCardFont(cachedFontId)) {
renderer.ensureSdCardFontReady(cachedFontId, reinterpret_cast<const char*>(buffer), /*styleMask=*/0x01);
}
// Parse lines from buffer // Parse lines from buffer
size_t pos = 0; size_t pos = 0;
@@ -231,7 +242,7 @@ bool TxtReaderActivity::loadPageAtOffset(size_t offset, std::vector<std::string>
break; break;
} }
int lineWidth = renderer.getTextWidth(cachedFontId, line.c_str()); int lineWidth = renderer.getTextAdvanceX(cachedFontId, line.c_str(), EpdFontFamily::REGULAR);
if (lineWidth <= viewportWidth) { if (lineWidth <= viewportWidth) {
outLines.push_back(line); outLines.push_back(line);
@@ -242,7 +253,8 @@ bool TxtReaderActivity::loadPageAtOffset(size_t offset, std::vector<std::string>
// Find break point // Find break point
size_t breakPos = line.length(); size_t breakPos = line.length();
while (breakPos > 0 && renderer.getTextWidth(cachedFontId, line.substr(0, breakPos).c_str()) > viewportWidth) { while (breakPos > 0 && renderer.getTextAdvanceX(cachedFontId, line.substr(0, breakPos).c_str(),
EpdFontFamily::REGULAR) > viewportWidth) {
// Try to break at space // Try to break at space
size_t spacePos = line.rfind(' ', breakPos - 1); size_t spacePos = line.rfind(' ', breakPos - 1);
if (spacePos != std::string::npos && spacePos > 0) { if (spacePos != std::string::npos && spacePos > 0) {
@@ -354,12 +366,12 @@ void TxtReaderActivity::renderPage() {
// x already set to left margin // x already set to left margin
break; break;
case CrossPointSettings::CENTER_ALIGN: { case CrossPointSettings::CENTER_ALIGN: {
int textWidth = renderer.getTextWidth(cachedFontId, line.c_str()); int textWidth = renderer.getTextAdvanceX(cachedFontId, line.c_str(), EpdFontFamily::REGULAR);
x = cachedOrientedMarginLeft + (contentWidth - textWidth) / 2; x = cachedOrientedMarginLeft + (contentWidth - textWidth) / 2;
break; break;
} }
case CrossPointSettings::RIGHT_ALIGN: { case CrossPointSettings::RIGHT_ALIGN: {
int textWidth = renderer.getTextWidth(cachedFontId, line.c_str()); int textWidth = renderer.getTextAdvanceX(cachedFontId, line.c_str(), EpdFontFamily::REGULAR);
x = cachedOrientedMarginLeft + contentWidth - textWidth; x = cachedOrientedMarginLeft + contentWidth - textWidth;
break; break;
} }
+118 -17
View File
@@ -11,6 +11,7 @@
#include "MappedInputManager.h" #include "MappedInputManager.h"
#include "SdCardFontGlobals.h" #include "SdCardFontGlobals.h"
#include "activities/network/WifiSelectionActivity.h" #include "activities/network/WifiSelectionActivity.h"
#include "activities/util/ConfirmationActivity.h"
#include "components/UITheme.h" #include "components/UITheme.h"
#include "fontIds.h" #include "fontIds.h"
#include "network/HttpDownloader.h" #include "network/HttpDownloader.h"
@@ -106,6 +107,7 @@ bool FontDownloadActivity::fetchAndParseManifest() {
baseUrl_ = doc["baseUrl"] | ""; baseUrl_ = doc["baseUrl"] | "";
families_.clear(); families_.clear();
fontInstaller_.refreshRegistry();
JsonArray familiesArr = doc["families"].as<JsonArray>(); JsonArray familiesArr = doc["families"].as<JsonArray>();
families_.reserve(familiesArr.size()); families_.reserve(familiesArr.size());
@@ -171,7 +173,7 @@ bool FontDownloadActivity::fetchAndParseManifest() {
void FontDownloadActivity::downloadAll() { void FontDownloadActivity::downloadAll() {
for (size_t i = 0; i < families_.size(); i++) { for (size_t i = 0; i < families_.size(); i++) {
if (families_[i].installed && !families_[i].hasUpdate) continue; if (families_[i].installed) continue;
downloadFamily(families_[i]); downloadFamily(families_[i]);
if (state_ == ERROR) return; if (state_ == ERROR) return;
} }
@@ -182,10 +184,59 @@ void FontDownloadActivity::downloadAll() {
} }
} }
size_t FontDownloadActivity::totalUninstalledSize() const { void FontDownloadActivity::updateAll() {
for (size_t i = 0; i < families_.size(); i++) {
if (!families_[i].hasUpdate) continue;
downloadFamily(families_[i]);
if (state_ == ERROR) return;
}
{
RenderLock lock(*this);
state_ = COMPLETE;
}
}
bool FontDownloadActivity::showDownloadAllRow() const {
for (const auto& f : families_) {
if (!f.installed) return true;
}
return false;
}
bool FontDownloadActivity::showUpdateAllRow() const {
for (const auto& f : families_) {
if (f.hasUpdate) return true;
}
return false;
}
int FontDownloadActivity::specialRowCount() const {
return (showDownloadAllRow() ? 1 : 0) + (showUpdateAllRow() ? 1 : 0);
}
bool FontDownloadActivity::isDownloadAllRow(int index) const { return showDownloadAllRow() && index == 0; }
bool FontDownloadActivity::isUpdateAllRow(int index) const {
return showUpdateAllRow() && index == (showDownloadAllRow() ? 1 : 0);
}
int FontDownloadActivity::listItemCount() const {
return families_.empty() ? 0 : static_cast<int>(families_.size()) + specialRowCount();
}
size_t FontDownloadActivity::totalDownloadSize() const {
size_t total = 0; size_t total = 0;
for (const auto& f : families_) { for (const auto& f : families_) {
if (!f.installed || f.hasUpdate) total += f.totalSize; if (!f.installed) total += f.totalSize;
}
return total;
}
size_t FontDownloadActivity::totalUpdateSize() const {
size_t total = 0;
for (const auto& f : families_) {
if (f.hasUpdate) total += f.totalSize;
} }
return total; return total;
} }
@@ -297,6 +348,7 @@ void FontDownloadActivity::downloadFamily(ManifestFamily& family) {
fontInstaller_.refreshRegistry(); fontInstaller_.refreshRegistry();
family.installed = true; family.installed = true;
family.hasUpdate = false;
{ {
RenderLock lock(*this); RenderLock lock(*this);
@@ -304,6 +356,47 @@ void FontDownloadActivity::downloadFamily(ManifestFamily& family) {
} }
} }
void FontDownloadActivity::promptDeleteSelectedFamily() {
const int pendingDeleteFamilyIndex = familyIndexFromList(selectedIndex_);
if (pendingDeleteFamilyIndex < 0 || pendingDeleteFamilyIndex >= static_cast<int>(families_.size())) {
return;
}
std::string heading = tr(STR_DELETE);
const auto& family = families_[pendingDeleteFamilyIndex];
std::string body = family.name;
startActivityForResult(std::make_unique<ConfirmationActivity>(renderer, mappedInput, heading, body),
[this](const ActivityResult& result) { onDeleteConfirmationResult(result); });
}
void FontDownloadActivity::onDeleteConfirmationResult(const ActivityResult& result) {
if (result.isCancelled) {
requestUpdate();
return;
}
auto& family = families_[familyIndexFromList(selectedIndex_)];
if (fontInstaller_.deleteFamily(family.name.c_str()) != FontInstaller::Error::OK) {
RenderLock lock(*this);
state_ = ERROR;
errorMessage_ = "Failed to delete font";
} else {
fontInstaller_.refreshRegistry();
family.installed = false;
family.hasUpdate = false;
}
requestUpdate();
}
bool FontDownloadActivity::isSelectedFamilyDeletable() const {
if (isDownloadAllRow(selectedIndex_) || isUpdateAllRow(selectedIndex_)) return false;
if (selectedIndex_ < specialRowCount() || selectedIndex_ >= listItemCount()) return false;
const auto& family = families_[familyIndexFromList(selectedIndex_)];
return family.installed && !family.hasUpdate;
}
// --- Input handling --- // --- Input handling ---
void FontDownloadActivity::loop() { void FontDownloadActivity::loop() {
@@ -338,12 +431,17 @@ void FontDownloadActivity::loop() {
if (mappedInput.wasPressed(MappedInputManager::Button::Confirm)) { if (mappedInput.wasPressed(MappedInputManager::Button::Confirm)) {
if (!families_.empty()) { if (!families_.empty()) {
if (isDownloadAllSelected()) { if (isDownloadAllRow(selectedIndex_)) {
downloadAll(); downloadAll();
} else if (isUpdateAllRow(selectedIndex_)) {
updateAll();
} else { } else {
const auto& family = families_[familyIndexFromList(selectedIndex_)]; auto& family = families_[familyIndexFromList(selectedIndex_)];
if (!family.installed || family.hasUpdate) { if (!family.installed || family.hasUpdate) {
downloadFamily(families_[familyIndexFromList(selectedIndex_)]); downloadFamily(family);
} else {
promptDeleteSelectedFamily();
return;
} }
} }
requestUpdateAndWait(); requestUpdateAndWait();
@@ -403,7 +501,7 @@ void FontDownloadActivity::render(RenderLock&&) {
renderer.clearScreen(); renderer.clearScreen();
GUI.drawHeader(renderer, Rect{0, metrics.topPadding, pageWidth, metrics.headerHeight}, tr(STR_FONT_DOWNLOAD)); GUI.drawHeader(renderer, Rect{0, metrics.topPadding, pageWidth, metrics.headerHeight}, tr(STR_FONT_BROWSER));
const auto lineHeight = renderer.getLineHeight(UI_10_FONT_ID); const auto lineHeight = renderer.getLineHeight(UI_10_FONT_ID);
const auto contentTop = metrics.topPadding + metrics.headerHeight + metrics.verticalSpacing; const auto contentTop = metrics.topPadding + metrics.headerHeight + metrics.verticalSpacing;
@@ -422,18 +520,21 @@ void FontDownloadActivity::render(RenderLock&&) {
Rect{0, contentTop, pageWidth, pageHeight - contentTop - metrics.buttonHintsHeight - metrics.verticalSpacing}, Rect{0, contentTop, pageWidth, pageHeight - contentTop - metrics.buttonHintsHeight - metrics.verticalSpacing},
listItemCount(), selectedIndex_, listItemCount(), selectedIndex_,
[this](int index) -> std::string { [this](int index) -> std::string {
if (index == 0) { if (isDownloadAllRow(index)) {
return std::string(tr(STR_DOWNLOAD_ALL)) + " (" + formatSize(totalUninstalledSize()) + ")"; return std::string(tr(STR_DOWNLOAD_ALL)) + " (" + formatSize(totalDownloadSize()) + ")";
}
if (isUpdateAllRow(index)) {
return std::string(tr(STR_UPDATE_ALL)) + " (" + formatSize(totalUpdateSize()) + ")";
} }
return families_[familyIndexFromList(index)].name; return families_[familyIndexFromList(index)].name;
}, },
[this](int index) -> std::string { [this](int index) -> std::string {
if (index == 0) return ""; if (isDownloadAllRow(index) || isUpdateAllRow(index)) return "";
return families_[familyIndexFromList(index)].description; return families_[familyIndexFromList(index)].description;
}, },
nullptr, nullptr,
[this](int index) -> std::string { [this](int index) -> std::string {
if (index == 0) return ""; if (isDownloadAllRow(index) || isUpdateAllRow(index)) return "";
const auto& f = families_[familyIndexFromList(index)]; const auto& f = families_[familyIndexFromList(index)];
if (f.hasUpdate) return tr(STR_UPDATE_AVAILABLE); if (f.hasUpdate) return tr(STR_UPDATE_AVAILABLE);
if (f.installed) return tr(STR_INSTALLED); if (f.installed) return tr(STR_INSTALLED);
@@ -441,12 +542,16 @@ void FontDownloadActivity::render(RenderLock&&) {
}, },
true, true,
[this](int index) -> bool { [this](int index) -> bool {
if (index == 0) return false; if (isDownloadAllRow(index) || isUpdateAllRow(index)) return false;
const auto& f = families_[familyIndexFromList(index)]; const auto& f = families_[familyIndexFromList(index)];
return f.installed && !f.hasUpdate; return f.installed && !f.hasUpdate;
}); });
const auto labels = mappedInput.mapLabels(tr(STR_BACK), tr(STR_DOWNLOAD), tr(STR_DIR_UP), tr(STR_DIR_DOWN)); const auto labels = mappedInput.mapLabels(tr(STR_BACK),
isSelectedFamilyDeletable() ? tr(STR_DELETE)
: isUpdateAllRow(selectedIndex_) ? tr(STR_UPDATE)
: tr(STR_DOWNLOAD),
tr(STR_DIR_UP), tr(STR_DIR_DOWN));
GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4); GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
} }
} else if (state_ == DOWNLOADING) { } else if (state_ == DOWNLOADING) {
@@ -466,10 +571,6 @@ void FontDownloadActivity::render(RenderLock&&) {
renderer, renderer,
Rect{metrics.contentSidePadding, barY, pageWidth - metrics.contentSidePadding * 2, metrics.progressBarHeight}, Rect{metrics.contentSidePadding, barY, pageWidth - metrics.contentSidePadding * 2, metrics.progressBarHeight},
static_cast<int>(progress * 100), 100); static_cast<int>(progress * 100), 100);
int percentY = barY + metrics.progressBarHeight + metrics.verticalSpacing;
renderer.drawCenteredText(UI_10_FONT_ID, percentY,
(std::to_string(static_cast<int>(progress * 100)) + "%").c_str());
} else if (state_ == COMPLETE) { } else if (state_ == COMPLETE) {
renderer.drawCenteredText(UI_10_FONT_ID, centerY, tr(STR_FONT_INSTALLED), true, EpdFontFamily::BOLD); renderer.drawCenteredText(UI_10_FONT_ID, centerY, tr(STR_FONT_INSTALLED), true, EpdFontFamily::BOLD);
const auto labels = mappedInput.mapLabels(tr(STR_BACK), "", "", ""); const auto labels = mappedInput.mapLabels(tr(STR_BACK), "", "", "");
+13 -4
View File
@@ -84,10 +84,19 @@ class FontDownloadActivity : public Activity {
bool fetchAndParseManifest(); bool fetchAndParseManifest();
void downloadFamily(ManifestFamily& family); void downloadFamily(ManifestFamily& family);
void downloadAll(); void downloadAll();
void updateAll();
static bool computeFileCrc32(const char* path, uint32_t& outCrc); static bool computeFileCrc32(const char* path, uint32_t& outCrc);
bool isDownloadAllSelected() const { return selectedIndex_ == 0 && !families_.empty(); } bool showDownloadAllRow() const;
int familyIndexFromList(int listIndex) const { return listIndex - 1; } bool showUpdateAllRow() const;
int listItemCount() const { return families_.empty() ? 0 : static_cast<int>(families_.size()) + 1; } int specialRowCount() const;
size_t totalUninstalledSize() const; bool isDownloadAllRow(int index) const;
bool isUpdateAllRow(int index) const;
bool isSelectedFamilyDeletable() const;
void promptDeleteSelectedFamily();
void onDeleteConfirmationResult(const ActivityResult& result);
int familyIndexFromList(int listIndex) const { return listIndex - specialRowCount(); }
int listItemCount() const;
size_t totalDownloadSize() const;
size_t totalUpdateSize() const;
static std::string formatSize(size_t bytes); static std::string formatSize(size_t bytes);
}; };
@@ -116,8 +116,8 @@ void OtaUpdateActivity::render(RenderLock&&) {
static_cast<int>(updaterProgress * 100), 100); static_cast<int>(updaterProgress * 100), 100);
y += metrics.progressBarHeight + metrics.verticalSpacing; y += metrics.progressBarHeight + metrics.verticalSpacing;
renderer.drawCenteredText(UI_10_FONT_ID, y, // Percent label is drawn by BaseTheme::drawProgressBar; this slot is left intentionally empty
(std::to_string(static_cast<int>(updaterProgress * 100)) + "%").c_str()); // so the bytes line below stays at the same Y it was at when the activity drew its own percent.
y += height + metrics.verticalSpacing; y += height + metrics.verticalSpacing;
renderer.drawCenteredText( renderer.drawCenteredText(
UI_10_FONT_ID, y, UI_10_FONT_ID, y,
@@ -230,7 +230,8 @@ void SdFirmwareUpdateActivity::render(RenderLock&&) {
Rect{metrics.contentSidePadding, y, pageWidth - metrics.contentSidePadding * 2, metrics.progressBarHeight}, Rect{metrics.contentSidePadding, y, pageWidth - metrics.contentSidePadding * 2, metrics.progressBarHeight},
static_cast<int>(pct), 100); static_cast<int>(pct), 100);
y += metrics.progressBarHeight + metrics.verticalSpacing; y += metrics.progressBarHeight + metrics.verticalSpacing;
renderer.drawCenteredText(UI_10_FONT_ID, y, (std::to_string(pct) + "%").c_str()); // Percent label is drawn by BaseTheme::drawProgressBar; this slot is left intentionally empty
// so the do-not-power-off line below stays at the same Y as before.
y += lineHeight + metrics.verticalSpacing; y += lineHeight + metrics.verticalSpacing;
renderer.drawCenteredText(UI_10_FONT_ID, y, tr(STR_FIRMWARE_UPDATE_DO_NOT_POWER_OFF)); renderer.drawCenteredText(UI_10_FONT_ID, y, tr(STR_FIRMWARE_UPDATE_DO_NOT_POWER_OFF));
} else if (state == State::SUCCESS) { } else if (state == State::SUCCESS) {
+2 -2
View File
@@ -57,9 +57,9 @@ void SettingsActivity::rebuildSettingsLists() {
systemSettings.push_back(SettingInfo::Action(StrId::STR_CHECK_UPDATES, SettingAction::CheckForUpdates)); systemSettings.push_back(SettingInfo::Action(StrId::STR_CHECK_UPDATES, SettingAction::CheckForUpdates));
systemSettings.push_back(SettingInfo::Action(StrId::STR_SD_FIRMWARE_UPDATE, SettingAction::SdFirmwareUpdate)); systemSettings.push_back(SettingInfo::Action(StrId::STR_SD_FIRMWARE_UPDATE, SettingAction::SdFirmwareUpdate));
systemSettings.push_back(SettingInfo::Action(StrId::STR_LANGUAGE, SettingAction::Language)); systemSettings.push_back(SettingInfo::Action(StrId::STR_LANGUAGE, SettingAction::Language));
// Insert "Download Fonts" right after the font family setting so users discover it naturally // Insert "Manage Fonts" right after the font family setting so users discover it naturally
readerSettings.insert(readerSettings.begin() + 1, readerSettings.insert(readerSettings.begin() + 1,
SettingInfo::Action(StrId::STR_DOWNLOAD_FONTS, SettingAction::DownloadFonts)); SettingInfo::Action(StrId::STR_MANAGE_FONTS, SettingAction::DownloadFonts));
readerSettings.push_back(SettingInfo::Action(StrId::STR_CUSTOMISE_STATUS_BAR, SettingAction::CustomiseStatusBar)); readerSettings.push_back(SettingInfo::Action(StrId::STR_CUSTOMISE_STATUS_BAR, SettingAction::CustomiseStatusBar));
// Update currentSettings pointer and count for the active category // Update currentSettings pointer and count for the active category
+10 -3
View File
@@ -97,7 +97,12 @@ void BmpViewerActivity::onEnter() {
} }
// 4. Prepare Rendering // 4. Prepare Rendering
const auto labels = mappedInput.mapLabels(tr(STR_BACK), tr(STR_SET_SLEEP_COVER), "", ""); bool hasPrevious = (siblingImages.size() > 1 && currentImageIndex > 0);
bool hasNext = (siblingImages.size() > 1 && currentImageIndex != -1 &&
currentImageIndex < static_cast<int>(siblingImages.size()) - 1);
const auto labels =
mappedInput.mapLabels(tr(STR_BACK), tr(STR_SET_SLEEP_COVER), (hasPrevious ? "<" : ""), (hasNext ? ">" : ""));
GUI.fillPopupProgress(renderer, popupRect, 50); GUI.fillPopupProgress(renderer, popupRect, 50);
@@ -185,7 +190,8 @@ void BmpViewerActivity::loop() {
return; return;
} }
if (mappedInput.wasReleased(MappedInputManager::Button::Up)) { if (mappedInput.wasReleased(MappedInputManager::Button::Left) ||
mappedInput.wasReleased(MappedInputManager::Button::Up)) {
if (siblingImages.size() > 1 && currentImageIndex > 0) { if (siblingImages.size() > 1 && currentImageIndex > 0) {
currentImageIndex--; currentImageIndex--;
std::string dirPath = FsHelpers::extractFolderPath(filePath); std::string dirPath = FsHelpers::extractFolderPath(filePath);
@@ -196,7 +202,8 @@ void BmpViewerActivity::loop() {
return; return;
} }
if (mappedInput.wasReleased(MappedInputManager::Button::Down)) { if (mappedInput.wasReleased(MappedInputManager::Button::Right) ||
mappedInput.wasReleased(MappedInputManager::Button::Down)) {
if (siblingImages.size() > 1 && currentImageIndex != -1 && if (siblingImages.size() > 1 && currentImageIndex != -1 &&
currentImageIndex < static_cast<int>(siblingImages.size()) - 1) { currentImageIndex < static_cast<int>(siblingImages.size()) - 1) {
currentImageIndex++; currentImageIndex++;
@@ -5,7 +5,6 @@
#include <I18n.h> #include <I18n.h>
#include <algorithm> #include <algorithm>
#include <cctype>
#include <string> #include <string>
#include <vector> #include <vector>
@@ -44,18 +43,6 @@ void drawScrollBar(const GfxRenderer& renderer, Rect rect, int itemCount, int pa
renderer.fillRect(barX, thumbY, barW, thumbH); renderer.fillRect(barX, thumbY, barW, thumbH);
} }
std::string sanitizeButtonLabel(std::string label) {
// Remove common directional prefixes/symbols (e.g. "<< Home", unsupported icon glyphs).
while (!label.empty() && !std::isalnum(static_cast<unsigned char>(label[0]))) {
label.erase(0, 1);
}
// Trim any extra left spaces.
while (!label.empty() && label[0] == ' ') {
label.erase(0, 1);
}
return label;
}
} // namespace } // namespace
int coverWidth = 0; int coverWidth = 0;
@@ -412,11 +399,11 @@ void RoundedRaffTheme::drawButtonHints(GfxRenderer& renderer, const char* btn1,
const bool backDisabled = (btn1 == nullptr || btn1[0] == '\0'); const bool backDisabled = (btn1 == nullptr || btn1[0] == '\0');
const int leftGroupX = sidePadding; const int leftGroupX = sidePadding;
const int rightGroupX = leftGroupX + groupWidth + groupGap; const int rightGroupX = leftGroupX + groupWidth + groupGap;
const std::string backLabel = backDisabled ? "" : sanitizeButtonLabel(std::string(btn1)); const std::string backLabel = backDisabled ? "" : std::string(btn1);
// Callers should provide the button labels. If a label is not specified, it should render empty. // Callers should provide the button labels. If a label is not specified, it should render empty.
const std::string selectText = (btn2 && btn2[0] != '\0') ? sanitizeButtonLabel(std::string(btn2)) : ""; const std::string selectText = (btn2 && btn2[0] != '\0') ? std::string(btn2) : "";
const std::string upText = (btn3 && btn3[0] != '\0') ? sanitizeButtonLabel(std::string(btn3)) : ""; const std::string upText = (btn3 && btn3[0] != '\0') ? std::string(btn3) : "";
const std::string downText = (btn4 && btn4[0] != '\0') ? sanitizeButtonLabel(std::string(btn4)) : ""; const std::string downText = (btn4 && btn4[0] != '\0') ? std::string(btn4) : "";
// Ensure button hints always "win" visually even if other elements accidentally render into this area. // Ensure button hints always "win" visually even if other elements accidentally render into this area.
renderer.fillRect(leftGroupX, hintY, groupWidth, hintHeight, false); renderer.fillRect(leftGroupX, hintY, groupWidth, hintHeight, false);
@@ -16,7 +16,6 @@ Requirements:
import argparse import argparse
import re import re
from collections import Counter from collections import Counter
import pyphen
from pathlib import Path from pathlib import Path
import zipfile import zipfile
@@ -75,6 +74,8 @@ def generate_hyphenation_data(
min_prefix: Minimum characters allowed before the first hyphen (default: 2) min_prefix: Minimum characters allowed before the first hyphen (default: 2)
min_suffix: Minimum characters allowed after the last hyphen (default: 2) min_suffix: Minimum characters allowed after the last hyphen (default: 2)
""" """
import pyphen
print(f"Reading from: {input_file}") print(f"Reading from: {input_file}")
# Read the input file # Read the input file