Compare commits

...
Author SHA1 Message Date
Dylan Byars 973a902332 fix: duplicate User-Agent header on wolfSSL requests breaks strict servers (aiohttp 400) (#2661) 2026-07-21 01:48:26 -04:00
Justin Mitchell cb9b971c26 Refactor SCOPE.md for improved formatting
Updated SCOPE.md to remove unnecessary line breaks and improve readability.
2026-07-21 01:20:26 -04:00
Justin MitchellandJulia Nguyen f42fab1c66 feat: Add touch coordinate mapping and RTOS task yielding (#2481)
Co-authored-by: Julia Nguyen <julia@uxj.io>
2026-07-20 16:31:07 -04:00
Justin Mitchell c9188a7347 chore: Clarify project scope and development priorities (#2149)
Expand scope to support multiple e-ink devices beyond X4. Add guiding
principle to fill gaps in stock firmware rather than duplicate
functionality. Establish current focus on memory/flash optimization and
code cleanup. Temporarily freeze new themes and network connectors to
consolidate codebase before multi-device expansion.
2026-07-20 16:02:02 -04:00
Leopoldo Pla Sempere e03aa16330 fix: select strongest AP for matching WiFi SSID (#2655) 2026-07-20 18:17:16 +03:00
Phạm Bình An 3319aa1720 fix(epub): preserve word continuation when splitting CJK text on MAX_WORD_SIZE (#2652) 2026-07-20 15:02:08 +03:00
CookieCaptainD 3dfbc0383f fix(i18n): add missing strings in PT-PT translation (#2632) 2026-07-20 14:12:06 +03:00
Thiago Kenji Okada 64364bec5d feat: add Nix development shell (#2645) 2026-07-20 00:57:37 +03:00
Stefan Blixten Karlsson 556b8aee5b fix: swedish translation (#2649) 2026-07-19 22:12:10 +03:00
b1d1d757ba feat: Slim dictionary (#2583)
Co-authored-by: Ryan Hitchman <hitchmanr@gmail.com>
Co-authored-by: Kurtis Grant <kurtis.b.grant@gmail.com>
Co-authored-by: kemonine <kemonine@kemonine.info>
Co-authored-by: DustinHu <hu.dustin@gmail.com>
Co-authored-by: Justin Mitchell <justin@jmitch.com>
2026-07-19 17:57:16 +03:00
Uri Tauber 5ba1d5747f fix: EndOfBookOptions fails to compile
ActivityManager.h forward-declares Activity but holds
std::unique_ptr<Activity> members. Instantiating that unique_ptr's
destructor requires the complete type, so any TU including
ActivityManager.h without Activity.h fails to compile.

Include Activity.h directly. Adding it to ActivityManager.h instead does
not work: Activity.h depends on HomeMenuItem, which ActivityManager.h
defines.
2026-07-19 16:45:42 +03:00
Thomas Symalla 9fe4dc5e38 feat: Add option to switch behavior for "back to browser / home" in Reader activity (#2366)
## Summary

It would be nice to switch back to the file list from an Reader activity
via a short back button press. This change adds an Reader option to
switch the default behavior, so a short back button press in the Reader
activity can now go back to the file list, and a long press on back goes
back to the home view. This does a fair bit of refactoring, introducing
a new constant for the ms limit.

* **What changes are included?**

- Changes to the translation
- Additional global Reader option 
- Refactoring of the back button behavior in the Reader activity
2026-07-19 08:29:13 +03:00
Justin Mitchell b1d037569b feat: Add kosync user registration and switch to crosspoint-sync server (#2587)
Implements createUser() endpoint to allow account creation via the
KOSync protocol. Changes default sync server from sync.koreader.rocks to
sync.crosspointreader.com with migration logic to preserve existing
users' server settings. Extends sync protocol to include position data
(spine index, page numbers, xpath) that crosspoint-sync supports while
remaining compatible with standard kosync servers.
2026-07-18 14:50:51 -04:00
Julia 9737cb335c fix: correct the settings enums for "blank" and "cover + custom" sleep screens (#2635) 2026-07-17 12:35:51 -04:00
Phạm Bình An fdffc2e5d9 fix: reduce CSS parse-time OOM risk in chapter layout (#2606) 2026-07-16 07:21:08 +03:00
a2db43d235 feat: enable CORS headers in the HTTP API (#2594)
Closes #2558.

Enables the Arduino WebServer's built-in CORS support
(`enableCORS(true)`), which adds
`Access-Control-Allow-Origin/Methods/Headers: *` to every response, and
answers preflight `OPTIONS` requests with `204` in `handleNotFound()` —
routes are registered per-method, so OPTIONS always lands there. The
AP-mode captive-portal redirect is untouched (the OPTIONS check runs
before it, and browsers don't send preflights for captive-portal
probes).

This lets web-based clients and PWAs served from other origins call the
JSON API (`/api/status`, `/api/files`, `/api/settings`, ...) directly
from the browser.

Overhead is three static response headers; no behavior change for the
built-in web UI.

Note: not yet tested on hardware.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: metoli <metoli@metoli-Mac-mini.local>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-15 18:35:10 -04:00
rxmmahandkira 63093e606c feat: Back on home menu opens the most recent book (#2619)
Co-authored-by: kira <rammah@tuta.io>
2026-07-15 23:50:22 +03:00
a4ac3b2788 feat: add smart KOReader progress sync (#2192)
## Summary

Make KOReader progress sync a one-click flow for common cases while
keeping the existing manual mode available.

### Changes

- Add a **Sync Behavior** setting:
  - **Smart sync** for new configurations
  - **Ask every time** for manual control
- Preserve **Ask every time** when migrating an existing credential file
that predates this setting.
- In Smart mode:
  - upload local progress when no remote record exists;
  - show a short confirmation when already synced;
  - upload when local progress is further ahead;
  - apply remote progress when remote progress is further ahead.
- Probe both KOReader document-matching hashes before making the Smart
decision, while keeping uploads on the user's configured matching
method.
- Auto-return after successful Smart terminal states, with Back/Confirm
still available.
- Persist the setting alongside the current credential, matching-method,
and send-metadata fields.
- Document both behaviors in the user guide.

## Additional context

This pairs well with #2189 (smart Wi-Fi auto-connect), but does not
depend on it. It does not add background Wi-Fi or passive network
detection; sync is still triggered by the user from the reader menu.

Smart sync uses the furthest progress because CrossPoint does not
currently persist local progress timestamps. Users who prefer explicit
conflict resolution can select **Ask every time**.

## Verification

- `git diff --check`
- `platformio check --fail-on-defect low --fail-on-defect medium
--fail-on-defect high` — no defects
- `platformio run -e default` — firmware build successful
- The original implementation was manually smoke-tested on an X4 device.

---

### AI Usage

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

AI tools were used to inspect the codebase, draft and review the
implementation, resolve the rebase against current `develop`, and
prepare the PR text. The resulting firmware was built locally.

---------

Co-authored-by: Alexander Hoffer <git@alexanderhoffer.com>
Co-authored-by: Alexander Hoffer <contact@alexanderhoffer.com>
2026-07-15 14:59:15 -04:00
Víctor Fernández 35a45f9c1e feat: add options to remember web upload settings & rename ebooks to {title} - {author} (#2534) 2026-07-15 14:43:42 -04:00
William Floyd f3cad2f9e1 fix(kosync): reload Epub before reading upload metadata (#2608) 2026-07-15 11:46:10 -04:00
Oscar Nogueira Neto 95d8cb712b feat: configurable OPDS download folder and filename format (#2571) 2026-07-15 10:53:27 -04:00
Mirza Arnaut b9867b0d09 feat(i18n): add Bosnian translation (#2616) 2026-07-15 10:49:42 -04:00
Uri Tauber 3dac4446b7 fix: remove duplicate sleep logic (#2492) 2026-07-15 10:48:08 -04:00
Uri Tauber f180069643 fix: ignore open-x4-sdk and fs_ (#2609) 2026-07-14 21:34:28 +03:00
Husam Younis a0ec5812b1 feat: Arabic translation YAML - PR 3/3 (#2599) 2026-07-14 20:33:42 +03:00
Phạm Bình An e84840b473 fix(css-parser): don't save unusable rules to RAM (#2604) 2026-07-14 17:00:13 +03:00
Tom-Inge Larsen 94ae750e89 feat(i18n): add Norwegian Bokmål translation (#2113) 2026-07-14 16:54:43 +03:00
metoli86andmetoli 8c5cd67f38 feat: complete Turkish translation (390/390) (#2592)
Co-authored-by: metoli <metoli@metoli-Mac-mini.local>
2026-07-14 16:50:03 +03:00
Justin Mitchell c5787d1cc1 fix: Use HALF_REFRESH for sleep and boot instead of FULL (#2588)
Changes all sleep screen display refreshes and boot screens from
FULL_REFRESH to HALF_REFRESH to match OEM firmware behavior. The stock
firmware uses a single-pass 0xD7 waveform for sleep screens, not the
multi-flash GC waveform (0xF7) that FULL_REFRESH triggers. For grayscale
mode, HALF_REFRESH is required because the gray nudge LUT is calibrated
against the pixel charge state left by the single-pass waveform; using
FULL_REFRESH causes blotchy noise in gray areas. Also adds X3 wb_gc
passive byte (the one-byte 0x54 to 0x00, matching stock's passive WB
table in the gc nudge bank): Every X3 grayscale render, reader AA,
covers, and the sleep-cover nudge, gets it.
2026-07-13 17:06:09 -04:00
Husam Younis 2a19eb9e0d feat: Arabic/Farsi/Urdu glyphs in built-in UI fonts - PR 2/3 (#2596) 2026-07-13 22:35:47 +03:00
Uri Tauber fae3423dec feat: add portuguese-PT.yaml (#2597) 2026-07-13 22:27:23 +03:00
Husam YounisandUri Tauber 932a472835 feat: Arabic/Farsi/Urdu bidi reordering and contextual shaping — PR 1/3 (#2541)
Co-authored-by: Uri Tauber <uritaube@gmail.com>
2026-07-13 16:02:21 +03:00
Jan Steinke 552b2683e6 fix: keep list item bullet inline with nested paragraph text (#2589) 2026-07-13 12:06:43 +03:00
Mr.Catfood bfa1d706b1 feat: Hidden wifi ssid support (#2360) 2026-07-13 02:33:48 -04:00
Jacob Latonis 73ab9ab20d feat: implement captive portal redirects for auto-loading the management page on hotspot mode (#2550) 2026-07-13 00:10:23 -04:00
Nick Perez 3a97f3e63e feat: Send optional document metadata with KOSync progress uploads (#1820) 2026-07-12 23:50:38 -04:00
Matthías Páll Gissurarson 39ea4b045f feat: render placeholders while waiting for images to render (#1003) 2026-07-12 21:40:07 +03:00
Justin Mitchell 444d87de82 fix: Add framebuffer release/realloc and improved lazy indexing (#2563) 2026-07-12 13:16:48 -04:00
Leopoldo Pla Sempere 859f6cb0d5 chore: update Spanish, Catalan, and Valencian Wi-Fi strings (#2578) 2026-07-12 12:13:06 +03:00
hooligan333andErica Jensen d8139e9605 perf: release CSS rule map after warm open (#2439)
Co-authored-by: Erica Jensen <erica@mailershaven.com>
2026-07-11 22:59:31 -04:00
hooligan333andErica Jensen b596f21e61 perf: stream NCX/NAV TOC into parser, drop temp-file round-trip (#2440)
Co-authored-by: Erica Jensen <erica@mailershaven.com>
2026-07-11 22:57:48 -04:00
hooligan333andErica Jensen 5fbc657aeb perf: always binary-search idref lookups in content.opf (#2433)
Co-authored-by: Erica Jensen <erica@mailershaven.com>
2026-07-11 22:56:28 -04:00
Jason Huebel bb06219c9c docs: add script to generate EPUB from USER_GUIDE.md (#2152) 2026-07-11 22:54:32 -04:00
Alexander Hoff ❍andAlexander Hoffer 5202bbf911 feat: auto-connect saved Wi-Fi networks (#2189)
Co-authored-by: Alexander Hoffer <git@alexanderhoffer.com>
2026-07-11 22:47:26 -04:00
hooligan333andErica Jensen e0253a8664 perf: reserve CSS rule map before loading from cache (#2435)
Co-authored-by: Erica Jensen <erica@mailershaven.com>
2026-07-11 22:24:43 -04:00
hooligan333andErica Jensen e142e88ce7 perf: drop per-image delay(50) on chapter build, retry getDimensions (#2434)
Co-authored-by: Erica Jensen <erica@mailershaven.com>
2026-07-11 22:23:52 -04:00
Timo 6add0b91bc feat: Add Finnish hyphenation (#2084) 2026-07-11 22:16:42 -04:00
Leopoldo Pla Sempere 287457f7dd fix: handle low-bit-depth, upscaled, and SVG EPUB images (#2503) 2026-07-11 21:45:53 -04:00
Justin Mitchell 3e627112f6 fix: oom exceptions for OPDS, KOSync, and OTA via wolfssl (#2475) 2026-07-12 01:53:26 +03:00
Stefan Blixten Karlsson 1f5669a08a fix: Swedish translation (#2577) 2026-07-11 23:20:20 +03:00
Ing. Jan Kaláb 0dc92c142c fix: update czech.yaml (#2574) 2026-07-11 21:09:09 +03:00
Leopoldo Pla Sempere 4b34a576eb chore: update Spanish, Catalan, and Valencian translations (#2566) 2026-07-10 17:45:40 +03:00
4fb843ef92 perf: skip redundant progress writes when position is unchanged (#2436)
Co-authored-by: Erica Jensen <erica@mailershaven.com>
Co-authored-by: Uri Tauber <uritaube@gmail.com>
2026-07-10 10:37:43 +03:00
Matteo Scopel a43061305a chore: update the Italian translation (#2559) 2026-07-08 22:22:47 +03:00
Uri Tauber 7ec07b4d9d fix: show "Failed to index" when failing to parse epub (#2556) 2026-07-08 21:22:35 +03:00
Uri Tauber c2c1badc7e chore: Refactor stores to use PersistableStore CRTP template (#2464) 2026-07-08 12:44:52 +03:00
Nick 5776911af0 fix: FontDecompressor OOM aborts on the render path (-fno-exceptions makes vector resize fatal) (#2526) 2026-07-07 09:06:18 +03:00
Pietro Campagnano 3c5c5e8aa7 feat: preview image files inline in web file browser (#2429) 2026-07-06 16:45:22 -04:00
Justin Mitchell 6f5c5a0900 fix: Flatten TextBlock word storage into single allocation (#2547) 2026-07-06 23:41:00 +03:00
Uri Tauber f2b00f6b93 chore: Replace product link with affiliate tracking link (#2401) 2026-07-06 23:13:27 +03:00
Uri TauberandBrooks Ilg 44ff313740 fix: render <br> between paragraphs as a visible section break (#2548)
Co-authored-by: Brooks Ilg <brooksmilg@gmail.com>
2026-07-06 20:10:31 +03:00
Jacob Latonis ca1b833126 fix: follow spec for zxing qr code generation (#2540) 2026-07-06 00:33:34 +03:00
Tom-Inge Larsen 3e1dd31e53 feat(reader): End of Book next-book suggestions (#2499) (#2532) 2026-07-04 23:12:59 +03:00
685d4e88f9 feat: Lazy incremental EPUB section indexing (#2452)
Co-authored-by: Uri Tauber <uritaube@gmail.com>
Co-authored-by: Julia Nguyen <julia@uxj.io>
2026-07-04 21:21:24 +03:00
Jakub Sypiański 79b4d63ee2 fix: settings persist on font clear, reserve() before push_back, minor (#2519) 2026-07-03 18:58:24 +03:00
Leopoldo Pla SempereandJiangoJ 57447a56a6 feat(epub): improve text-decoration support (#2397)
Co-authored-by: JiangoJ <jiangj2620@gmail.com>
2026-07-02 12:35:24 +03:00
Rahat Ahmed 5c86bfe1fd feat(network): add device serial number to web UI (#2506) 2026-07-02 00:53:55 +03:00
Justin Mitchell 4cdefb137c Update README with firmware flashing warnings and crosspet status
Removed warning about flashing Papyrix on USB-locked devices and updated crosspet status to unmaintained.
2026-07-01 14:22:27 -04:00
Justin Mitchell e7f3406704 fix: Add socket module import to build-sd-fonts script (#2504) (#2505) 2026-07-01 12:10:15 -04:00
Bastian 6f08a88b12 fix: changing translation in czech to fix overflowing navigation (#2502) 2026-07-01 11:58:24 -04:00
Tom-Inge LarsenandClaude Opus 4.8 e4d3981469 fix(reader): correct slider side-button direction and legend on X3 (#2402) (#2428)
## Summary

* **What is the goal of this PR?** Fixes #2402. On the X3, the "Time to
Sleep"
picker's 5-minute side buttons were inverted (left increased, right
decreased)
  and the on-screen legend didn't match the physical buttons.
* **What changes are included?**
  * Flip the large-step (±5 min) direction on X3 so the left side button
decreases and the right increases, matching the layout. X4 is unchanged.
* Add a device-specific step-hint string
(`STR_SLEEP_TIMER_STEP_HINT_X3`)
selected via `gpio.deviceIsX3()`. X3 shows `+/-: 1 min Side buttons: 5
min`;
    X4 keeps the original `Left/Right: 1 min  Up/Down: 5 min`.
* Same fix applied to both slider pickers that read the raw up/down side
    buttons: the Time-to-Sleep picker and the "Go to %" picker
(`EpubReaderPercentSelectionActivity`), each with its own X3 hint string
    (`STR_SLEEP_TIMER_STEP_HINT_X3`, `STR_PERCENT_STEP_HINT_X3`).

## Additional Context

* **Root cause:** the X3's side buttons sit one on each edge of the
screen
(power on top), whereas the X4 has a vertical up/down rocker on the
right
edge. So `BTN_UP` is physically the *left* button on X3 but the *top*
button
  on X4. The picker mapped up→+5 / down→−5 unconditionally, which reads
naturally on the X4 rocker but inverted on the X3's left/right buttons.
The
  static legend ("Up/Down: 5 min") had the same X3-only mismatch.
* The codebase already detects the device at runtime and handles this
layout
difference elsewhere (e.g. `LyraTheme::drawSideButtonHints` draws "Up on
left,
Down on right" for X3), so this reuses the same `gpio.deviceIsX3()`
signal.
* Swept the codebase for other side-button sliders:
`ClockOffsetActivity` already
uses the `Next/Previous` (`ButtonNavigator`) abstraction, which resolves
to
left=decrement / right=increment on X3, so it needs no change. List/page
navigation (Wi-Fi, KOReader sync, BMP viewer) and the keyboard cursor
are also
unaffected. The two slider pickers above were the only ones using raw
up/down.
* **X4 is untouched** — same direction, same legend wording as before.
* **Tested on X3 hardware:** left side button now decreases, right
increases, and
  the legend matches. X4 not yet tested on device (no unit on hand); its
  behaviour and strings are unchanged from master.
* **Translations:** the new `STR_SLEEP_TIMER_STEP_HINT_X3` and
`STR_PERCENT_STEP_HINT_X3` were added to all 26 languages, but the
non-English
  ones are AI-generated and would benefit from a native-speaker pass —
particularly Hebrew (RTL ordering with a leading `+/-`), Kazakh, and
Vietnamese.

---

### AI Usage

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

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 11:06:31 -04:00
Bastian c9d27826f9 docs: update TOC in userguide (#2479) 2026-06-30 08:48:21 +03:00
Bastian 13bd00bf9d fix: sort sleep screen menu options to be more logically consistent (#2480) 2026-06-30 08:47:25 +03:00
Uri Tauberandpablohc fef39da23c feat: Selection Popup (#2358)
Co-authored-by: pablohc <pablonoviello@outlook.com>
2026-06-29 16:59:25 -04:00
Uri Tauber fd43ca2fe1 chore: Initial multi-core compatibility (#2294) 2026-06-29 21:57:21 +03:00
Pietro Campagnano 28255061fb fix(icons): align home menu icons with their labels (#2470) 2026-06-29 13:59:54 -04:00
Julia Nguyen 43ff1b6644 Merge remote-tracking branch 'origin/master' into develop 2026-06-29 12:29:58 -04:00
Егор Мартынов 2754a5ff01 feat: add Vollkorn font (#2473)
CI (build) / clang-format (push) Canceled after 0s
CI (build) / cppcheck (push) Canceled after 0s
CI (build) / build (push) Canceled after 0s
CI (build) / unit-tests (push) Canceled after 0s
CI (build) / Test Status (push) Canceled after 0s
2026-06-29 15:54:01 +03:00
Justin Mitchell 6edeed73c5 fix: x4 sleep/boot ghosting (#2471) 2026-06-29 12:04:33 +03:00
Justin Mitchell 8ac302018c fix: x4 ghosting by bumping sdk (#2469) 2026-06-28 22:38:35 +03:00
Rodrigo Matsuura a11517a62d fix: update Portuguese (Brasil) translations (#2458) 2026-06-28 15:07:52 +03:00
Bastian cbaa498ccc docs: adding quick resume option and quick resume on timeout to userguide (#2425) 2026-06-27 22:40:21 +03:00
Justin Mitchell ebebc6f202 chore: migrate from open-x4-sdk to freeink-sdk (#2449)
This PR moves us from the xteink openx4 SDK to the freeink sdk from
https://freeink.org. Out of the box there are NO changes needed in the
firmware to support this swap, it all magically works as is. However as
we support more than just the x3/x4 devices, this sdk allows us to pass
env vars into the build commands to include support for other devices.
As support for new hardware such as touch screens and bluetooth are
added the xteink builds decide at compile time if the libraries are used
or not. For example right now the freeinkui and icons libraries are in
the platform.io file but as they are not used anywhere, they won't be
included in the final build. Once the touch branch and sd themes branch
are merged in this sdk is required for them to function correctly. All
the docs for freeink are available at freeink.org/docs. x4/x3 is a
single binary build unlike other devices that will build unique binaries
for each device. Eventually we will want to remove a lot of the manual
isx3 type stuff from our firmware and go through the boardsupport api
the sdk provides as it will generalize everything into one common system
that any device can support. The upcoming touch branch does a lot of
this for us but this initial PR is JUST to get the sdk swapped over
without any code changes to show seamless integration without any
regressions.
2026-06-27 21:22:58 +03:00
Julia 970b2c6ca1 chore: release 1.4.1 (#2447)
Compile Release / build-release (push) Canceled after 0s
## Improvements

* Moved the File Manager breadcrumb into the Contents card header for a
cleaner, more consistent interface.
* Updated the Wireless Transfer section of the User Guide with clearer
instructions.
* Battery status bar indicator no longer changes position when adding or
removing bookmarks.

## Performance

* Optimized path normalization for faster file handling.
* Significantly improved bookmark rendering by removing unnecessary
XPath lookups.
* Optimized dithered rectangle drawing (fillRectDither) using a
byte-aligned rendering implementation, improving display performance on
supported devices.

## Bug Fixes

* Fixed an issue where the Inverted Orientation label was incorrectly
combined with the Color Filter label for Geman localization.
* Fixed excessive ghosting on the X3 cover screen during sleep
2026-06-26 17:40:04 -04:00
Julia Nguyen b6ce599b20 fix: address release review feedback 2026-06-26 16:22:20 -04:00
Juliaandcoderabbitai[bot] f54eab2725 fix: typo in STR_ORIENTATION_INVERTED Spanish translation
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2026-06-26 16:13:19 -04:00
Justin Mitchell 0d4c9ab91b Bump version to 1.4.1 2026-06-26 15:48:19 -04:00
Julia 0daa9db243 fix: keep status bar indicators stable when toggling bookmarks (#2444) 2026-06-26 19:26:33 +03:00
Uri TauberandRyan Mercado a2f2eea79e perf: Optimize fillRectDither with Byte-Aligned fillRectImpl (#2270)
## Summary

* **What is the goal of this PR?**
Replace the pixel-by-pixel `fillRectDither` implementation with a new
byte-aligned `fillRectImpl` that eliminates per-pixel
`rotateCoordinates` calls and Read-Modify-Write bitwise loops, yielding
a significant rendering speedup on ESP32 E-ink framebuffers.

Ported from @rhythmerc's crosspoint-reader fork commit 27ea625.

* **What changes are included?**

- **`GfxRenderer.cpp` — `fillRectDither` refactor:** The existing
`if/else if` chain is replaced with a `switch` statement that delegates
each `Color` case to the new `fillRectImpl<Color>()` template,
eliminating runtime branching.

- **`GfxRenderer.cpp` — new `fillRectImpl<C>()` template:** Core of the
optimization. Key behaviors:
    - Clips the rectangle in logical space upfront.
- Rotates only **2 opposing corner points** (top-left and bottom-right)
into physical framebuffer space instead of rotating every pixel
individually.
- Derives physical-space `byteStart`/`byteEnd` and precomputes
`headMask` / `tailMask` for MSB-first partial-byte boundaries,
performing RMW only on the edge bytes.
- **Solid fills (`Black` / `White`):** Uses `memset` for all interior
full-byte runs per row — no per-pixel writes.
- **Dithered fills (`LightGray` / `DarkGray`):** Precomputes both parity
variants of `blackMask` (even/odd `py`) **outside** the row loop,
eliminating the previously re-evaluated 8-bit construction loop on every
physical row. Interior full bytes are then written with a single
`memset(whiteMask)`.
- Uses `if constexpr` throughout to dispatch on `Color` at compile time,
generating zero runtime branches per template instantiation.

- **`GfxRenderer.h`:** Declares the new private `fillRectImpl<Color>()`
template method with an explanatory doc-comment.

- **Explicit template instantiations** added for all four active `Color`
variants (`Black`, `White`, `LightGray`, `DarkGray`).

## Additional Context

* **Performance:** The primary motivation is ESP32 E-ink framebuffer
performance. The old path called `rotateCoordinates` and did a full RMW
for every single pixel in the rectangle. The new path calls
`rotateCoordinates` exactly **twice** per fill regardless of rectangle
size, then operates at byte granularity — a complexity reduction from
O(W×H) coordinate transforms to O(1).
* **Dither correctness:** The `blackMask` precomputation relies on the
dither pattern having period 2 in both logical X and Y, which makes the
per-row byte pattern repeat with period 2 in `py`. Reviewers should
verify the `lxBase`/`lyBase` derivations for all four orientations
(`Portrait`, `PortraitInverted`, `LandscapeClockwise`,
`LandscapeCounterClockwise`) match the inverse of `rotateCoordinates`.
* **Edge case — single-byte rows:** When `byteStart == byteEnd`, the
head and tail masks are ANDed together into a single `rectMask` to avoid
double-masking the same byte. This path should be tested with narrow
rectangles (width < 8px).
* **No behavioral change for `Color::Clear`:** The `Clear` case exits
early via `if constexpr` and is a no-op, matching the original behavior.

---

### 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 >**_

---------

Co-authored-by: Ryan Mercado <rmercado@firstdollar.com>
2026-06-26 12:16:25 -04:00
Ankit 86a9b9c4a2 docs: update user guide wireless transfer section (#2369)
Co-authored-by: Uri Tauber <uritaube@gmail.com>
Closes #1407
2026-06-26 08:25:52 +03:00
Julia Nguyen ba44978ac4 Merge branch 'master' into develop 2026-06-25 17:51:56 -04:00
Pietro Campagnano 555f76da88 feat: move file manager breadcrumb into contents card header (#2430) 2026-06-26 00:45:43 +03:00
Justin Mitchell 0a57c0a5a7 Fix X3 display ghosting on cover screen transitions
Force display resync on X3 when HALF refresh is requested to clear prior content before rendering. Add grayscale preconditioning for X3's UC81xx controller to even out single-pixel dithering artifacts that appear as speckle with its turbo BW waveform.
2026-06-25 17:42:19 -04:00
Uri Tauber a09aef0889 fix: Optimize Bookmark Rendering by Removing XPath Lookup (#2417)
## Summary

fix #2414 

### Root Cause

`updateBookmarkFlag()` was introduced in commit 1db1442 and is executed
on every render (every page turn). The function calls:

`ProgressMapper::toSavedProgress()`
→ `ChapterXPathResolver::findXPathForProgress()`

This path decompresses the current EPUB section content twice:

1. To count visible characters.
2. To resolve the corresponding XPath.

For larger sections (e.g. ~133 KB decompressed content), this adds
approximately **1 second of I/O overhead per page turn**, with the cost
increasing as chapter size grows.

### Fix

`updateBookmarkFlag()` only needs to determine whether a bookmark falls
within the currently displayed page range.

The required information is already available during rendering:

* `currentPage`
* `section->pageCount`
* `currentSpineIndex`

Instead of converting the current location to a saved progress object
(and resolving an XPath), the implementation now computes the current
page's progress range directly and compares bookmark percentages against
that range.

This is effectively the same percentage-based matching logic already
used as a fallback in `bookmarkMatchesProgress()` when XPath matching is
unavailable.

---

### 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-06-25 09:20:24 -04:00
Bastian 8626d69f46 fix: small translation changes for german (#2420) 2026-06-25 09:29:05 +03:00
Uri Tauber fc89e57e69 perf: optimise normalisePath (#2162) 2026-06-25 09:01:50 +03:00
Julia 487613b082 fix: split inverted orientation label from color filter label (#2421)
## Summary

* **What is the goal of this PR?** 
* Fix the “Inverted” translation so reader orientation and color/filter
inversion can use separate labels.
* **What changes are included?**
* Adds `STR_ORIENTATION_INVERTED` for the inverted portrait orientation
option.
* Updates the reader orientation setting to use
`STR_ORIENTATION_INVERTED` instead of reusing `STR_INVERTED`.
* Leaves `STR_INVERTED` for the sleep cover filter and tilt page-turn
mode
  * Adds the new orientation string across all 26 locale YAML files.

## Additional Context

* The original issue was found by a user in German, where `STR_INVERTED`
was translated as `Hochformat 180°`, which made sense for orientation
but not for color filters.
* This is a UI-label-only change. It does not change persisted
orientation values or settings behavior.
* Reviewer note: non-English wording may still benefit from
native-speaker review, especially for the new orientation-specific
labels and to verify the interchangeable usage between inverted color
and inverted tilt page turn direction.
---

### 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-06-25 00:47:06 -04:00
JuliaandHusam Younis 8d5b119644 fix: sync master into develop (#2423)
Sync develop branch with master

Co-authored-by: Husam Younis <youhusam@gmail.com>
2026-06-24 22:07:13 -04:00
Husam Younis 7271c00d35 feat: Allow statusbar clock to be on the left (#2359) 2026-06-25 00:23:23 +03:00
Uri Tauber 6e8dbd7f23 chore: Update version to 1.4.0 (#2283)
Compile Release / build-release (push) Canceled after 0s
2026-06-24 18:30:32 +03:00
Uri Tauber c4b1d9644a fix: correct font size selection (#2410) 2026-06-24 17:01:10 +03:00
Justin Mitchell f6e59aab72 feat: Add displayGrayscaleBase method for differential refresh
(#2334)

Introduces a new display method that prepares the framebuffer as a base
frame for grayscale overlays. On X3 panels, this uses the OEM
differential base waveform (AA-pre-BW) without forcing a resync. Other
panels fall back to normal display with configurable refresh mode.


Dependent upon matching SDK commit to work
2026-06-24 07:53:19 -04:00
Uri Tauber 9ad2da0950 fix: add seven missing hebrew translations (#2409) 2026-06-24 09:34:40 +03:00
Uri TauberandJulia Nguyen 1db1442319 fix: several bookmarks UX improvments (#2372)
## Summary

This PR enhances the EPUB reader's bookmark system with two
complementary improvements: a per-page bookmark indicator icon and
toggle behavior on the existing long-press action.

---

### What Changed

**Bookmark Toggle (was: add-only)**

The long-press Confirm action now toggles bookmarks rather than always
adding. `addBookmark()` checks whether a bookmark with the same xpath
already exists in the in-memory cache:
- If found → removes it and shows "Bookmark removed."
- If not found → adds it and shows "Bookmark added."

A new `STR_BOOKMARK_REMOVED` translation string was added to support the
removal message.

**Bookmark Icon Indicator**

A `BookmarkIcon` is now drawn at the top-right corner of the page
whenever the current page has a bookmark. `updateBookmarkFlag()` is
called at render time to determine whether the current page is
bookmarked.

**In-Memory Bookmark Cache**

Bookmarks are now loaded into `cachedBookmarks` on `onEnter()` rather
than being re-read from disk on every toggle. All subsequent add/remove
operations work against this cache and flush to disk, avoiding redundant
file reads on each bookmark action.

**Faster bookmarks list**

Previously, calculating "page X/Y" for each entry required decompressing
the entire spine item. We now persist `si`/`pc`/`pp` (spine index, page
count, and page progress) in the bookmark JSON when saving, and restore
them when loading. This avoids the expensive `toCrossPoint()` loop in
`onEnter()`, significantly reducing the cost of initializing the
bookmarks list.

---

### Files Changed

- `EpubReaderActivity.cpp` — `addBookmark()` toggle logic,
`updateBookmarkFlag()` (new), icon rendering in `renderContents()`,
cache initialization in `onEnter()`
- `EpubReaderActivity.h` — new fields: `currentPageBookmarked`,
`bookmarkRemoved`, `cachedBookmarks`; new method declaration
`updateBookmarkFlag()`

---

### 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: Julia Nguyen <julia@uxj.io>
2026-06-23 14:55:01 -04:00
Leopoldo Pla Sempere 362dcb2a65 feat: Smooth progressive JPEG cover upscales in BMP conversion (#2214) 2026-06-22 13:55:12 -04:00
Julia d1abcc00a2 feat: render grayscale epub images without text aa (#2393) 2026-06-22 17:15:27 +03:00
Zeph 0ce874f8d4 feat: footnote returns to original position on deep sleep to avoid losing reading position (#2394) 2026-06-22 15:28:42 +03:00
kygia 282514f755 fix(epub): flush displaced anchor before overwrite (#2336) (#2382)
## Summary

* **What is the goal of this PR?** (e.g., Implements the new feature for
file uploading.)

Fixes footnote links landing in start section instead of the specific
foot note target.

* **What changes are included?**

ChapterHtmlSlimParser.cpp: call flushPendingAnchor() before overwriting
pendingAnchorId. First id got lost due to consecutive non-block elements
carry ids, the first id was lost and the reader had no page to jump to,
so it defaulted to page 0.

## Additional Context

Tested with the epubs attached to to #2336. Need to clear .crosspoint/
cache after flashing so the anchor map gets rebuilt with the fix.

* 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? 

Partially assisted with writing documentation.
2026-06-21 16:43:39 -04:00
SurprisedDuck d1e4650e19 fix: don't justify-stretch a leading no-break space (#2185) (#2298) 2026-06-21 16:12:14 +03:00
Justin Mitchell 370f87ea01 feat: add Spacing Modifier Letters range to font presets (#2194) 2026-06-21 09:14:42 +03:00
Leopoldo Pla Sempere 8b9a73a735 chore: update Spanish, Catalan, and Valencian translations (#2384) 2026-06-21 00:26:30 +03:00
Julia 29c69b3d8f fix: automatically connect to wifi for clock sync (#2379)
## Summary

* **What is the goal of this PR?** 
* Make manual clock sync work even when the device is not already
connected to Wi-Fi, so the user can start the sync flow directly from
settings instead of being blocked by connection state.
* **What changes are included?**
* `ClockSyncActivity` now launches the normal Wi-Fi selection flow
before syncing when the device is offline.
* After Wi-Fi selection succeeds, clock sync resumes automatically and
performs the existing forced NTP sync.
* If clock sync had to bring Wi-Fi up, the activity disconnects and uses
the existing silent restart cleanup path to avoid leaving the device in
a fragmented post-Wi-Fi heap state.
* The completion UI now only advertises Back, matching the updated input
handling.

## Additional Context

* Existing behavior is unchanged when Wi-Fi is already connected: the
activity syncs immediately.
* Cancelling Wi-Fi selection exits the clock sync flow.

---

### 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-06-20 22:09:28 +03:00
Matteo Scopel 4b5a84dd81 chore: update the Italian translation (#2376) 2026-06-20 22:08:35 +03:00
muhasandmuhas b9874c8114 fix(lang): [russian] new string (#2374)
Co-authored-by: muhas <mail@muhas.name>
2026-06-19 13:15:15 +03:00
Paul Delestrac b1131795d8 feat: add live font preview pane to font selection screen (#2349)
## Summary
* Adds a live font preview pane to the font selection screen so users
can see how a font looks before committing to it.
* Changes
* A preview pane occupying the top 30% of the font selection screen,
rendering sample pangram text in the previewed font
* A two-step confirm flow: first press (enter button) previews the font,
second press selects it
* Back restores the original font settings, so browsing has no side
effects
* Layout dimensions cached in `onEnter()` to avoid redundant
recalculation between `loop()` and `render()`

## Additional Context
* Preview sample text is hardcoded English; didn't want to use AI for
translation as I would not be able to verify the output in most
languages...
* The preview pane reduces visible list height; this is compensated by
passing the reserved height into `getNumberOfItemsPerPage`

---
### AI Usage
Did you use AI tools to help write this code? **PARTIALLY**
AI use for assisting in coding and in writing the PR description.
2026-06-18 18:16:28 -04:00
Pedro CardosoandClaude Opus 4.8 5e990b3991 fix: prevent progress.bin corruption from interrupted writes (#2275) (#2305)
## Summary

* **What is the goal of this PR?**
Fixes #2275. A book could get stuck reopening on an old page, with
progress no
longer saving and neither "Delete Book Cache" nor "Clear Reading Cache"
able to
fix it. Root cause: `progress.bin` was written truncate-in-place, so an
interrupted write (power loss, or a crash mid-SPI during sleep) left it
with a
broken FAT cluster chain that the firmware could neither rewrite nor
delete —
  recovery required `fsck`/manual deletion on a host PC.

  Confirmed in the SDK: `SDCardManager::openFileForWrite` opens with
`O_RDWR | O_CREAT | O_TRUNC`, so the canonical file is zeroed before the
few
progress bytes are rewritten — exactly the window that corrupts the FAT
chain.

* **What changes are included?**
  * New shared helper `ProgressFile::writeAtomic()`
    (`src/activities/reader/ProgressFile.h`): writes progress to
    `progress.bin.tmp`, flushes and closes it, then `remove`s the old
`progress.bin` and `rename`s the temp into place. An interrupted write
now
only ever damages the throwaway temp; the canonical file is never torn.
  * All three readers route their progress saves through the helper:
EPUB (`EpubReaderUtils.h`), `TxtReaderActivity`, `XtcReaderActivity` —
they
    all shared the identical vulnerable pattern.
* Minor: `EpubReaderUtils::saveProgress` now takes `const Epub&` (clears
a
    cppcheck `constParameterReference` finding).

## Additional Context

* **Crash-safe, not metadata-atomic.** On FAT the replace is `remove` +
`rename`
(two directory ops; SdFat's `rename` won't overwrite, hence
remove-first). A
crash between them leaves *neither* file, which reads as "no saved
progress" on
next launch — a harmless reset to an old page, never a
corrupt/unclearable file.
  The guarantee is that `progress.bin` is never half-written.
* **Prevents, does not repair.** This stops new corruption on healthy
cards. It
cannot fix an already-corrupted `progress.bin` (removing it may itself
fail at
the FAT level) — those still need `fsck`/manual deletion, as in the
issue's
  workaround.
* **Known follow-up (out of scope here):** a crash *while writing the
temp* can
leave an orphan `progress.bin.tmp`. It's harmless and self-healing (the
next
save overwrites it, and it never blocks reading progress), but a
boot-time
  orphan-`.tmp` cleanup would be a tidy follow-up.
* **Focus areas for review:** the close-before-rename ordering in
  `ProgressFile.h` and the remove-before-rename rationale.

## Verification

* `./bin/clang-format-fix` — clean
* `pio check --fail-on-defect low --fail-on-defect medium
--fail-on-defect high` — no defects
* `pio run` — SUCCESS (RAM 30.9%, Flash 78.8%; footprint essentially
unchanged)
* Tested on a **Xteink X4** device: open book, turn pages, sleep/exit,
reopen —
  progress now restores to the navigated page across all three readers
  (EPUB / TXT / XTC).

---

### AI Usage

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

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 18:01:56 -04:00
Uri Tauber ff1951c715 fix: correct behaviour for prev/next side buttons (#2373)
## Summary

* **What is the goal of this PR?** fix #2365
2026-06-18 13:13:35 -04:00
Ankit 121c0690b0 fix: use STR_SELECT instead of STR_OPEN in bookmark button hint (#2371) 2026-06-18 19:58:47 +03:00
16b0853654 fix(epub): NFC-normalize EPUB text so NFD diacritics render correctly (#2277)
Co-authored-by: Uri Tauber <uritaube@gmail.com>
Co-authored-by: Julia <julia@uxj.io>
2026-06-18 13:00:01 +03:00
Uri Tauber 7d639cf880 fix: submodule pointer (#2368) 2026-06-17 22:35:12 +03:00
Justin MitchellandUri Tauber 22f3575064 feat: Support for Korean line breaks and glyph spacing (#2288)
Co-authored-by: Uri Tauber <uritaube@gmail.com>
2026-06-17 17:27:03 +03:00
darkbubluandClaude Opus 4.7 d4069aeae5 feat: long-press Confirm launches KOReader sync from EPUB (#1808)
## Summary
- Hold the menu (Confirm) button for ≥1s while reading an EPUB to launch
the existing `KOReaderSyncActivity` directly — replaces the three-step
path (open reader menu → scroll to Sync → confirm) with a single
gesture.
- Reuses `ReaderUtils::GO_HOME_MS` (same 1s threshold used by long-press
Back) and the existing `KOREADER_STORE.hasCredentials()` guard.
- Adds a Controls picker **"Long-press Menu"** (`longPressMenuFunction`,
default **Bookmark**) that **cycles through the available functions**
bound to the long-press gesture: `KOSync → Disabled → Bookmark`. The
field name and `LONG_PRESS_MENU_FUNCTION` enum are intentionally general
so future actions (dictionary lookup, table of contents, etc.) can be
appended without another schema migration. The setting is **not** a
binary toggle.
- Existing menu Sync entry still works — both call sites share one
extracted helper (`launchKOReaderSync`); no logic duplication.
- Short-press Confirm release is gated on duration so the reader menu
does not also open after a long press that *acts*, mirroring the
existing long-press Back pattern.
- **No-credentials fall-through:** `launchKOReaderSync()` now returns
whether it acted. When the function is set to KOSync but no KOReader
credentials are stored, the long-press is a no-op that **falls through
to open the reader menu** — so the menu stays reachable instead of the
hold silently swallowing the gesture. The release is only suppressed
when sync actually launched or surfaced a save error.

## Test plan
- [x] `pio run` succeeds clean for the `default` ESP32-C3 environment.
- [x] On-device, value **KOSync**, valid KOReader credentials:
long-press Confirm ≥1s → sync screen launches; get + update progress
return HTTP 200; release does **not** also open the reader menu; returns
to the same page.
- [x] On-device, value **KOSync**, **no** credentials: long-press
Confirm falls through and **opens the reader menu** (regression fix);
short-press also opens the menu.
- [x] On-device: menu → Sync still launches the same screen (shared
helper) and syncs (200/200).
- [x] On-device, value **Bookmark**: long-press drops a bookmark and
does **not** also open the menu.
- [x] On-device, value **Disabled**: long-press Confirm opens the menu
on release; no sync, no bookmark.
- [x] On-device: long-press Back still goes to the file browser
(unchanged path).
- [x] Heap: epub is released before the TLS handshake (frees ~16 KB);
min free heap stayed ~84 KB during sync, well above the safe floor. No
panics/OOM across the session.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-17 17:10:26 +03:00
Mauvis LedfordandJulia 54d5a788a5 feat: add drag-and-drop to upload modal (#2290)
Co-authored-by: Julia <julia@uxj.io>
2026-06-17 16:53:31 +03:00
Ankit f66220a245 fix: enable Shift key for URL keyboard input (#2178) (#2357) 2026-06-17 16:32:36 +03:00
KymAndriyandKymAndriy 2266060e84 fix: Ukrainian translation (#2354)
Co-authored-by: KymAndriy <test@notamail.ua>
2026-06-16 14:02:23 +03:00
Julia 90039d7f4d fix(koreader): resolve element XPath progress against visible body text (#2308) 2026-06-15 22:03:32 -04:00
rafaelmsseandRafael Santos 16eb66d7cf fix: add missing HTML 4.01 named entities (#2352)
Co-authored-by: Rafael Santos <rmsantos@applaudostudios.com>
2026-06-15 22:25:37 +03:00
RoninandUri Tauber 02bab00be6 fix: swap reader menu navigation direction in CCW/inverted (#2321) (#2341)
Co-authored-by: Uri Tauber <uritaube@gmail.com>
2026-06-15 22:19:34 +03:00
Mauvis Ledford 55a56914d7 fix(i18n): localize home empty-state strings (#2342) 2026-06-14 21:40:05 +03:00
Uri Tauber 8634cb8ad3 fix: Restore first-line paragraph indentation (#2320) 2026-06-12 11:55:00 -04:00
Julia 21caebd6e4 fix: use Noto Sans as punctuation fallback when generating downloadable SD fonts (#2331) 2026-06-12 11:54:34 -04:00
Matteo Scopel 6f05e401a1 feat: add the Libre Baskerville font family (#2088)
## Summary

This PR adds the [Libre Baskerville
](https://github.com/impallari/Libre-Baskerville) font family to the
fonts available for download in CrossPoint.


## Additional Context

Baskerville is a classic font family, widely available in some form or
the other in most ereaders and widely used in publishing and in the
academy.

Libre Baskerville is a libre (SIL Open Font License v1) implementation
of it, which makes it possible for CrossPoint to ship it without issues.


![Vita-e-destino_ch30_p1_13pct_99220.bmp](https://github.com/user-attachments/files/28070672/Vita-e-destino_ch30_p1_13pct_99220.bmp)


---

### AI Usage

Did you use AI tools to help write this code? _**NO**_
2026-06-11 19:10:41 +03:00
Zach Nelson f2e3d117dc fix: Hanging indent causes overlapping words (#2324) 2026-06-11 18:23:49 +03:00
Zach Nelson 9202522a39 refactor: Drop redundant self-class prefix on applyDirectionToEntry (#2325)
## Summary

Non-functional cleanup: `ChapterHtmlSlimParser::` prefix is redundant
when calling `applyDirectionToEntry` within `ChapterHtmlSlimParser`
methods.

---

### AI Usage

Did you use AI tools to help write this code? _**NO**_
2026-06-11 10:38:00 -04:00
Uri Tauber 301f1d1a38 fix: Compile Error: Duplicate _order values found (#2323) 2026-06-11 15:18:53 +03:00
Hoang Manh LinhandClaude Opus 4.8 1f83087aa5 feat(fonts): add Vietnamese glyph coverage to the built-in UI font (#2280)
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 15:11:50 +03:00
Hoang Manh LinhandClaude Opus 4.8 68138fffdb feat(i18n): add Vietnamese translation (#2279)
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 15:10:45 +03:00
MartinandUri Tauber eb98735115 feat: Slovak translation add (#2251)
Co-authored-by: Uri Tauber <uritaube@gmail.com>
2026-06-10 18:49:39 +03:00
Uri Tauber b94b58756f fix: german traslation for STR_INVERTED (#2315) 2026-06-10 16:33:54 +03:00
muhas ca6ebc56fb fix(lang): russian.yaml new string (#2309) 2026-06-10 08:10:26 +03:00
Uri Tauber 2ea042a68a fix: skip <span> anchors (#2303)
## Summary

* **What is the goal of this PR?** Prevent heap memory exhaustion caused
by thousands of machine-generated anchor IDs injected by epub converters
like Kobo KePub.
* Fix #2292.

* **What changes are included?**
* **Element-type filter (Layer 1):** Introduced the
`isNonNavigableInlineElement()` function in `ChapterHtmlSlimParser.cpp`
to automatically skip recording IDs on `<span>` elements, as they are
purely inline wrappers used for tracking and lack navigable meaning.
* **Hard cap (Layer 2):** Added the `MAX_ANCHORS_PER_CHAPTER = 1024`
constant to act as a fallback safety net against unbounded heap growth
from unknown future ID-injection patterns on non-span elements.
* **TOC Safety net:** Ensured that IDs matching known Table of Contents
(TOC) entries explicitly bypass both the `<span>` filter and the
1024-anchor cap, guaranteeing that chapter page-break and core
navigation logic are never compromised.


---

### 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-06-09 08:58:21 -04:00
Lorenzo Santini 9c12b90737 feat: add power button short press for quick access to footnotes (#1658) 2026-06-08 14:45:41 -04:00
Leopoldo Pla Sempere 7f01eab62e docs: refresh cache formats and web server workflows (#2233)
## Summary

* **What is the goal of this PR?** Update project documentation to match
the current master implementation for cache formats, i18n, file
transfer/web server workflows, SD-card fonts, and root user-facing docs.
* **What changes are included?** Refreshes `book.bin`/`section.bin` docs
for v6/v25, updates File Transfer/Calibre/WebDAV/API docs, documents 24
UI languages and JSON language persistence, updates root
README/USER_GUIDE cache and network details, and syncs the tracked
CLAUDE skill doc cache-version notes.

## Additional Context

* Docs-only change. Verified with `git diff --check origin/master..HEAD`
and stale-reference greps for old cache versions, removed i18n APIs, old
WiFi screen wording, and raw `Serial.printf` examples. No firmware build
was run.

---

### 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-06-07 15:47:44 -04:00
Zach Nelson 3a1e9f3023 refactor: Dedupe wifi scan in-place without std::map (#2262)
## Summary

`std::map` heap-allocates each node. Avoid using it for wifi SSID
dedupe, and instead just dedupe in-place with the existing `networks`
vector. Removed dead `ipAddress` member in `WifiNetworkInfo` struct.

---

### AI Usage

Did you use AI tools to help write this code? _**PARTIALLY**_
2026-06-07 08:27:20 -04:00
SurprisedDuck fad1a801a4 fix: scale SUP/SUB underline to match 50%-scaled glyphs (#2255) 2026-06-06 18:55:47 -04:00
Julia 67936cb3af fix(epub): decode footnote href path before spine lookup (#2271) 2026-06-05 18:38:13 -04:00
Uri Tauber fd5b8078c6 chore: add t5s3 fork (#2268) 2026-06-05 18:50:22 +03:00
Julia bd101b2af8 fix(epub): decode percent-encoded internal asset paths so assets render correctly (#2249)
## Summary

* **What is the goal of this PR?** 
* Fix EPUBs where internal file references are written in URL-style
escaped form, like spaces appearing as `%20`, so the reader can find the
right files instead of treating those references as missing.
* **What changes are included?**
* Added a small shared helper that converts those escaped EPUB-internal
paths back into their normal filenames before we try to look them up.
* Applied that cleanup step across the EPUB parsing flow wherever we
resolve internal references, including cover images, manifest items, TOC
links, spine entries, and inline HTML images.
* Kept the change narrowly focused on EPUB-internal asset resolution
rather than changing broader URL or networking behavior.

## Additional Context

* The user-facing bug here is that some books package their internal
filenames in an escaped form, so a file like `Chapter 1.xhtml` may be
referenced more like `Chapter%201.xhtml`. The reader was treating that
escaped text as the literal filename, which means otherwise-valid books
could lose images, covers, or chapter targets because the lookup no
longer matched the real file inside the EPUB.
* Risk is intentionally low. The helper only rewrites valid `%XX` escape
sequences and leaves malformed input alone, so it should improve
compatibility with escaped filenames without broadening the parser’s
behavior in unrelated cases.

## Local Testing Performed
* This was tested on my device with the user-provided optimized epub
that was not rendering images within the text prior to this fix (cover
image and chapter headers were rendering fine):

[orv_main_baseline.epub.zip](https://github.com/user-attachments/files/28529545/orv_main_baseline.epub.zip)
* This was also tested by the user with a local build and the affected
epub and confirmed to be working

## Steps for Testing
* Try to open the affected epub (linked above) or any epub that has
similar percent-encoding on a build prior to this fix.
* Apply this fix, clear book cache, and re-open the affected book.
* Images should render properly.
---

### 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-06-05 10:31:20 -04:00
Uri Tauber 19d51ec08c fix: german translation for STR_INVERTED (#2269) 2026-06-05 12:22:29 +03:00
Uri Tauber ea231ffd18 Revert "Update german.yaml" (#2267) 2026-06-05 11:22:36 +03:00
Uri Tauber a2d4c6a139 Update german.yaml
Fix #2266
2026-06-05 11:17:22 +03:00
Zach Nelson b5b1f650e2 perf: Minimize string allocations in CSS parsing (#2263)
## Summary

Use `std::string_view` and case-insensitive comparisons to avoid string
allocations during CSS parsing.

**Hot path:** `resolveStyle` (called per HTML start tag during chapter
rendering) now does zero heap allocations. Previously it allocated a
normalized tag string, a vector of class strings, and a composite key
per class. For a chapter with ~2000 tags × 2 classes each, that's ~12
000 small short-lived allocations eliminated per page render — primarily
a heap-fragmentation win on the ESP32-C3's ~380KB RAM.

**Cold path:** CSS load no longer allocates per-rule selector vectors or
per-token strings; `splitOnChar`/`splitWhitespace` are gone, replaced
with callback-based tokenization (`forEachDelimitedToken`).

**Behavioral notes:**
- The selector `unordered_map` now uses an ASCII-case-insensitive
hash/equal. Selectors are stored with their original case rather than
pre-lowercased; the observable lookup result is unchanged.
- `stripTrailingImportant` is now case-insensitive (per CSS spec;
previously matched only lowercase `!important`).

**Cache compatibility:** `CSS_CACHE_VERSION` unchanged. Old caches
(lowercase selectors) load correctly under the new lookup; new caches
will contain verbatim-case selectors — both forms work.

---

### AI Usage

Did you use AI tools to help write this code? _**PARTIALLY**_
2026-06-04 18:05:32 -04:00
Eloren1 f04b8aa9a9 fix: apply progress bar offset from top instead of bottom edge (#2250) 2026-06-04 17:54:57 +03:00
Justin Mitchell d9bcef7a58 fix: Replace full-image cache buffer with streaming band buffer to reduce memory usage (#2230) 2026-06-04 10:46:22 -04:00
Uri TauberandJulia a60f31cdd4 fix: Crash on invalid font filename (#2253)
Co-authored-by: Julia <julia@uxj.io>
2026-06-04 10:34:34 -04:00
Uri TauberandJulia f055fdd774 fix: long-press back should move to the start of chapter (#2243)
Co-authored-by: Julia <julia@uxj.io>
2026-06-04 16:35:38 +03:00
Julia bb078bae09 fix: KOReader sync drift when syncing at chapter start (#2245) 2026-06-04 15:23:52 +03:00
Matteo Scopel 2d65808302 chore: small Italian fixes (#2252) 2026-06-03 14:43:43 +03:00
Matheus Martins 6e75e5a6be docs: Fix section numbering and table of contents in USER_GUIDE.md (#2244) 2026-06-02 22:12:00 +03:00
Uri Tauber db94a86fba fix: Skip Underline Calculations During Font Cache Scan Pass (#2237)
## Summary

This PR optimizes the text rendering process by skipping underline style
calculations and measurements during the initial font cache scanning
phase. This prevents excessive and unnecessary SD card reads on pages
with heavy use of underlines (e.g., Table of Contents pages).

### **The Problem**

During the first rendering pass (the font cache scan pass used to
collect text for prewarming), `GfxRenderer::drawText()` early-returns
after recording text as expected. However, `TextBlock::render()`
continued past this point to execute the underline decoration logic.

Because underline calculation calls `getTextWidth()` and
`getTextAdvanceX()`, it triggered immediate glyph lookups via
`EpdFont::getGlyph()`. Since the SD card font had not been prewarmed yet
at this stage, the lookups fell back to the `glyphMissHandler`,
resulting in hundreds of individual, slow SD card reads into a limited
8-slot ring buffer.

### **The Fix**

1. **Exposed Scan State:** Added `GfxRenderer::isFontCacheScanning()` to
safely check if the font cache manager is currently in
text-collection/scan mode.
2. **Bypassed Underline Logic:** Modified `TextBlock::render()` to check
this state and skip underline measurement and drawing entirely while
scanning is active.

> [!NOTE]
> The text itself is still properly captured for prewarming via
`drawText()`. The underlines will be safely and efficiently calculated
and drawn during the actual render pass after the fonts have been
completely prewarmed.

---

### 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-06-02 09:47:24 -04:00
Uri Tauber b12839d1d4 chore: Move opendyslexic from flash to SD (#2231) 2026-06-01 13:49:57 -04:00
Justin Mitchell 50e4d550fb Fix ghosting on pages following images in grayscale (#2226) 2026-05-31 23:33:26 -04:00
Julia 03f73fadc7 fix: avoid zip-wide css scan for large epubs (#2213)
## Summary

* **What is the goal of this PR?** 
* This PR ports over Crossink's handling of large EPUBs that helps
prevent crashes during open after the book metadata cache is built.

* **What changes are included?**
* Removes the post-indexing ZIP-wide CSS discovery pass that built an
in-memory map of every ZIP entry.
* Reuses `content.opf` parsing to collect declared CSS files without
writing spine entries again.
* Temporarily releases the loaded book metadata cache while rebuilding
CSS for cached books.
* Parses CSS before reloading `book.bin` after a fresh cache build,
leaving more heap available during CSS rule parsing.

## Additional Context

* User reported their EPUB opening fine on Crossink but would crash on
Crosspoint. Verified this claim on my own devices.
* The crash this addresses happened after `book.bin` was successfully
built, when CSS discovery allocated a large `unordered_map` for ~3k EPUB
ZIP entries.
* Tradeoff: CSS files not declared in `content.opf` are no longer
discovered by scanning the full ZIP. This avoids the high-risk memory
allocation but improperly formatted EPUBs (ones that don't declare their
CSS styles in `content.opf` will render without styling and fallback to
inline styles.
* User provided epub that was crashing prior to this change:
https://www.mediafire.com/file/g57ea4mj13iunvh/Quang+%C3%82m+Chi+Ngo%E1%BA%A1i+-+Nh%C4%A9+C%C4%83n.epub/file

---

### 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-31 18:31:06 -04:00
Wilhelm Schuster 0d64980d25 fix: update German translation (#2224) 2026-06-01 00:51:52 +03:00
Nathanael Maher 4fe80ba7a5 fix: bookmark percentage always 0% and page number starts at 0 (#2188)
## Summary

* Fix the issue described in #2176 where bookmark percentages are always
0%
* Also fixed the page number rendering to +1 because they are 0 index
based, but should be rendered starting from 1

---

### AI Usage

Did you use AI tools to help write this code? _**< NO >**_
2026-05-31 17:37:21 -04:00
Wilhelm Schuster b229428a17 chore: remove unused sleep timer interval strings (#2223) 2026-06-01 00:26:17 +03:00
Matteo Scopel 36d0e52e4b chore: new Italian fixes (#2218) 2026-05-31 17:39:15 +03:00
Uri Tauber 94c0ba466c feat: Hebrew localization (#2068) 2026-05-31 16:57:51 +03:00
muhas da2e4eabb0 fix(lang): Russian translate for new strings (#2217) 2026-05-31 16:05:47 +03:00
Matteo Scopel 3392b3e35a chore: update the Italian translation (#2207) 2026-05-30 23:42:19 +03:00
Matheus Martins 52e7446757 fix(docs): Section numbering in USER_GUIDE.md (#2195) 2026-05-30 23:34:44 +03:00
Jeremy Klein d83605e003 docs: add Claude Code contributor skills for firmware code quality (#2212) 2026-05-30 23:30:18 +03:00
WuTofuandJulia ebf0413fe1 feat: add custom sleep timer picker (#2206)
## Summary

* **What is the goal of this PR?**
Mainly fixes #2196, but also allows users to set a sleep time of 1~30
min or to never sleep.

* **What changes are included?**

1. Revert changes from #1948 and #2137 (Sorry @Uri-Tauber)
2. Cherrypick changes from
https://github.com/uxjulia/CrossInk/commit/0cb91c3dd1c63f0559d4392fadc4a8340af5bdde
and
https://github.com/uxjulia/CrossInk/commit/4e6bd634b6ede51ff1a7107ba3e8efcd152bc6ee
for a timeout interval picker UI, the custom sleep time feature, and
migrate the existing setting in v1.3.0, `sleepTimeout`, to
`sleepTimeoutMinutes` (Thanks! @uxjulia)
3. Add a "Never" at the far right of the timeout interval picker

---

### 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: Julia <julia@uxj.io>
2026-05-30 08:28:07 -04:00
Julia 66d9e4403a fix: redesign X3's UTC offset picker so it is easier to use (#2205)
## Summary

* **What is the goal of this PR?**
Improve the X3 clock UTC offset picker so it is clearer which part of
the offset will change when the user presses up/down.

* **What changes are included?**
Replaces the single underlined `UTC +/- H:MM` string with separate
editable fields for the sign, hour, and minutes.
The editable fields are spaced out, outlined, and the currently selected
field is highlighted with a light gray background.

## Additional Context

* This is a render-only UI change in `ClockOffsetActivity`; it does not
change how UTC offsets are stored, clamped, saved, or applied.
* The selected field still cycles with Confirm, and up/down still
adjusts the active sign/hour/minute value.

Screenshot of **current design** showing the sign (+/-) highlighted:
<img width="264" height="396" alt="old utc offset"
src="https://github.com/user-attachments/assets/c88d69ea-089c-47da-b5b3-ed0ebda5b112"
/>


Screenshot of **new design** showing the sign (+/-) highlighted:
<img width="264" height="396" alt="utc offset fix"
src="https://github.com/user-attachments/assets/f51da8b3-9aab-4f48-a9d1-21c828c01499"
/>

---

### 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-29 19:37:38 -04:00
Leopoldo Pla Sempere 174a24ba42 fix: complete Spanish, Catalan and Valencian strings (#2203) 2026-05-29 16:09:14 +03:00
Stefan Blixten Karlsson 9897025ded fix: swedish translation for strings added by #1337 and #2105 (#2197) 2026-05-29 11:13:57 +03:00
Uri TauberandZach Nelson f5bc554ae7 feat: add RTL support in epub and txt readers (#1700)
Co-authored-by: Zach Nelson <zach@zdnelson.com>
2026-05-29 03:25:17 -04:00
Angad Kandhari cc2079a578 docs: sync book.bin/section.bin version refs to source (#2169) 2026-05-29 03:07:57 -04:00
Djef0o 75a35f0578 chore: Update font size translation in French YAML (#2187) 2026-05-29 00:39:52 +03:00
Jeremy Klein f872f1f549 fix(docs): Remove fork with upstream attribution issues. (#2177)
Due to attribution issues with jpirnay's crosspoint fork, I am removing
it from our blessed list of forks.

I will not tolerate stolen code.
2026-05-27 22:21:10 -04:00
Justin Mitchell 02573dc592 chore: bump open-x4-sdk to 344c479 (restore X3 4-level grayscale LUT) (#2174) 2026-05-27 18:56:59 -04:00
muhasandmuhas dfefbfe9b5 feat(lang): Russian translate for new string (#2173)
Co-authored-by: muhas <mail@muhas.name>
2026-05-28 00:22:51 +03:00
36a3a0cc3a feat: epub bookmarks (#1337)
Co-authored-by: vedi0boy <nate@origin8publishing.com>
Co-authored-by: Uri Tauber <uritaube@gmail.com>
2026-05-27 15:52:01 -04:00
Uri Tauber bbb3e06eb1 fix: BOOK_CACHE_VERSION jump (#2161) 2026-05-26 22:13:09 +03:00
Vadim KaushanandClaude Sonnet 4.6 213972badc fix: navigate to TOC anchor when selecting sub-chapters (#1981)
Chapter selection previously only used the spine index, causing
navigation to always land on page 0 of the spine item. Sub-chapters that
share a spine file but differ by anchor (e.g. `chapter.xhtml#sec2`) were
silently ignored. Now the TOC anchor is passed through `ChapterResult`
and applied via the existing `pendingAnchor` mechanism.

## Summary

* This PR implements navigation to sub-chapters which didn't work
correctly previously. If a sub-chapter of the current top-level chapter
was selected, nothing happened. If a sub-chapter of another top-level
chapter was selected, reader switched to the beginning of the top-level
chapter.
* In addition, chapters now always start from a new page. This fixes
anchor to page calculation for the cases when the actual chapter content
doesn't fit on the page where the corresponding ToC anchor was found.

## Additional Context

* I might misuse `pendingAnchor` here which was previously used for
footnote navigation, please double check. I'm open to suggestions for
improvements.
* Note that the chapter selected by default when
`EpubReaderChapterSelectionActivity` opens is still wrong. I'm going to
fix this separately. This PR addresses only navigation to the selected
chapter.
* I tested this PR on my X4 and verified that navigation to a different
sub-chapter works correctly, both inside and outside the current spine.
* Some of the changes were borrowed from
https://github.com/crosspoint-reader/crosspoint-reader/pull/1455
---

### 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: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-26 12:14:32 -05:00
c5e861d71c feat: <sup> and <sub> support (#2131)
## Summary

* **What is the goal of this PR?** Support for `<sup>` and `<sub>` tags.

## Additional Context

This isn't my work, but @jpirnay 's (missing you here, man!). I migrated
his work from
https://github.com/jpirnay/crosspoint-reader/commit/bcd8c32cf26447ccc792cfedd2fdbfce4fee5210
with some micro-optimizations.

Screenshots: 

[Subscript-and-Superscript-Tests_ch2_p1_10pct_55632.bmp](https://github.com/user-attachments/files/28196936/Subscript-and-Superscript-Tests_ch2_p1_10pct_55632.bmp)

[Subscript-and-Superscript-Tests_ch3_p1_21pct_77473.bmp](https://github.com/user-attachments/files/28196937/Subscript-and-Superscript-Tests_ch3_p1_21pct_77473.bmp)


---

### 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 >**_

---------

Co-authored-by: jpirnay <jens@pirnay.com>
Co-authored-by: Julia <julia@uxj.io>
2026-05-26 12:38:40 -04:00
Zach Nelson 34e923d722 chore: Pin PNGdec lib_dep to 1.1.6 (#2154) 2026-05-26 01:06:24 -04:00
Jacob Latonis f4e7eaa198 feat: implement CSS enumeration through OPF directory when missing from manifest (#2148) 2026-05-25 21:23:19 -04:00
Zach Nelson 94d4b0c7bb ci: Cache PlatformIO packages between runs (#2142) 2026-05-25 17:03:10 -07:00
Zach Nelson 6dbfc29c7e test: Migrate unit tests to gtest, integrate with CI (#2144) 2026-05-25 15:51:53 -07:00
Jeremy Klein 4ee406897b feat: tiled grayscale rendering to drop the storeBwBuffer peak (#2106)
Tiled grayscale rendering to drop the storeBwBuffer peak largest
contiguous free block (the value that actually drives OOM on the C3)
from ~114 KB to ~82-90 KB.

This renders each grayscale plane band-by-band into a small (~8 KB)
scratch and
streams each band straight to controller RAM (community-sdk
writeGrayscalePlaneStrip), leaving the BW framebuffer intact. No save,
no
restore; controller RAM is re-synced for the next differential turn
directly
from the live framebuffer.

Three writers honor the active band target so per-band re-rendering
stays cheap
and correct:

- drawPixel (text) redirects writes to the band scratch and clips to it.
- renderCharImpl skips glyphs whose physical y-extent is outside the
band before
the bitmap decode (glyphIntersectsStrip), so the per-band re-render
doesn't
  pay N x glyph decode.
- DirectPixelWriter (images) writes the band scratch via getWriteTarget
instead
  of the framebuffer. Without this, image pixels wrote the live BW frame
directly and cleanup re-synced that corruption, leaving thin outlines
after
  navigating away from an image.

Controller specifics live in the SDK (X4 setRamArea windowing, X3 PTL);
the
reader checks supportsStripGrayscale() and is otherwise
controller-agnostic.

Measured on hardware (X4 and X3, text and images, visually correct):

- Grayscale scratch ~8 KB vs ~50 KB save; largest contiguous free block
held at
  full size during grayscale instead of dropping ~25-32 KB.
- X4 text page about +25 ms/page; X3 page time is dominated by its
intrinsic
  grayscale refresh, not tiling.

Depends on community-sdk #13 (the writeGrayscalePlaneStrip API). The
submodule
bump here points at that branch, so until #13 merges the submodule won't
resolve
from upstream and CI will fail there; keeping this a draft until then.
Will
rebase onto master and re-point the submodule to the merged SDK commit
once #13
lands.

Did you use AI tools to help write this code? partial
2026-05-25 18:03:01 -04:00
Joseph DiGiovanni 67973166e3 feat: Allow disabling side buttons in reader (#2105) 2026-05-25 18:00:27 -04:00
Jason HuebelandUri Tauber 50bc9325a4 docs: update USER_GUIDE.md for v1.1.0–v1.3.0 features (#2134)
Co-authored-by: Uri Tauber <uritaube@gmail.com>
2026-05-26 00:00:00 +03:00
WuTofu d736e52bdd fix: clear cache when deleting folders in FileBrowserActivity (#1892) 2026-05-25 16:47:35 -04:00
OrbisAI Security 91d17d9f48 perf: right-size (#2093) 2026-05-25 16:46:49 -04:00
Leopoldo Pla Sempere d5c28fa1f1 fix: refresh Catalan, add Valencian locale, and complete Spanish clock strings (#2063) 2026-05-25 16:41:48 -04:00
Leopoldo Pla Sempere f69650fb86 fix: validate OPF cover items as images (#2062) 2026-05-25 16:40:41 -04:00
Boris Faure e3298c6e43 docs: update presets on sd-card-fonts (#2110) 2026-05-25 16:31:38 -04:00
Chun Ming Lee 56ba9e4337 feat: add image auto-cropping in web epub optimizer (#2139) 2026-05-25 16:31:05 -04:00
Stefan Blixten Karlsson b56d3d853d fix: update Swedish translations and remove unused strings (#2090) 2026-05-25 16:30:22 -04:00
Zach Nelson e9120888fa refactor: Drop FsFile alias, use HalFile in downstream code (#2141) 2026-05-25 16:26:54 -04:00
Eloren1 d7797baff2 fix: Fill the gap under the screen for the progress bar (#2138) 2026-05-25 15:10:45 -04:00
Zach Nelson b53ac3d52c chore: Minor cleanup flagged by newer gcc (#2140)
## Summary

Minor cleanup flagged by newer gcc:
- Removed unused variables
- Removed unimplemented function declaration
- `static` -> `inline` to avoid per-TU duplication

---

### AI Usage

Did you use AI tools to help write this code? _**NO**_
2026-05-25 11:39:28 -05:00
Uri Tauber 38210be820 fix: SLEEP_TIMEOUT enum mismatch (#2137)
## Summary

* **What is the goal of this PR?** fixes #2132.

---

### 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-25 09:51:52 -05:00
Jeremy Klein 88b10c82a3 fix: serialize SdFat FsFile close through HalStorage mutex (#2135)
SdFat's SdSpiCard tracks SPI bus state with an unsynchronized
m_spiActive bool. When two tasks call into SdFat concurrently they can
confuse that state machine, ending with one task calling
SPIClass::endTransaction() against a paramLock the other task holds.
That trips FreeRTOS's xTaskPriorityDisinherit assert (tasks.c:5156,
pxTCB == pxCurrentTCBs[0]) and panics the system.

HalStorage already serialized every explicit method call via
storageMutex, but HalFile's destructor was `= default`, which let the
underlying SdFat FsFile destructor run close() outside any lock
(DESTRUCTOR_CLOSES_FILE=1). Any task that destructed a HalFile while
another task was mid-SD-op would race the unsynchronized state.

Move the locking discipline into HalFile::Impl::~Impl: an explicit
close() under StorageLock, then the FsFile member destructor's redundant
close() is a no-op. HalFile's special members can stay = default.

Switch storageMutex to xSemaphoreCreateRecursiveMutex so openFileForRead
and openFileForWrite can hold the lock while assigning to a HalFile&
out-param whose prior Impl needs locked teardown. Priority inheritance
still applies to recursive mutexes.

Also documented the no-bypass rule in CLAUDE.md: never call SdFat /
SdSpiCard / FsBaseFile / SDCardManager directly, never define
HAL_STORAGE_IMPL outside HalStorage.cpp.

Addresses my admittedly synthetic repro for #2047

Did you use AI tools to help write this code? partial
2026-05-24 21:48:33 -04:00
Jeremy Klein 19954aa1b5 refactor: route OTA version check through HttpDownloader (#2076)
With both OtaUpdater and HttpDownloader on esp_http_client
(https://github.com/crosspoint-reader/crosspoint-reader/pull/2074,
https://github.com/crosspoint-reader/crosspoint-reader/pull/2075),
checkForUpdate no longer needs its own client and event handler to fetch
the release JSON. It streams the response straight into
ReleaseJsonParser through a new HttpDownloader::fetchUrl(url,
DataCallback) overload, dropping the duplicate esp_http_client setup,
the HTTP_EVENT_ON_DATA handler, and the totalBytesReceived file global.

DataCallback hands body chunks to a callback without buffering. The
naive alternative, collecting the ~32KB JSON into a std::string, aborts
under -fno-exceptions: the growing allocation collides with the TLS
session's heap mid-fetch and operator new calls abort().

The OTA install path stays on esp_https_ota (flash-write streaming),
which has no HttpDownloader equivalent.

HttpDownloader.h must precede the lwip (esp_http_client) headers in
OtaUpdater.cpp, or Arduino/SdFat macros collide with lwip.


Did you use AI tools to help write this code? partial
2026-05-24 21:44:30 -04:00
Jeremy Klein 2823a4a2cd refactor: move HttpDownloader onto esp_http_client (#2075)
Based on the learnings from
https://github.com/crosspoint-reader/crosspoint-reader/pull/2074 , I
wanted to bring the same buffer savings to the rest of our HTTP Client
stack. That being said, HttpDownloader (fonts/OPDS) used the Arduino
HTTPClient.

HttpDownloader was the last consumer of the Arduino HTTPClient +
NetworkClientSecure stack. OtaUpdater already runs on esp_http_client,
so this drops the parallel HTTP/TLS implementation. It also fixes a
class of OPDS/font download failures: HTTPClient's setTimeout is uint16
and truncates, and its short per-read deadline killed slow or chunked
responses (the -11 / incomplete-data errors).

What changed:

- Rewrote fetchUrl/downloadToFile around esp_http_client with a
streaming open() -> fetch_headers() -> read() loop, manual redirect
following, and is_complete_data_received() as the completeness gate.
Body bytes go straight to the sink (OPDS parser stream, std::string, or
file), so nothing buffers the payload.

- HTTPS is now verified against the CA bundle instead of
NetworkClientSecure::setInsecure(). esp-tls is built with
CONFIG_ESP_TLS_INSECURE off, so an unverified handshake can't be set up
anyway; the model is public servers over verified https and local
servers over plain http (transport is chosen from the URL scheme).

** Self-signed https servers are no longer supported, by design. **

- timeout_ms is 60s; esp_http_client's timeout is uint32, so unlike
HTTPClient it doesn't silently truncate.

- HTTP buffers are 4096 (rx) / 1024 (tx). 4096 holds real OPDS server
headers; the GitHub release CDN sends more and logs a non-fatal
truncation warning, but the headers we read (Location, Content-Length)
come first and survive.

- Removed the now-unused UrlUtils::isHttpsUrl and a stale HTTPClient
comment in FontDownloadActivity.

Validated on device: OPDS browse and a 3.4 MB book download over
verified https, GitHub font downloads (crc-checked), redirect handling
matching curl, and slow/erroring servers surfaced correctly.


Did you use AI tools to help write this code? partial
2026-05-24 00:03:56 -04:00
Danila Yudin 929f290042 fix: close leaked resource handles (#2040) 2026-05-23 21:26:13 +03:00
Leopoldo Pla Sempere 0f021ea5f1 fix: normalize Wi-Fi spelling across remaining locales (#2094) 2026-05-23 21:01:12 +03:00
Justin Mitchell 99ab8b2772 fix: improves Edge case font/glyph handling (#2100) 2026-05-23 20:40:19 +03:00
Julia 7accc607af feat: ports hr tag rendering from crossink (#2117) 2026-05-23 09:47:16 -04:00
Julia f39ba7037f fix(settings): preserve quick resume timeout preference (#2101)
## Summary

### **What is the goal of this PR?**
This fixes an unintended settings side effect when cycling the `Sleep
Screen` option through `Quick Resume`.

Previously, selecting `Sleep Screen = Quick Resume` globally forced
`Quick Resume on Timeout = ON` and left it enabled even after the user
toggled `Sleep Screen` to another option within the same settings
session. Now the auto-enable behavior is scoped to the Settings screen
session:

- If `Quick Resume on Timeout` was already `ON` when entering Settings,
it stays `ON`.
- If it was `OFF`, selecting `Sleep Screen = Quick Resume` temporarily
turns it `ON`.
- If the user then switches away from `Quick Resume`, it turns back
`OFF`.

### **What changes are included?**

- Removes the global logic that permanently forced `Quick Resume on
Timeout` to `ON` whenever `Sleep Screen` was set to `Quick Resume`, even
if it was just due to toggling through the options.
- Adds Settings-screen session tracking so `Quick Resume on Timeout` is
only auto-enabled while the user has `Sleep Screen = Quick Resume`.
- Restores `Quick Resume on Timeout` back to `OFF` when the user
switches away, but only if it was `OFF` when they entered Settings.
- Preserves existing `ON` timeout preferences.
- Same behavior applies to the web settings

## Additional Context

- Tested this on device and via the settings UI
---

### 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-21 21:06:51 -04:00
WuTofu 2dd491b62e refactor: unify book cache clearing for epub, txt, and xtc files (#1875) 2026-05-21 14:44:10 +03:00
Matteo Scopel dc404b41c7 chore: fix the Italian translation (#2095) 2026-05-21 13:08:50 +03:00
Jeremy Klein 4ffc2a7e7e fix: sleep from a WiFi activity instead of silent-rebooting (#2092)
Unify the two splash-skip signals (RTC silent-reboot flag, SD
seamless-sleep flag) into one BootResume enum driving a single switch.
Storage unchanged; behavior-preserving apart from the fix.

Holding power to sleep from a WiFi activity (Font Download, OPDS, web
server, Calibre, KOReader sync) rebooted to home instead of sleeping.
goToSleep() runs the outgoing activity's onExit(), and those activities
call silentRestart() to clear heap fragmentation, so the heap-defrag
reboot fired before deep sleep could start.

enterDeepSleep() now latches deepSleepInProgress before goToSleep();
silentRestart()/silentRestartToReader() no-op while it's set. Deep sleep
is a full chip reset on wake, so it already clears the fragmentation the
reboot existed for.



Did you use AI tools to help write this code? partial
2026-05-21 00:03:46 -04:00
Julia c44555007b feat(settings): modify "Page as Sleep Screen" to "Quick Resume" options (#2089)
## Summary

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

Adds a clearer Quick Resume sleep-screen flow. The previous “Page as
Sleep Screen” behavior is now exposed as a dedicated `Sleep Screen >
Quick Resume `option, with the timeout-only behavior controlled by a
renamed `Quick Resume on Timeout `setting.

**What changes are included?**

- Adds `Quick Resume` as a new `Sleep Screen` option.
- Renames the old `Page as Sleep Screen` setting to `Quick Resume on
Timeout`.
- Changes that setting’s choices from `Never / After Timeout / Always`
to `OFF / ON`.
- Makes `Quick Resume on Timeout = ON` equivalent to the old `After
Timeout` behavior.
- Makes `Sleep Screen > Quick Resume` equivalent to the old `Always`
behavior.
- Automatically forces `Quick Resume on Timeout` to `ON` when `Sleep
Screen` is set to `Quick Resume`.
- Renames internal setting references from `seamlessSleepScreen` to
`quickResumeSleepScreen`.
- Updates translations for the renamed setting label.

**Additional Context**

- This is mostly a settings/labeling restructure around existing
behavior, not a new rendering path.
- The runtime quick-resume behavior still uses the existing saved
framebuffer / last-screen sleep flow.
- Review focus areas:
  - Sleep entry behavior from manual sleep vs timeout sleep.
- The automatic dependency where selecting `Sleep Screen > Quick Resume`
sets `Quick Resume on Timeout` to `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? _**< YES >**_

---
**New `Quick Resume` option for `Sleep Screen` will automatically set
`Quick Resume on Timeout` to `ON`**:
<img width="480" height="800" alt="quick resume"
src="https://github.com/user-attachments/assets/94c553fd-a122-47a8-add9-f29694f55566"
/>

**Example where a different sleep screen setting like `Cover` can be
used in combination with the `Quick Resume on Timeout` setting**:
<img width="480" height="800" alt="cover + quick resume"
src="https://github.com/user-attachments/assets/dd18ce18-230b-4b78-808b-ac85f5e7d5d8"
/>
2026-05-20 22:43:48 -04:00
Vadim Kaushan d9aa5b4de1 fix: take orientation into account for border generation in ScreenshotUtil (#1977)
## Summary

Previously `ScreenshotUtil` used physical display size to draw a border
around the screen contents. Because of this, in landscape orientation
the border was shown as a broken square. This PR changes border drawing
to use logical screen size instead of a physical display size to take
orientation into account.

## Additional Context

* Tested on X4 in all 4 reading orientations. Behavior is now correct,
however it doesn't look perfect on my X4: the border is much closer to
the physical top side of the display than to the other sides. This might
be related to assembly variation during manufacturing, but it might as
well be related to the way a eink controller is connected to the display
(controller supports bigger display sizes, so an offset may be present).

---

### 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-20 15:48:06 -04:00
jpirnayandArthur Tazhitdinov a7aa4c55d8 fix: Prefer epub format over derived formats when downloading from opds server (#1480)
## Summary

* **What is the goal of this PR?** Prefer epub format over kepub or
other formats offered from an OPDS server
* **What changes are included?**

## Additional Context

Should address #1419 

---

### 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: Arthur Tazhitdinov <lisnake@gmail.com>
2026-05-20 11:46:11 -05:00
Jeremy Klein 3800179595 fix: wire through silent restart clear resume state with sdk (#2033)
## Summary

After a silent reboot, there was a small window where the esp32 would
listen for button presses but the full refresh would hold the event
loop. This gave a UX experience where the silent reboot had completed to
the home screen, a user taps select (at any time during the process),
and they find themselves unexpectedly in a book.

## Additional Context

This must land after
https://github.com/crosspoint-reader/community-sdk/pull/11 and will need
the submodule SHA changes included in. 

---

### 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-20 08:58:43 -04:00
Jeremy Klein 0b9d1a7d23 fix: keep wifi OTA off the heap floor (#2074)
OTA install streams the full 5.8MB image over a multi-minute TLS session
while wifi/LWIP already holds the big internal arena. Measured on
device, the arena bottomed out at ~7.7KB free with the largest
contiguous block down to ~2.4KB; for 80% of the download there wasn't
even a contiguous 8KB block. It finishes on a clean heap but tips into
OOM for anyone carrying more pre-OTA fragmentation.

Two avoidable drains, both in OtaUpdater:

- The esp_http_client RX/TX buffers were 8192/8192 on both the version
check and the install. RX only has to hold response headers (bodies
stream through the parser / OTA writer) and TX only carries our GET, so
trim both to 4096/1024. 4096 still fits the github->CDN redirect
headers; the 512 IDF default truncates them, which is why they got
oversized in the first place.

- installUpdate fired the progress callback every ~100ms perform
iteration, waking the render task on every tick. Its framebuffer work
fights the TLS session for the same arena, and epd can't really repaint
faster than a percent anyway. Throttle it to whole-percent changes.

On device, combined: floor 7.7KB -> 19KB, worstcase contiguous block
2.4KB -> 21KB, zero sub 8KB iterations across the whole download.

KOReaderSyncClient already uses small buffers; HttpDownloader is on the
Arduino HTTPClient stack with no equivalent knob, so neither changed.



Did you use AI tools to help write this code? partial, heap-exploration
assisted by Claude.
2026-05-20 08:46:49 -04:00
Justin Mitchell 252a64fa97 chore: Add funding badge for contributors in README (#2072) 2026-05-19 22:42:45 -04:00
Jeremy Klein 75764bc6eb fix: 0 -> 1, even more deep-sleep fix (#2073)
setTimeout(0) on the serial could trigger a subtle but obnoxious
underflow.

Eat a milli, save a reset button.


Did you use AI tools to help write this code? no
2026-05-19 22:40:44 -04:00
Dave Allie 0ddefdc0c9 chore: Remove FUNDING.yml (#2071)
## Summary

* I am no longer maintaining or running the project, so avoiding
collecting money for nothing
* This will likely be updated by Justin in the near future with
different details
* See
https://github.com/crosspoint-reader/crosspoint-reader/discussions/2070
2026-05-20 11:33:32 +10:00
Eloren1andClaude Sonnet 4.6 41e6e15229 feat: Seamless sleep/wake screens for displaying book pages during deep sleep (#2064)
<img width="605" height="454" alt="image"
src="https://github.com/user-attachments/assets/bfd84afe-3b58-436e-9a5d-539af3ec3d4e"
/>

Actualized "Last" sleep screen setting from previous PRs, rebranded as a
~~`Seamless Sleep`~~ `Page as Sleep Screen` option with more
improvements.


https://github.com/user-attachments/assets/59029ba6-007e-4841-abfa-f680d7e98b79

---

New option: `Page as Sleep Screen` - `Never (default)`, `After Timeout`,
`Always`

When enabled, it seamlessly sleeps on timeout or power off, making a
fast refresh for the moon icon. When waking up, we still show the last
page, instead of the boot screen, making it fully seamless.

I tried different icons such as "refresh arrow" and others, but they
looked not as nice as 3 simple dots.

With this mode, the device turns off 4 seconds faster. And has 6 seconds
less delay when turning back on. Much more responsive.

Previously, even a 10-minute timeout sometimes wasn't enough, and I'd
worry about seeing the book cover. It's now easier to use a shorter
sleep timeout: if I get distracted during a reading session but don't
want to stop, the new screen is much more inviting to come back to.

---

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

---

Test v1.3.0 firmware.bin file
[download](https://github.com/user-attachments/files/28015193/firmware.zip)

Based on PRs #410 and #495

Closes #400, #1649

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-19 18:45:28 -04:00
Danila Yudin 082b5f295b docs: update delete endpoint reference (#1940)
## Summary

Update the `/delete` endpoint reference to match the current web server
handler.

## Details

The endpoint docs still described a `type` form field and the old
`Deleted successfully` response. The handler now accepts either `path`
for a single item or `paths` as a JSON array for multi-delete, infers
file versus folder from the SD card entry, and returns `All items
deleted successfully` when all deletes complete.

This updates the curl examples, parameter table, success response, and
error response list to match the implemented behavior.

## Validation

- Compared the documented parameters and response strings with
`CrossPointWebServer::handleDelete()`
- `git diff --check`
2026-05-19 17:39:04 -05:00
Jackson beb7876ccf feat: Make page turn naturally follow orientation (#2023) 2026-05-19 23:13:27 +03:00
Uri Tauber b69111bea5 refactor: Consolidate theme rendering into ThemeMetrics (#1868) 2026-05-19 22:50:26 +03:00
Jeremy Klein 86a9510957 fix: stabilize deep sleep wake on USB power (#2060)
When the device went into deep sleep while plugged into USB, a
power-button press would occasionally not wake it. The display held its
last frame, the chip stayed in deep sleep, and recovery required an
unplug + reset + hold-power cycle. On battery the symptom never surfaced
because the power button physically re-energises the chip.

Two peripherals were holding power domains alive across the deep sleep
boundary and interfering with the configured GPIO wake on the power
button:

1. HWCDC. Once Serial is initialized, the USB Serial/JTAG peripheral
keeps its power domain configured even with TX timeout at zero and no
host draining. Tear it down with Serial.end() in
HalPowerManager::startDeepSleep, gated by ENABLE_SERIAL_LOG to match the
Serial.begin site. This hit me if I was charging off my computer.

2. WiFi. enterDeepSleep had no WiFi teardown, so sleeping from any
network-using activity left the modem domain alive. Call
WiFi.disconnect(true) + WiFi.mode(WIFI_OFF) when WiFi is active. Wake
from deep sleep is effectively a chip reset, so no WiFi state needs to
survive. While this doesn't cause higher power drain, it apparently was
causing issues where I'd occasionally have the chip hang on sleep
transition from a wifi activity.

Confirmed on device.


Did you use AI tools to help write this code? partial
2026-05-19 12:04:38 -04:00
Jeremy Klein acc1ed4358 fix: guard DC writes in JPEGDEC MCU_SKIP path (#2058)
EIGHT_BIT_GRAYSCALE decode of a 3-component progressive JPEG calls
JPEGDecodeMCU_P with MCU_SKIP for Cb and Cr after every Y MCU. The
existing safe-pMCU patch redirects the wild pointer to &sMCUs[0] but
leaves the DC store unguarded, so each chroma skip overwrites the
just-decoded Y DC with the chroma DC predictor. Output reads sMCUs[0],
gets the trailing Cr DC (~0), and renders an all-black image.

Add `if (iMCU >= 0)` guards to the two pMCU[0] writes (main DC store and
successive-approximation update). The pointer redirect stays as the AC
wild-pointer defense; the new guards stop the silent corruption at
sMCUs[0]. The two fixes are independent and both required.

fixes the progressive 8bit grayscale jpeg regression in 1.3.0

Did you use AI tools to help write this code? partial
2026-05-19 09:52:29 -04:00
Uri Tauber 08461c08e7 fix: small QoL: return to the last selected menu location (#1629)
## Summary

* **What is the goal of this PR?** Small UX improvement to the Home
screen by preserving the last selected cursor position when returning to
it.

It supersedes #985 and #1103, which are both significantly outdated and
hundreds of commits behind master.

---

### AI Usage

Did you use AI tools to help write this code? _**< YES >**_
2026-05-19 08:41:39 -05:00
Justin Mitchell dac7fef49d fix: Update documentation with new features and links (#1991) 2026-05-19 07:32:30 +03:00
Jeremy Klein a14c8e762d perf: shrink HomeActivity cover cache from 48KB framebuffer to 16KB region (#2035)
On-device repro showed the cover snapshot pinning ~52KB of contiguous
heap (cloning the full 48KB framebuffer with malloc overhead). MaxAlloc
on Home was 61KB; nothing was leaving headroom for HTTPS, which needs
30-50KB contiguous for the mbedTLS handshake.

Add region-aware framebuffer helpers to GfxRenderer that translate a
logical rect through rotateCoordinates and copy only the byte range that
contains the rotated rect. HomeActivity records the tile rect it passes
to drawRecentBookCover and caches only that subregion.

Measured on device (X3, Portrait):

  Idle on Home    | Free 102K -> 139K  | MaxAlloc 61K -> 115K
  Mid-EPUB-read   | Free  81K -> 134K  | MaxAlloc 70K -> 115K
  Cover cache     |        ~52K -> ~16K (per allocation)

Works in all four orientations because the bounds helper samples the
four logical corners through the existing rotation, so the cached byte
range always covers the pixels the theme could have drawn into.

Savings will vary with theme, but should be significant across all.
2026-05-18 22:41:02 -04:00
Jeremy Klein a525606d7f fix: USB serial logs now flow on cold+warm boot without jiggle (#2034)
The "logs only flow if you unplug and replug the USB cable at the right
moment" symptom traced to two interacting problems with the ESP32-C3 USB
Serial/JTAG controller (HWCDC):

1. Serial.begin was gated on gpio.isUsbConnected(). That check sampled
USB state at one specific microsecond during boot. If USB enumeration on
the host hadn't completed by that moment (common after a reset that
auto- reconnects a moment later), Serial was never initialized and
stayed dead until the next boot where the timing happened to win.

2. HWCDC writes block for up to the configured TX timeout (default 250
ms) when the host has the port open but isn't actively draining — a
state the macOS USB CDC stack enters intermittently after reconnect. The
firmware then appears to hang on logging until a USB unplug+replug
cycles the peripheral and flushes the TX FIFO.

Fix: move the Serial init to the very top of setup() with a 250 ms stall
before Serial.begin (lets the USB peripheral power-on and host
enumeration complete on cold boot), and call logSerial.setTxTimeoutMs(0)
so writes drop bytes harmlessly when the host is slow instead of
stalling the firmware. Both warm reboot and cold power-on now produce
logs immediately.

Did you use AI tools to help write this code? partial
2026-05-18 22:26:25 -04:00
KemoNine df53faab91 feat: allow removing book from recent list (#2045)
## Summary

Add ability to long press 'confirm' on a book in the recent books list
to be prompted to remove it from the list.

---

### 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-18 22:16:33 -04:00
Justin Mitchell f2adefe729 chore: Update open-x4-sdk submodule for faster page turns on x3 (#2055) 2026-05-18 21:41:40 -04:00
KemoNine 6a98c2d865 feat: add setting that allows removing books from recent list when read (#2043) 2026-05-18 21:13:41 -04:00
Justinian cfd3a381ed feat: X3 clock display with DS3231 RTC and NTP sync (#1612) 2026-05-18 21:06:56 -04:00
Jeremy Klein 151bf1dae4 fix: silent-restart on exit from KOReader auth and OTA update (#2036)
PR #1908 silent-restarts on exit from any wifi-using activity to defuse
LWIP/mbedTLS heap fragmentation, but two of the wifi-using paths slipped
through that audit:

  KOReaderAuthActivity (Settings -> KOReader sync -> Authenticate)
  OtaUpdateActivity    (Settings -> Check for update, back-out paths)

Both used WiFi.disconnect + WiFi.mode(WIFI_OFF) on exit and returned
control to Settings, leaving ~50KB of contiguous heap stranded for the
rest of the session.

Mirror the FontDownloadActivity pattern: if WiFi was activated,
disconnect and silentRestart. OTA's success path is unchanged:
SHUTTING_DOWN already calls plain ESP.restart() so the new firmware
boots normally; only the cancel/fail/no-update back-out paths now go
through silentRestart().


Did you use AI tools to help write this code? partial
2026-05-18 21:04:11 -04:00
KemoNine 06d28d6ffa feat: port crossink 'read book move' feature to crosspoint (#2032) 2026-05-18 12:39:50 -04:00
CaptainFrito 8a11f44571 feat: Themed reader menus (#1072) 2026-05-18 17:35:27 +03:00
WuTofu 061c7688a4 chore: add 3-minute sleep option (#1948) 2026-05-18 00:17:24 +03:00
muhasandmuhas 510ee10153 feat: update Russian translation (#2017)
Co-authored-by: muhas <mail@muhas.name>
2026-05-17 15:17:56 +03:00
zgredexandJustin Mitchell c6d116024c fix: harden EPUB optimiser UI gating, size reporting, and picker teardown (#1947)
Co-authored-by: Justin Mitchell <justin@jmitch.com>
2026-05-17 12:12:56 +03:00
Matteo Scopel 85e08f9a93 feat: add the Domitian font family (#2016) 2026-05-17 12:08:16 +03:00
Justin Mitchell 0af0ad5a17 fix: bump open-x4-sdk to clear grayscale state after AA cleanup (#2022)
Pulls in community-sdk PR #9, which clears inGrayscaleMode inside
cleanupGrayscaleBuffers() after the restored BW frame is written back
into RED RAM. Without this, the next BW page turn would still see the
flag set and trigger a redundant grayscaleRevert() refresh, producing
visible ghosting on the X4 with text anti-aliasing enabled.

Regression introduced by SDK commit 0a8ada2 (factory LUT grayscale
support), which removed a redundant inGrayscaleMode guard in
grayscaleRevert() and so caused the cleanup to actually run for the
first time.

Bypassing rules to avoid this going stale and all nightly builds being broken for x4 users
2026-05-17 03:45:11 -04:00
KemoNine 93e81daf41 fix: prune books missing form sd card in recent books list (#1959) 2026-05-16 22:03:49 +03:00
mvidelatraduc a3e51f9b1e chore: Update spanish.yaml (#2011) 2026-05-16 21:55:27 +03:00
Blue 90d4c885e1 fix: update URL-encoded image during EPUB optimization (#1985)
## Summary

* **What is the goal of this PR?**  
Fix EPUB optimization when XHTML image references are URL-encoded.

* **What changes are included?**  
The optimizer already converts image files to `.jpg`, but XHTML files
could still reference the original URL-encoded image path, for example:

```html
<img src="images/wensday%201%20full%202.png">
````

The optimized EPUB then contained the converted file:

```text
images/wensday 1 full 2.jpg
```

but the XHTML still pointed to the old `.png`, so CrossPoint failed to
extract/render the image.

The issue was that the previous replacement logic matched only the plain
filename form, such as:

```text
wensday 1 full 2.png
```

but not the URL-encoded form:

```text
wensday%201%20full%202.png
```

This PR updates XHTML image `src` attributes through the existing
DOMParser pass by decoding and resolving the image path before matching
it against renamed images.

After this fix, the optimized EPUB correctly rewrites the XHTML image
reference to the generated `.jpg`, and the image renders correctly.

---

### 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**_

---

Please let me know if you have questions,
Thank you!
2026-05-15 21:30:49 -05:00
Kira ee947b06d1 fix: prevent card overflow on screens (#1943)
## Summary

prevent card overflow

## Additional Context

<img width="1920" height="1080" alt="bug"
src="https://github.com/user-attachments/assets/df84e233-908e-4ce1-8289-d0e9b579bc13"
/>
<img width="1920" height="1080" alt="Bug"
src="https://github.com/user-attachments/assets/cfd20f51-6421-4271-9a62-9c1987cc0dd0"
/>
<img width="1920" height="1080" alt="fix"
src="https://github.com/user-attachments/assets/4fbe83fe-5376-4393-bd45-a825f24e19e3"
/>
<img width="1920" height="1080" alt="fix2"
src="https://github.com/user-attachments/assets/4397848e-d8fa-4c51-ab4d-c32b2fcf33d1"
/>


---

### AI Usage

Did you use AI tools to help write this code? _**NO**_
2026-05-15 21:29:38 -05:00
marcinoktawian 28b907321e fix: use power button held time for shutdown logic (#1890)
## Summary

* **What is the goal of this PR?**
Fix incorrect power button long-press detection during shutdown/wake
verification by introducing dedicated power button timing logic.
* **What changes are included?**
* Added getPowerButtonHeldTime() to HalGPIO as a wrapper over input
manager logic
* Replaced generic getHeldTime() usage with power-button-specific timing
in verifyPowerButtonWakeup()
* Ensures shutdown/wake decision is based only on actual power button
hold duration, not any-button timing
  * Minor header update for new API exposure in HalGPIO.h
## Additional Context

This fixes a bug where holding another button while briefly pressing the
power button could incorrectly trigger shutdown behavior due to shared
timing state (getHeldTime()).

The change isolates power button timing to prevent cross-button
interference and makes shutdown logic reliable during multi-button
interactions.

No behavioral changes are expected outside of power-button handling
logic.

**Dependencies**
- SDK PR: https://github.com/crosspoint-reader/community-sdk/pull/3

This PR requires the `community-sdk` submodule to be updated after the
SDK change is merged.

- Fixes: #1881

---

### AI Usage
Did you use AI tools to help write this code? _**PARTIALLY**_
2026-05-15 21:28:52 -05:00
Stefan Blixten Karlsson 77afea4d95 feat: Add swedish hyphenation (#1637)
## Summary

* Add swedish hyphenation using scripts/update_hypenation.sh
* Add hyphenation test data using the Swedish translation of Andy Weir's
Project Hail Mary

---

### 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-15 21:27:52 -05:00
WuTofu 2f342508bc fix: several QoL updates for SD font's UI (#1965)
## Summary

* **What is the goal of this PR?**  
Improve the UI based on feedback from someone on discord

> Downloading ALL fonts feature.
> 1.1 Disable sleep when downloading, in my case went directly to sleep
just right after downloading.
> 1.2 It would be great to have and overall progress indicator as we
only have the indication of each font family
> 1.3 Any cancel or pause function might come in handy in case battery
is running out and then resume or retry with pending fonts

* **What changes are included?**  
- Now the UI can show overall progress across every file being
downloaded in the batch, not just progress inside the current family.
- Extended `HttpDownloader::downloadToFile()` to accept a cancel flag
and abort the download.
- Rendered a cancel button in the font download UI while a download is
in progress.
- `preventAutoSleep()` in `FontDownloadActivity.h` now returns true for
`state_ == COMPLETE` and `state_ == ERROR` in addition to
`LOADING_MANIFEST` and `DOWNLOADING`

## Additional Context

Not very satisfied with how `HttpDownloader.cpp` is right now, might try
to refactor it after v1.3.0

---

### 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-15 21:25:48 -05:00
luca 7bb1f7ed76 fix: update Italian translation (#1970)
## Summary
* **What is the goal of this PR?** Update the Italian translation.
* **What changes are included?** Took the latest `english.yaml` as
reference and updated `italian.yaml` accordingly, translating new
strings and revising existing ones where needed. Specific changes can be
inspected from the diff.

## Additional Context
* Nothing special to flag — happy to adjust any wording the reviewer
disagrees with.

---
### 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**_ — Claude
provided a first-pass draft; I revised and rewrote a substantial portion
by hand.
2026-05-15 20:52:42 -05:00
Jeremy Klein 7acc31bc34 fix: silent-reboot on wifi activity exit to clear heap fragmentation (#1908)
WiFi/LWIP/netif teardown scatters long-lived allocations across the
heap, leaving ~50KB of contiguous space unrecoverable without a reboot.

Reboot the SoC on exit from any wifi-using activity to guarantee a clean
heap. An RTC_NOINIT flag survives the reboot and tells setup() to skip
the boot splash and route the user back where they came from:
  - File transfer / Calibre / OPDS / Font download -> home
  - KOReader sync -> currently-open EPUB

Activities check WiFi.getMode() before rebooting, so backing out of the
network mode menu without joining doesn't trigger a cycle. KOSync also
esp_wifi_stop()s after the sync result so the radio is off while the
user reads it; full teardown happens at the reboot.


## Additional Context

The silent reboot skips the booting splash screen - it visibly looks
like a screen refresh. This does cause a disconnection/reconnection blip
for developers actively pulling logs over serial, but `pio device
monitor` and the like successfully reconnect and feed in the early boot
serial.
as an example: 
```
[256676] [DBG] [ACT] Exiting activity: KOReaderSync
[256706] [DBG] [MAIN] Silent restart (target=reader)

ESP-ROM:esp32c3-api1-20210207
Build:Feb  7 2021
rst:0xc (RTC_SW_CPU_RST),boot:0xf (SPI_FAST_FLASH_BOOT)
Saved PC:0x403872bc
SPIWP:0xee
mode:DIO, clock div:1
load:0x3fcd72a0,len:0x990
load:0x403cbf10,len:0xac8
load:0x403ce710,len:0x4d28
entry 0x403cbf10
[22] [INF] [MAIN] Hardware detect: X4
[29] [SD] SD card detected
[43] [DBG] [CPS] Settings loaded from file
[58] [DBG] [KRS] Loaded KOReader credentials for user: jeremydk
[69] [DBG] [OPS] Loaded 1 OPDS servers from file
[69] [DBG] [UI] Using Lyra theme
[70] [DBG] [MAIN] Starting CrossPoint version 1.2.0-dev-detached-bde75787

...

[203] [DBG] [ACT] Entering activity: Reader
[211] [DBG] [EBP] Loading ePub: /Halting State - Charles Stross.epub
[221] [DBG] [BMC] Loaded cache data: 51 spine, 41 TOC entries
[246] [DBG] [CSS] Loaded 41 rules from cache
[247] [DBG] [EBP] Loaded ePub: /Halting State - Charles Stross.epub
```
---
### 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-15 19:27:54 -05:00
938ca3fac1 chore: Update version to 1.3.0 (#1827)
## Summary

This release adds SD card fonts — the most-requested feature since
launch — brings the X3 to first-class status, redesigns the on-screen
keyboard, overhauls OPDS, and ships SD-card firmware updates. 144
changes from 53 contributors, 32 of whom are new to the project.

**🔠 SD Card Fonts**
Custom fonts are here. A complete font subsystem lets you install and
use fonts beyond the three built-in families. A new `.cpfont` binary
format packs multiple styles (regular, bold, italic, bold-italic) into a
single file per size, with on-demand glyph loading from the SD card. A
two-pass prewarm renderer bulk-reads glyphs per page, achieving
near-flash performance for Latin text and viable CJK rendering. Fonts
can be downloaded over WiFi directly from the device, uploaded via the
web interface, or copied manually to the SD card. The build pipeline
ships a 17-family font library (serif, sans, mono, accessibility) with
CI distribution via a dedicated crosspoint-fonts repository. As a bonus,
CJK characters no longer get spurious hyphens at line breaks, and an
advance-table cache eliminates 30+ second stalls during CJK section
indexing.

**📱 X3 Comes of Age**
The X3 graduates from initial bring-up to a proper target. Grayscale
antialiasing is sharper, EPUB images render correctly, OTA updates work,
and sleep screen dimensions are dialed in. The headline addition:
gyroscope-based tilt page turning via the QMI8658 IMU — tilt the device
to turn pages hands-free. SD-card firmware update support and X3
bootloader compatibility mean users can update without a USB connection.

**⌨️ Redesigned On-Screen Keyboard**
The keyboard has been completely redesigned with improved layout, better
key feedback, and a fix for the space key barely moving the cursor. Text
entry across WiFi setup, OPDS search, and KOSync login is noticeably
smoother.

**👁️ Focus Reading**
A new reading mode bolds the initial characters of each word (similar to
Bionic Reading) to create artificial fixation points, helping improve
reading speed and focus. The bolding ratio is 45%, with a minimum of 1
character and a maximum of 9, applied dynamically during indexing.

**📚 OPDS Overhaul**
OPDS gains in-catalog search with next/prev page navigation, support for
multiple servers, correct handling of relative paths and query
parameters (fixing CopyParty compatibility), and KOReader-compatible
download filenames.

**🔤 Text Rendering Refinements**
Combining marks (diacritics) now use font metrics for positioning
instead of heuristics, proportional numeral spacing is supported, and
differential rounding eliminates uneven inter-glyph gaps. Hyphenation
now recognizes ISO 639-2 language codes, nested block-level CSS styles
are tracked correctly, and horizontal CSS insets are capped at 2em to
prevent runaway margins. Bookerly has been replaced with Noto Serif for
licensing reasons.

**🎨 New Theme: RoundedRaff**
A new rounded theme joins the theme picker, with fixes for sleep cover
crop grid artifacts.

**🔋 Battery & Power**
Battery percentage smoothing on the X4 eliminates jittery readings. A
short press on the power button can be set to trigger a manual screen
refresh — handy for clearing ghosting.

**📶 WiFi & Networking**
WiFi connections now self-heal from transient drops without manual
intervention, and a dBm signal strength indicator appears during web
server sessions. WiFi networks can be edited directly from the web UI.

**🔄 KOSync**
Reading position sync is significantly more accurate. The old
character-offset approach frequently landed on the wrong paragraph after
syncing between devices — the new xpath-based mapping syncs at the
paragraph level, matching KOReader's own behavior. A separate fix
switches the HTTP layer to `esp_http_client`, and the reader now
releases ~65KB of EPUB heap before the TLS handshake — together these
eliminate the out-of-memory crashes that plagued KOSync on large books.

**🛡️ Stability**
Two memory leaks patched, a wild pointer crash in JPEGDEC MCU_SKIP
handling fixed, boot loops with large XTC files eliminated, legacy XTC
headers supported, the OTA updater now streams GitHub release JSON
instead of buffering it in RAM, and a JPEG downscaler y-axis scale
factor bug is corrected.

**🌐 Languages**
Slovenian is new. Russian, Ukrainian, Swedish, Italian, and Spanish
translations received significant updates.

---

Also in this release: **SD-card firmware updates without USB**, **file
extensions in the file browser**, **full path bar navigation**,
**end-of-book navigation improvements**, **XTC status bar**, **smarter
"Cover + Custom" sleep screens**, **set sleep cover from the BMP
viewer**, **orientation-aware popups**, **page turn buttons that follow
orientation**, **long-press delete for directories**, **context-aware
screenshot filenames with book title**, **crash reason displayed on
boot**, **empty line rendering in the TXT reader**, **wallpaper recency
buffer to prevent clustering**, **font family deletion from the
device**, **next/prev labels in the BMP viewer**, **non-breaking space
justification fix**, **README guidance for USB-locked third-party Xteink
units**, and a long tail of web UI polish, i18n memory optimizations,
and code quality improvements.

## What's Changed

### Features

* feat: add SD card font support with on-device download and web
management by @adriancaruana, @znelson, @itsthisjustin, @jpirnay, and
@mcrosson
* feat: Initial support for the x3 by @itsthisjustin in
https://github.com/crosspoint-reader/crosspoint-reader/pull/875
* feat: X3 grayscale antialiasing improvements by @juicecultus in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1607
* feat: X3 gyroscope-based tilt page turning via QMI8658 IMU by
@juicecultus in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1636
* feat(update): SD-card firmware update + X3 bootloader compatibility by
@eunchurn in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1786
* feat: self-heal from transient WiFi loss, add dBm indicator during
WebServerActivity by @jeremydk in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1780
* feat: edit wifi networks in webui by @osteotek in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1743
* feat: add OPDS search support & next/prev page navigation by @rxmmah
in https://github.com/crosspoint-reader/crosspoint-reader/pull/1462
* feat: Support for multiple OPDS servers by @osteotek in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1209
* feat: Adjust Navigation at End of Book by @nscheung in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1425
* feat: Display file extensions in File Browser by @CaptainFrito in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1019
* feat: show full path bar in file browser by @zgredex in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1411
* feat: enable manual screen refresh on power button short press by
@bdeshi in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1626
* feat: Rework "Cover + Custom" sleep screens to show covers only when
currently reading by @iandchasse in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1256
* feat: Set sleep cover from BMP viewer by @el in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1104
* feat: show crash reason on boot by @ngxson in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1453
* feat: Support for proportional numeral spacing by @znelson in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1414
* feat: add orientation-aware popups for reader activities by @mrtnvgr
in https://github.com/crosspoint-reader/crosspoint-reader/pull/1428
* feat: smooth battery percentage for x4 by @jonvex in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1635
* feat: context-aware screenshot filenames with book title by
@jonstieglitz in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1589
* feat(theme): add roundedraff theme and fix sleep cover crop grid
artifacts by @bunsoootchi in
https://github.com/crosspoint-reader/crosspoint-reader/pull/918
* feat: Page turn button orientation change by @mchuck in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1069
* feat: Status bar for XTC files by @leecming82 in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1849
* feat: enhance long press action to delete both files and directories
by @WuTofu in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1803
* feat: Added Slovenian translation by @thehijacker in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1551
* feat: focus reading by @vjapolitzer in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1670
* feat: add next / prev labels to bmp viewer by @Telemaniaka in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1852
* feat: add font family deletion functionality by @WuTofu in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1919
* feat: separate into "Download All" and "Update All" in font manager by
@WuTofu in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1955
* feat: verify CRC32 checksum for font files by @WuTofu in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1904
* feat: increase default weight of Bitter font for improved rendering by
@uxjulia in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1922
* feat: allow unnamed intervals by @steka in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1903

### Fixes

* fix: epub images not rendering correctly on x3 by @itsthisjustin in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1572
* fix: OTA update on x3 and progress bar on x4 and x3 by @itsthisjustin
in https://github.com/crosspoint-reader/crosspoint-reader/pull/1805
* fix: boot looping when opening large XTC files by @itsthisjustin in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1648
* fix: Wild pointer crash in JPEGDEC MCU_SKIP handling by @itsthisjustin
in https://github.com/crosspoint-reader/crosspoint-reader/pull/1627
* fix: two small memory leaks by @Uri-Tauber in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1628
* fix: use esp_http_client for KOSync to prevent TLS OOM on ESP32-C3 by
@trilwu in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1381
* fix: Read GH release JSON as stream in OTA updater by @znelson in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1810
* fix: support legacy XTC file headers where pageTableOffset=48 by
@uxjulia in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1816
* fix: Use font metrics for combining mark positioning by @znelson in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1310
* fix: Use differential rounding for consistent inter-glyph spacing by
@znelson in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1413
* fix: Support hyphenation for EPUBs using ISO 639-2 language codes by
@znelson in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1461
* fix: Track block style stack for nested styles by @daveallie in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1582
* fix: cap per-side horizontal CSS inset at 2em by @rhoopr in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1694
* fix: increase loadable epub size by @CSCMe in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1638
* fix: Switch to xpath map for paragraph level syncing in KOSync by
@itsthisjustin in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1686
* fix: free Epub RAM and simplify KOSync navigation via ActivityManager
by @wylanswets in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1860
* fix: improve KOSync bidirectional position matching accuracy by
@wylanswets in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1897
* fix: Fix failing very first wifi connection attempt by @jpirnay in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1521
* fix: avoid skipping chapter after screenshot by @Mraulio in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1625
* fix: back navigation from BMPViewer by @Telemaniaka in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1597
* fix: Fix ghosting on exit of BMPViewer by @jpirnay in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1432
* fix: make footnotes consider orientation for gutters by @Telemaniaka
in https://github.com/crosspoint-reader/crosspoint-reader/pull/1665
* fix: footnote link text by @steka in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1666
* fix: Erroneous navigation with long filenames in footnote links by
@CSCMe in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1723
* fix: prevent wallpaper clustering with 16-entry recency buffer by
@zgredex in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1606
* fix: webserver /delete API backward compatibility by @DianaNites in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1475
* fix: relative opds paths and query param with copyparty by @philips in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1535
* fix: use same file name as KOReader for OPDS downloads by @spfenwick
in https://github.com/crosspoint-reader/crosspoint-reader/pull/1286
* fix: pressing space barely moves input cursor (#1729) by @pablohc in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1733
* fix: keyboard feedback #1644 by @pablohc in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1697
* fix: pluralize folder/file counts correctly in file list summary by
@fain182 in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1701
* fix: rendering bug of scrollbar in RoundedRaff theme by @Uri-Tauber in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1814
* fix: two roundedraff bugs by @Uri-Tauber in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1851
* fix: overlap in download font list layout by @pablohc in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1900
* fix: remove duplicate 'Download Fonts' menu entry and improve
navigation by @zgredex in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1893
* fix: Add common ligatures to SD font conversion ranges by @znelson
* fix: capture instantiateVariableFont return value by @jpirnay in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1911
* fix: Roundraff theme home menu offset with no recent books by @znelson
in https://github.com/crosspoint-reader/crosspoint-reader/pull/1845
* fix: Missing navigation button labels in Roundedraff theme by
@Uri-Tauber in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1905
* fix: gracefully resolve fonts missing variants by @Uri-Tauber in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1921
* fix: distribute justifyExtra to non-breaking space tokens by
@prawnwhoyawns in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1783
* fix: remove percent rendering from activities by @mcrosson in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1901
* fix: Restore performance in fontconvert_sdcard.py by @znelson in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1924
* fix: Prepare SD card font caches from txt reader by @znelson in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1973
* fix: make script help paths lightweight by @sabraman in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1937
* fix: Replaced Bookerly with Noto Serif for licensing reasons by
@znelson in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1736
* fix: incorrect y-axis scale factor in jpeg nearest-neighbor downscaler
by @WuTofu in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1807
* fix: display empty lines in txt reader by @Uri-Tauber in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1841
* fix: short-press power action triggered after screenshot combo release
by @pablohc in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1853
* fix: correct Russian auto-turn translations by @a-ignatev in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1566
* fix: Update Ukrainian translations for footnotes (issue 1409) by
@mirus-ua in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1585
* fix: missing swedish translations by @steka in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1667
* fix: Add swedish keyboard translations by @steka in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1726
* fix: swedish translations by @steka in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1762
* fix: swedish translation by @steka in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1829
* fix: swedish translation by @steka in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1888
* fix: Polish translation by @th0m4sek in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1909
* fix: Ukrainian-translation by @KymAndriy in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1946
* fix: Ukrainian translation by @KymAndriy in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1939
* fix: python requirements files by @steka in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1768
* fix: missing requirement by @steka in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1896
* fix: Use LOG_ macros in loc functions by @znelson in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1794

### Internal

* refactor: redesign on-screen keyboard by @pablohc in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1644
* refactor: replace picojpeg with JPEGDEC for cover art conversion by
@jpirnay in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1517
* refactor: Refactor drawArc / fillArc for faster execution by @jpirnay
in https://github.com/crosspoint-reader/crosspoint-reader/pull/1540
* perf: replace i18n pointer tables with offset tables, strip unused
strings by @jpirnay in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1408
* refactor: Store only unique localization strings in offset buffers by
@znelson in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1802
* refactor: Move language setting into JSON settings by @znelson in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1796
* refactor: Use C++20 'requires' in ActivityResult constructor by
@znelson in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1420
* refactor: Use default member initializers for JpegContext and
PngContext by @znelson in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1435
* refactor: logPrintf and predefined log level strings by @CSCMe in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1546
* refactor: RAII scoped open/close for ZipFile by @znelson in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1433
* refactor: Deduplicated BMP header writing in Xtc by @znelson in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1439
* refactor: Added shared XML parser teardown helper by @znelson in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1438
* refactor: Removed redundant FsFile close() calls by @znelson in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1434
* refactor: Deduplicate battery drawing code and fix Lyra charging
indicator by @znelson in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1437
* refactor: Deduplicate Roundraff battery drawing by @znelson in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1847
* refactor: Simplify sort in GfxRenderer::fillPolygon by @znelson in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1817
* refactor: Avoid vector for page turn rates list by @znelson in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1818
* refactor: Use std::size instead of sizeof/sizeof by @znelson in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1819
* refactor: Use fixed-size integers for BookMetadataCache data by
@znelson in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1844
* refactor: Simplify isReaderActivity bookkeeping by @znelson in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1838
* refactor: Simplify XtcReaderActivity with detectPageTurn by @znelson
in https://github.com/crosspoint-reader/crosspoint-reader/pull/1837
* refactor: change ukrainian translation to adaptation and add missing
lines by @KymAndriy in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1828
* chore: drop JPEGDEC patch in favour of upstream fix by @martinbrook in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1465
* chore: clang-format.fix.ps1 script: Add .venv to list of path
exclusions by @jpirnay in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1515
* chore: Updating sleep screen dimensions for X3 by @jensechu in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1688
* chore: Clarify X3 RTC in SCOPE.md by @znelson in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1687
* chore: Improved Italian translations by @znelson in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1685
* chore: change ukrainian translation to adaptation by @KymAndriy in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1684
* chore: Update spanish.yaml by @mvidelatraduc in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1717
* chore: One Italian translation tweak by @znelson in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1718
* chore: git pre-commit hook for format fix by @osteotek in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1730
* chore: Update SDK to fork in CrossPoint org by @znelson in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1836
* chore: Added RAM to firmware_size_history.py script by @znelson in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1830
* chore: Updated docs to reflect DESTRUCTOR_CLOSES_FILE=1 by @znelson in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1878
* feat: cap compressed group size at 64 KB by @jpirnay in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1913
* fix: build-script bug fixes for fontconvert{,_sdcard}.py by @jpirnay
in https://github.com/crosspoint-reader/crosspoint-reader/pull/1910
* feat: include short SHA in CROSSPOINT_VERSION by @osteotek in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1728
* feat: show long branch names by @steka in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1727
* feat: enable pio build cache by @Uri-Tauber in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1769
* style: put page name first in browser titles by @fain182 in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1703
* style: unify page headers across web UI by @fain182 in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1702
* style: move file type badges into Type column by @fain182 in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1793
* style: align action buttons vertically with page title by @fain182 in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1795
* docs: Update README with firmware flashing instructions by @ryneches
in https://github.com/crosspoint-reader/crosspoint-reader/pull/1654
* docs: fix typos by @kianmeng in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1705
* docs: update README.md to reflect the current state of crosspoint by
@Uri-Tauber in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1812
* docs: Add documentation for USB-locked Xteink devices by
@itsthisjustin in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1990
* docs: expand first use of OPDS acronym and provide a wikipedia link by
@sizezero in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1824
* docs: fix KOReader sync guide link by @sabraman in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1930
* docs: fix hyphenation updater script name by @sabraman in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1931
* fix: sd font download urls in docs by @mcrosson in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1945
* fix: sd font folder paths in documentation by @mcrosson in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1944
* chore: Add verbose mode to build-sd-fonts.py by @znelson in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1923

## New Contributors
* @a-ignatev made their first contribution in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1566
* @CSCMe made their first contribution in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1546
* @thehijacker made their first contribution in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1551
* @Telemaniaka made their first contribution in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1597
* @Mraulio made their first contribution in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1625
* @rxmmah made their first contribution in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1462
* @bdeshi made their first contribution in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1626
* @DianaNites made their first contribution in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1475
* @ryneches made their first contribution in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1654
* @zgredex made their first contribution in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1411
* @jonvex made their first contribution in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1635
* @KymAndriy made their first contribution in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1684
* @jensechu made their first contribution in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1688
* @kianmeng made their first contribution in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1705
* @philips made their first contribution in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1535
* @fain182 made their first contribution in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1701
* @mvidelatraduc made their first contribution in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1717
* @bunsoootchi made their first contribution in
https://github.com/crosspoint-reader/crosspoint-reader/pull/918
* @rhoopr made their first contribution in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1694
* @spfenwick made their first contribution in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1286
* @trilwu made their first contribution in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1381
* @jonstieglitz made their first contribution in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1589
* @uxjulia made their first contribution in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1816
* @mchuck made their first contribution in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1069
* @sizezero made their first contribution in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1824
* @leecming82 made their first contribution in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1849
* @jeremydk made their first contribution in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1780
* @WuTofu made their first contribution in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1803
* @wylanswets made their first contribution in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1860
* @sabraman made their first contribution in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1930
* @prawnwhoyawns made their first contribution in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1783
* @mcrosson made their first contribution as co-author on SD card font
support

**Full Changelog**:
https://github.com/crosspoint-reader/crosspoint-reader/compare/1.2.0...release/1.3.0

---------

Co-authored-by: Justin Mitchell <justin@jmitch.com>
Co-authored-by: Chun Ming Lee <95391408+leecming82@users.noreply.github.com>
Co-authored-by: Uri Tauber <uritaube@gmail.com>
2026-05-15 15:51:43 -05:00
Uri TauberandZach Nelson 0fbe6ff469 fix: update README.md to reflect the current state of crosspoint (#1812)
## Summary

As noted in
[#1680](https://github.com/crosspoint-reader/crosspoint-reader/discussions/1680#discussioncomment-16661106),
the README hasn't been updated in a while and has fallen behind the
actual firmware. This PR brings it up to date.

Beyond the feature list, I added a section acknowledging community forks
worth knowing about. I also took some deliberate editorial choices
around how CrossPoint is framed — I think it has the potential to be
more than just "an alternative Xteink firmware", and the wording
reflects that.

A note on process: I wrote the bulk of the text myself, but used AI
tools to scan the codebase and catch features I might have missed, and
to clean up my English (I'm fluent but not a native speaker). If any
line reads as unnatural or AI-sounding, please flag it — I'd rather fix
it than leave it.

---

One thing outside the scope of this PR: I think the cover photo could
use a refresh, ideally replaced with a small gallery showing different
CrossPoint screens. If you have a professional camera and an Xteink
device and want to help with that, let me know.

---------

Co-authored-by: Zach Nelson <zach@zdnelson.com>
2026-05-15 15:38:22 -05:00
Justin Mitchell 43b20bd8be 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.
2026-05-15 10:44:47 -05:00
Zach NelsonandJustin Mitchell b186529120 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>
2026-05-15 09:50:41 -05:00
IjonFryderykandClaude Opus 4.6 5fa5a71ba2 feat: Add Polish hyphenation support (#1590)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-05-14 16:32:59 +03:00
Zach Nelson aa43bd3569 chore: Removed unused icon header files (#1975)
## Summary

Deleted two unused header files containing binary icon data.

---

### 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-14 07:34:35 -05:00
Zach Nelson 8377ac9e31 refactor: Added utils for non-throwing memory allocation and scoped cleanup (#1832)
## Summary

Pared down version of #1418.

Following up on b5df6cb2b5. Added
lib/Memory/Memory.h with:
- `makeUniqueNoThrow<T>` a `nothrow` wrapper for `std::make_unique` that
return `nullptr` on OOM instead of calling `abort()` (the behavior of
bare `new` with `-fno-exceptions`)
- `ScopedCleanup` a helper to call a cleanup lambda on scope exit.

These utilities help to write code that handles OOM scenarios
gracefully, and consistently cleans up resources on scope exit.

JpegToBmpConverter.cpp has been converted to use these utilities. Other
files can be converted later.

This will simplify some of the SD card font resource management in
#1327.

---

### 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-13 21:19:47 -05:00
Fabio Barbonanddrbourbon 71518b3336 fix: KOSync authentication with Calibre-Web-Automated (#1951)
## Summary

* **What is the goal of this PR?** Fix regression of
Calibre-Web-Automated authentication
* **What changes are included?** Trivial fix using ESP HTTP Client
primitives

## Additional Context

Calibre-Web-Automated KOSync stopped working after refactoring HTTP
client to use `ESP HTTP Client`, resulting in a _Server error (try again
later)_ error.

---

### AI Usage

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

---------

Co-authored-by: drbourbon <fabio@MacBook-Air-di-Fabio.local>
2026-05-13 08:32:18 -05:00
Zach Nelson 30b14f2ecf refactor: Removed SdCardFontGlobals.h (#1962)
## Summary

First of several changes to decouple and clean up SD card fonts
integration. This change eliminates SdCardFontGlobals.h:
- Simply declare the `extern SdCardFontSystem sdFontSystem` in
SdCardFontSystem.h.
- `ActivityManager::goToReader` should not care about loading SD card
fonts. Instead do the same work in `ReaderActivity::onEnter`, after the
previous activity has exited and after ReaderActivity has validated the
file path.

---

### 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-13 08:26:56 -05:00
Zach Nelson 6f7f4c592a refactor: Eliminated relative path includes (#1961)
## Summary

Relative includes can hide inappropriate dependency relationships. In
this case, I found that lib/KOReaderSync/KOReaderCredentialStore.cpp was
dependent on src/JsonSettingsIO.h -- a lib -> app dependency, the
opposite direction dependencies should flow in this project.

This change replaces all relative includes with root-relative includes,
and corrects the KOReaderCredentialStore dependency by moving its JSON
settings serialization local to the KOReaderSync library.

---

### 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-13 08:26:30 -05:00
zgredex bc57e5d64b fix: jump page on hold in font family and language selection (#1925)
## Summary
- Holding the navigation button in **Settings → Reader → Font family**
now advances the selection by a full visible page instead of one item at
a time.
- Same fix applied to **Settings → Language**, which had the same
one-item-only behavior.
- Mirrors the pattern already used in font download (ceb3fed) and
chapter selection screens.

## Test plan
- [ ] Settings → Reader → Font family: tap moves by one item; hold jumps
a page (wraps at ends)
- [ ] Settings → Language: tap moves by one item; hold jumps a page
(wraps at ends)
2026-05-12 17:37:56 -04:00
Chun Ming LeeandUri Tauber db3bb850b2 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>
2026-05-12 22:11:37 +03:00
Uri Tauber 30a209de8b fix: Characters from unsupported characterset are overlapping (#1958)
## Summary

* **What is the goal of this PR?** fix #1956.

## Additional Context



[NixOS-and-Flakes-Book_ch3_p7_4pct_130044.bmp](https://github.com/user-attachments/files/27648392/NixOS-and-Flakes-Book_ch3_p7_4pct_130044.bmp)

---

### 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-12 14:00:17 -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
th0m4sek aba393f900 fix: Polish translation (#1909)
## Summary

* **What is the goal of this PR?** (e.g., Implements the new feature for
file uploading.)
* **What changes are included?**

## 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? _**< YES | PARTIALLY | NO
>**_
2026-05-10 11:02:09 -05:00
jpirnay 45441e0789 feat: closest-pt size selection instead of ordinal slot (#1912)
Add SdCardFontFamilyInfo::pickClosestSize(targetPtSize) and have the
manager and SdCardFontSystem drive size selection from a target point
size derived from the user's font-size enum (SMALL=12, MEDIUM=14,
LARGE=16, EXTRA_LARGE=18) rather than indexing the family's sorted size
list by enum ordinal.

The ordinal-slot approach mis-selects whenever a family doesn't ship the
canonical {12,14,16,18} set: a family with only [10,14,18] would map
SMALL/MEDIUM/LARGE/EXTRA_LARGE to 10/14/18/18 — fine for SMALL but
arbitrary for the rest. Closest-pt always picks the on-disk file nearest
to the user-intended point size, with a deterministic smaller-pt
tie-break.

No change for canonical-sized families.

## Summary

* **What is the goal of this PR?** (e.g., Implements the new feature for
file uploading.)
* **What changes are included?**

## 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? _**< YES | PARTIALLY | NO
>**_
2026-05-10 11:01:24 -05:00
Julia cff54d776c feat: increase default weight of Bitter font for improved rendering (#1922)
## Summary

**What is the goal of this PR?**
* Provide a better default weight of Bitter font for improved rendering

**What changes are included?**
* Changes the default weight for Bitter font that is defined in the
custom font catalog to medium (500) from regular (400) for improved
rendering on e-ink, especially when font anti-aliasing is turned on

## Additional Context

* Bitter font at regular weight can become washed out when AA is on due
to some of the thinner stems and posts of the font. Bumping the weight
up to medium makes it sturdier and holds up better when AA is turned 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-10 10:56:30 -05:00
jpirnay 5917c561b3 fix: build-script bug fixes for fontconvert{,_sdcard}.py (#1910)
Fontconvert_sdcard.py:

* Move face.set_char_size() before any load_glyph() call. The validation
loop runs load_glyph() with FT_LOAD_RENDER, which renders at the
*active* size — calling it before set_char_size() wastes work at the
default size and triggers Invalid_Size_Handle on some fonts.

* Walk bitmap.buffer using bitmap.pitch with negative-pitch handling.
The previous linear iteration assumed pitch == width and a top-down
layout, which silently corrupts output for any padded or bottom-up
bitmap FreeType returns.

* Fix operator-precedence bug in 2-bit tail-padding: px << (4 - … % 4) *
2 evaluated as (px << (4 - … % 4)) * 2 due to << binding tighter than *.
Add the missing outer parens.

* Drop SMP codepoints (> U+FFFF) from kern and ligature tables before
packing — the binary format uses uint16/uint32 codepoint fields and
would raise struct.error otherwise.

* Skip GPOS kern subtables that aren't lookup type 2 (PairPos).
_extract_pairpos_subtable assumes Type-2 layout; cursive attachment and
other types reachable through the kern feature crash inside it.

Fontconvert.py:

* Force UTF-8 stdout so `python fontconvert.py … > foo.h` on Windows
doesn't emit UTF-16 / replacement characters in the generated header.

## Summary

* **What is the goal of this PR?** (e.g., Implements the new feature for
file uploading.)
* **What changes are included?**

## 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? _**< YES | PARTIALLY | NO
>**_
2026-05-10 10:54:04 -05:00
jpirnay 5d5533b3d2 fix: capture instantiateVariableFont return value (#1911)
instantiateVariableFont() returns a *new* TTFont with the requested axes
pinned; the existing call discards that return and proceeds to
font.save() the original (still-variable) font. The "static instance"
written to disk is therefore identical to the source variable font and
the requested axis values are silently ignored.

For sd-fonts.yaml entries that use the `variable: {wght: 400}` form
(e.g. Bitter, Lora) this means every weight ends up rasterised from the
same default-weight glyphs — bold and regular look the same.

Capture the return value, and pass two diagnostically useful kwargs
while we're touching the call:

* updateFontNames=True — rewrite the name table so the saved TTF reports
its actual weight/style instead of retaining the source variable-font
names.
* optimize=False        — skip the gvar interpolation optimisation;
                          fully pinning every axis drops gvar anyway,
                          so the work would be wasted.

The atomic-write scaffolding around the call is left untouched.

## Summary

* **What is the goal of this PR?** (e.g., Implements the new feature for
file uploading.)
* **What changes are included?**

## 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? _**< YES | PARTIALLY | NO
>**_
2026-05-10 10:07:08 -05:00
jpirnay c5d2dc2e00 feat: cap compressed group size at 64 KB (#1913)
Add GROUP_MAX_UNCOMPRESSED_BYTES=65536 on top of the existing script-
based grouping. When adding the next glyph would push a group past the
cap, the group is closed and a new one started with the same script ID.

Without the cap, the size of a script group is bounded only by however
many glyphs the font supplies in that block. Dense scripts (CJK,
Vietnamese precomposed, user-supplied fonts with large Unicode blocks)
can produce a single group whose uncompressed size exceeds what fits in
the embedded decompressor's transient malloc on the ESP32-C3, which
manifests as a runtime allocation failure instead of a build error.

The 64 KB ceiling is large enough to hold any single built-in script
group with headroom and small enough to be a comfortable transient
allocation on-device.

The check uses byte-aligned size (4-pixel-aligned row stride × height),
which is what the decompressor actually consumes — not the packed
on-disk length. A defensive guard rejects any single glyph whose own
size exceeds the cap with a clear error pointing at the offending
codepoint.

## Summary

* **What is the goal of this PR?** (e.g., Implements the new feature for
file uploading.)
* **What changes are included?**

## 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? _**< YES | PARTIALLY | NO
>**_
2026-05-10 10:04:59 -05:00
Uri Tauber 91de6ac278 fix: two roundedraff bugs (#1851) 2026-05-10 07:35:16 +03:00
Stefan Blixten Karlsson c7ad14eb36 fix: swedish translation (#1888)
## Summary

* Add missing swedish translations

---

### 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-09 22:00:45 -05:00
Stefan Blixten Karlsson d3e0aeb62b feat: allow unnamed intervals (#1903)
## Summary

* Previous `fontconvert_sdcard.py` requires that the intervals are named
(i.e. listed in `INTERVAL_PRESETS`, ex. "latin1"), this change will also
allow them to be unnamed (ex. "(0x2100-0x214F)") for a more flexible
usage.


---

### 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-09 21:52:13 -05:00
WuTofu 3efc863038 feat: verify CRC32 checksum for font files (#1904)
## Summary

* **What is the goal of this PR?**  
Add end-to-end integrity verification for downloaded font files by
including CRC32 checksums in the font manifest and validating downloaded
`.cpfont` files on device.

* **What changes are included?**  
- `generate-font-manifest.py`: compute and include `crc32` for each
`.cpfont` asset in the generated `fonts.json` manifest.
- `FontDownloadActivity.h`: extend manifest file metadata with `crc32`
and declare checksum helper.
- `FontDownloadActivity.cpp`: parse `crc32` from manifest, compute CRC32
of downloaded files using `esp_rom_crc32_le`.

---

### 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-09 21:46:59 -05:00
pablohc bf30964982 fix: overlap in download font list layout (#1900)
## Summary

Fix font download list layout overlap in Classic and RoundedRaff themes.
The font description was shown as a right-aligned value, causing text to
overlap with the font name on narrow screens.

### Changes
- Move font description from `rowValue` to `rowSubtitle` (second line)
- Show only status ("Installed"/"Update Available") as `rowValue`
- Fix Classic and RoundedRaff themes `rowValue` truncation (was fixed
60px, now dynamic)

## Screenshots

### Classic
| RC 1.3.0 | #1900 |
|-|-|
|<img width="480" height="800" alt="image"
src="https://github.com/user-attachments/assets/534434d6-d7a6-4f73-9d80-8fe5d060907c"
/>|<img width="480" height="800" alt="image"
src="https://github.com/user-attachments/assets/e616c4b7-43b9-49a8-8aa6-ab19f29865f1"
/>|

### Lyra
| RC 1.3.0 | #1900 |
|-|-|
|<img width="480" height="800" alt="image"
src="https://github.com/user-attachments/assets/5b42bad4-2860-4863-a025-5292aa391c1a"
/>|<img width="480" height="800" alt="image"
src="https://github.com/user-attachments/assets/4c27dcbb-8e8e-4ef0-8771-f92975210f11"
/>|

### RoundedRaff
| RC 1.3.0 | #1900 |
|-|-|
|<img width="480" height="800" alt="image"
src="https://github.com/user-attachments/assets/f0510c6f-7712-4cec-8fc8-4386951b1795"
/>|<img width="480" height="800" alt="image"
src="https://github.com/user-attachments/assets/017596b8-fe95-4541-884b-5de034c24193"
/>|

---

### 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-09 22:11:13 +02:00
zgredex ceb3fed392 fix: remove duplicate 'Download Fonts' menu entry and improve navigation (#1893)
## Summary
Removes duplicate "Download Fonts" menu entry and adds complete
navigation support to the font download activity.

## Problem
"Download Fonts" was appearing in **both** Reader settings and System
settings, creating a confusing duplicate menu entry.

## Changes

### 1. Remove Duplicate Menu Entry
- Removed `STR_DOWNLOAD_FONTS` from `systemSettings` in
`SettingsActivity.cpp`
- "Download Fonts" now only appears in **Reader settings**, positioned
right after the font family setting
- Rationale: Font settings logically belong together in the Reader
category

### 2. Navigation Improvements
- **Single-item navigation**: Replaced manual bounds checking with
`ButtonNavigator::nextIndex()` and `previousIndex()` methods
  - Navigation now wraps from last item to first (and vice versa)
  
- **Page navigation**: Added continuous navigation handlers
  - Long-pressing up/down buttons now navigates by page
  - Uses `UITheme::getNumberOfItemsPerPage()` for consistent behavior

## Files Changed
- `src/activities/settings/SettingsActivity.cpp`: Removed duplicate
entry
- `src/activities/settings/FontDownloadActivity.cpp`: Navigation
improvements

## Testing
- Build:  Compiled successfully
- Device testing: Recommended before merge
2026-05-09 18:51:36 +02:00
Stefan Blixten Karlsson f03d3a8056 fix: missing requirement (#1896)
## Summary

* Fix missing requirement (needed by
lib/EpdFont/scripts/build-sd-fonts.py)
* Add a root requirements.txt to simplify installation

---

### 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-09 11:37:09 -05:00
Wylan SwetsandClaude Sonnet 4.6 bf894fd343 fix: improve KOSync bidirectional position matching accuracy (#1897)
## Summary

**Goal:** Fix bidirectional KOSync position matching between CrossPoint
and KOReader so that syncing in either direction lands on the correct
page with character-level accuracy.

**Changes included:**

**Download — `toCrossPoint` (server XPath → CrossPoint page)**
- **XPath ancestry mode for structured elements**: The previous
`ParagraphStreamer` only tracked `<p>` elements. Replaced with a full
ancestor-walking mode that correctly resolves XPaths pointing into
`<li>`, `<ul>`, and other structured elements. Char offset within the
target element is bounded to the matched element's content only.
- **Slash-in-attribute-value corrupts depth tracking**:
`processByteInTag()` treated every `/` byte as a self-closing tag
marker, including `/` inside quoted attribute values (e.g.
`xmlns="http://..."`, `src="Links/image.jpg"`). This drove `htmlDepth`
to 0 prematurely, causing the ancestry search to exit far short of the
target paragraph. Fixed with `inAttrQuote` tracking.
- **Off-by-one in page formula**: `intra * totalPages` rounds up
incorrectly for last-page positions. Changed to `intra * (totalPages -
1)` to map the `[0, 1]` intra fraction correctly onto the `[0,
totalPages-1]` page range. Example: page 14 of 17 was returned as 15.

**Upload — `toKOReader` (CrossPoint page → server XPath)**
- **Off-by-one in page-to-intra formula**: Symmetric fix — `pageNumber /
totalPages` changed to `pageNumber / (totalPages - 1)`, with the guard
updated from `> 0` to `> 1` to avoid division by zero.
- **`<li>`-based XPath generation**: When the current page starts on a
list item, `findXPathForProgress` now generates `ul[N]/li[M]` XPaths
rather than falling back to the preceding `<p>`. Requires the new
`listItemIndex` field in `PageLutEntry` (section cache version bumped to
23).
- **Text-node precision with correct `text()[N].M` format**: KOReader
expects `text()[N].M` where `N` is the 1-based index of the specific
text node within the element. The previous attempt generated `text().M`
(no brackets), which caused KOReader to jump to the front of the book.
Implements a per-element text-node index stack in
`XPathProgressResolver` — parallel to the existing element path stack —
that correctly tracks text node indices relative to each element. Empty
text nodes from bare anchor elements (`<a id="anchor"/>`) are
intentionally skipped, matching KOReader's own text node counting
behavior.

**Reviewer-caught bugs**
- **Double `onCloseTag()` on malformed `</br/>`**: Both the `tagIsClose`
path and the self-closing `/` check were firing, double-decrementing
`htmlDepth`. Fixed with a `!tagIsClose` guard.
- **Dangling pointer in `LOG_DBG`**:
`std::to_string(*nextParagraphPage).c_str()` passed a pointer to a
temporary destroyed before the variadic call. Fixed with `snprintf` into
a stack `char[8]` buffer.

## Additional Context

- Section cache version bumped from 22 → 23 due to the new
`listItemIndex` field in `PageLutEntry`. Users upgrading will see a
one-time re-render of all cached sections on first load — no data loss.
- The `textNodeIndexStack` in `XPathProgressResolver` is a
`std::vector<int>` that mirrors the existing `path` and `parentStates`
stacks — same depth, same lifetime. No additional heap pressure beyond
what was already present.
- All fixes verified on device with *Gentle and Lowly* by Dane C.
Ortlund (spine 21, 17 pages). Download syncs land on the correct page;
upload syncs land at the correct paragraph with character-level offset.

## Test plan

- [ ] Download: sync from KOReader → CrossPoint lands on correct page
for `text()[N].M` XPaths
- [ ] Download: ancestry correctly resolves `<li>` positions inbound
from KOReader
- [ ] Upload: sync from CrossPoint → KOReader lands within one page for
mid-paragraph positions
- [ ] Upload: sync from CrossPoint → KOReader correctly targets `<li>`
elements when page starts on a list item
- [ ] Upload: `text()[N].M` format XPaths do not cause KOReader to jump
to front of book
- [ ] Section cache version 23: delete `.crosspoint/` and verify clean
re-parse with no crashes

---

### AI Usage

Did you use AI tools to help write this code? **YES** — developed with
Claude Code (Anthropic).

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-09 12:22:24 -04:00
Zach Nelson 628794f8b8 fix: Add common ligatures to SD font conversion ranges 2026-05-09 11:09:58 -05:00
Wylan SwetsandClaude Sonnet 4.6 e64155ed63 fix: free Epub RAM and simplify KOSync navigation via ActivityManager (#1860)
## Summary

* **What is the goal of this PR?** Fix KOSync failing with "Network
error" on large/complex EPUBs, and simplify the sync navigation flow by
removing the callback/result pattern.

* **What changes are included?**

This PR combines the approaches from #1855 and #1760 into a single,
cleaner solution:

  **Memory fix (from #1855):**
- `EpubReaderActivity` pre-computes the local KOReader position and
chapter name, then explicitly releases `epub` and `section` before
launching `KOReaderSyncActivity`. This frees ~65KB measured on device,
giving the TLS handshake sufficient heap. The root cause was
`MBEDTLS_ERR_X509_ALLOC_FAILED` (-0x2880) when a 3-cert chain consumed
~48KB during the handshake with only ~50KB available.
- `KOReaderSyncActivity` no longer receives a `shared_ptr<Epub>` at
construction — it lazy-loads the Epub after TLS only if remote progress
is found (`ensureEpubLoaded()`).
- Added `MIN_HEAP_FOR_TLS = 55000` guard in `KOReaderSyncClient` —
returns `LOW_MEMORY` early if aggregate free heap is too low before
attempting a TLS connection.

  **Navigation simplification (from #1760):**
- Replaced `startActivityForResult` + callback with
`activityManager.replaceActivity` / `activityManager.goToReader`.
Progress is saved to `progress.bin` before the epub is released
(cancel/upload paths) and in `saveProgressAndReturn` (apply remote
path). The reader re-launches from the saved position naturally via
`goToReader`, eliminating the need to reload the epub in a callback.
- Extracted `ReaderUtils::saveProgress()` as a shared helper used by
both `EpubReaderActivity` and `KOReaderSyncActivity`.
- Added `STR_SAVE_PROGRESS_FAILED` to all 22 language files for the case
where writing the synced position to SD fails.

  **Orientation fix (found during device testing):**
- `EpubReaderActivity::onExit()` resets the renderer to portrait before
destruction. With `replaceActivity` the reader is fully torn down before
KOSync starts, so KOSync was always rendering in portrait even when
reading in landscape. Fixed by calling `ReaderUtils::applyOrientation`
in `KOReaderSyncActivity::onEnter()`.

## Additional Context

Heap measurements on device (large EPUB with complex CSS):

| Metric | Before | After |
|---|---|---|
| Heap before Epub release | 88,156 bytes | — |
| Heap after Epub release | — | 153,892 bytes (+65,736) |
| Heap at TLS handshake | ~50,000 bytes (fails) | ~116,384 bytes
(passes) |
| Min-free-ever during sync session | 2,600 bytes | 33,052 bytes |
| TLS result | `MBEDTLS_ERR_X509_ALLOC_FAILED` | HTTP 200 |

Tested on device: sync from inside a large EPUB in both portrait and
landscape, cancel, apply remote progress, upload local progress.

---

### AI Usage

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

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-08 22:01:31 -05:00
7993b2bb97 feat: add SD card font support with on-device download and web management
Add a complete SD card font subsystem that enables users to install and
use custom fonts beyond the three built-in families. This combines the
back-end firmware support (#1327) with the font configuration, build
pipeline, CI distribution, and user-facing management UI (#1392).

Core font system:
- Custom .cpfont binary format (v4) with multi-style support (regular,
  bold, italic, bold-italic) packed into a single file per size
- On-demand glyph loading from SD card with two-pass prewarm rendering
  to bulk-read glyphs per page, achieving near-flash performance for
  Latin text (~697ms vs ~681ms) and viable CJK rendering (~32% slower)
- Persistent advance cache for layout measurement without SD I/O
- Overflow ring buffer for glyph cache misses during rendering
- Memory-conscious design: only advance tables kept in RAM; glyph
  bitmaps, kern tables, and ligatures loaded on demand from SD

Font management:
- On-device WiFi download from GitHub Releases with manifest-based
  discovery, install/update detection, and progress UI
- Web interface font upload, listing, and deletion via /fonts page
- Manual SD card copy to /fonts/ or /.fonts/ directories
- Font selection integrated into Settings > Reader > Font Family

Build pipeline:
- Declarative YAML config (sd-fonts.yaml) as single source of truth
  for the 17-family font library (serif, sans, mono, accessibility)
- Python converter (fontconvert_sdcard.py) for TTF/OTF to .cpfont with
  FreeType rasterization, class-based kerning, and ligature extraction
- Parallel build orchestrator with variable font instance extraction
- CI workflow publishing versioned + stable releases to a dedicated
  crosspoint-fonts repository with auto-incrementing revision tags
- Centralized version constants (cpfont_version.py) shared across
  build tooling and CI, with firmware headers as manual sync points

Additional fixes:
- CJK characters no longer get hyphens inserted at line breaks
- Advance table eliminates 30+ second stalls during CJK section
  indexing for paragraphs with >512 unique codepoints

Closes #930

Co-authored-by: Zach Nelson <zach@zdnelson.com>
Co-authored-by: Justin <itsthisjustin@users.noreply.github.com>
Co-authored-by: jpirnay <jens@pirnay.com>
Co-authored-by: mcrosson <kemonine@kemonine.info>
2026-05-08 21:50:06 -05:00
Chun Ming LeeandZach Nelson 29fd29f537 feat: Status bar for XTC files (#1849)
## Summary
Add ability to show a status bar for 1-bit XTC files  (closes #1848) 

Overlays a status bar (similar style to that for epubs) over the image.
Defaults to hidden, users can set to show and either top or bottom of
the screen within "Customize Status Bar" reader settings.

I've leaned toward a simple overlay approach rather than trying anything
clever e.g. resizing the original image or allowing for shifting the
image around. I think it's more straight forward for the user to create
a buffer zone when generating the XTC files.

For 2-bit image files, it'd require more work to handle the multiple
render passes so I'm holding off on that.

Sample from a Japanese text XTC
<img width="400" height="600" alt="image"
src="https://github.com/user-attachments/assets/03ac67cf-acec-4b49-969f-73e3656760ce"
/>

### 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 | PARTIALLY | NO
>**_
YES - Codex

---------

Co-authored-by: Zach Nelson <zach@zdnelson.com>
2026-05-08 14:34:42 -05:00
pablohc b463966045 Revert "feat(sleep-screen): add sleep screen orientation setting" (#1877)
Reverts crosspoint-reader/crosspoint-reader#1748

The images in landscape mode are borked.

Cover:
<img width="696" height="1004" alt="image"
src="https://github.com/user-attachments/assets/354950f3-d3db-4c9d-a771-60f0868666d2"
/>

Sleep Screen:
<img width="688" height="1044" alt="image"
src="https://github.com/user-attachments/assets/3d675b55-93c1-4b5f-9e98-10e18e54c53e"
/>
2026-05-08 12:31:02 -05:00
Zach Nelson a48ad3cd61 chore: Updated docs to reflect DESTRUCTOR_CLOSES_FILE=1 (#1878)
## Summary

Follow-up to 23aad213 which removed redundant FsFile close() calls from
the codebase. This updates CLAUDE.md so AI agents and contributors stop
reintroducing them. Adds the build flag to the Critical Build Flags
section with a clear "do not add file.close() on local FsFile variables"
rule, enumerates the three cases where explicit close is still required
(close before Storage.remove, close before reopening the same variable,
and member variables that outlive function scope), and updates the
HalStorage example, RAII guidance, and Activity Lifecycle sections to be
consistent with the new policy.

---

### 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-08 12:29:21 -05:00
xiro-codesandTravis Davis e3fb3bba37 feat(sleep-screen): add sleep screen orientation setting (#1748)
Co-authored-by: Travis Davis <me@tdavis.dev>
2026-05-08 11:41:06 +03:00
Arthur TazhitdinovandCopilot e8d7153d7f feat: edit wifi networks in webui (#1743)
## Summary

**What is the goal of this PR?**
Add Wi-Fi network management to the Web UI settings page, similar to
existing OPDS server management, so users can view, add, edit, and
delete saved Wi-Fi credentials from the browser. Closes
https://github.com/crosspoint-reader/crosspoint-reader/issues/1544 and
https://github.com/crosspoint-reader/crosspoint-reader/discussions/607

**What changes are included?**

- Added Wi-Fi API endpoints:
- GET /api/wifi Listing saved networks (without exposing plaintext
passwords)
  - POST /api/wifi Creating/updating networks
  - POST /api/wifi/delete Deleting networks by index

- Added a new Wi-Fi Networks management section web UI settings page

<img width="921" height="712" alt="image"
src="https://github.com/user-attachments/assets/42c20a63-f335-4389-8246-d38065d6b65a"
/>


---

### 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 >**_

---------

Co-authored-by: Copilot <copilot@github.com>
2026-05-07 21:29:48 -05:00
WuTofu 1e2f6e2d67 feat: enhance long press action to delete both files and directories (#1803)
## Summary

* **What is the goal of this PR?** 
Enhance file browser long-press behavior so the delete action now works
for both files and directories.

* **What changes are included?**
Updated `FileBrowserActivity.cpp` to support for directory deletion on
long-press

---

### 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-07 17:32:30 -05:00
Zach Nelson e841c84194 refactor: Deduplicate Roundraff battery drawing (#1847)
## Summary

Deduplicated battery drawing code for Roundraff theme with other themes.

Now `BaseTheme` provides non-virtual `drawBatteryLeft` and
`drawBatteryRight` methods, which use a shared static
`drawBatteryOutline` implementation, and defer to a virtual
`fillBatteryIcon` implementation. In Classic and Roundraff
`fillBatteryIcon` uses a solid fill, while Lyra and Lyra Extended use a
segmented fill.

The charging indicator inside the battery was missing for Roundraff, but
is now consistent with other themes because of the shared drawing
implementation.

---

### 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-07 16:57:33 -05:00
Zach Nelson 83f0cee565 fix: Roundraff theme home menu offset with no recent books (#1845)
## Summary

With the Roundraff theme selected and no recent books, the home screen
menu shows a "Continue Reading" option and menu handling is offset by
one ("Continue Reading" actually does "Browse Files", "File Transfer"
actually does "Settings", etc.) Simple fix here is to omit the "Continue
Reading" menu item when there are no recent books.

---

### 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-07 08:17:51 -05:00
pablohc c15b5b9bc5 fix: short-press power action triggered after screenshot combo release (#1853)
When Power+Down is released with a slight stagger (Down before Power),
the Power release event leaked through to the activity loop, triggering
short-press Power actions like clip selection. Add a
screenshotComboActive flag that suppresses all input processing until
Power is fully released after a screenshot combo.

## Summary

* **What is the goal of this PR?** (e.g., Implements the new feature for
file uploading.)
* **What changes are included?**

## 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? _**YES**_
2026-05-07 08:10:17 -04:00
Uri Tauber dadce519c4 fix: display empty lines in txt reader (#1841) 2026-05-07 09:07:57 +03:00
Zach Nelson 11bc36ebb5 refactor: Use fixed-size integers for BookMetadataCache data (#1844) 2026-05-07 09:03:51 +03:00
Zach Nelson 395e68ea2b refactor: Simplify isReaderActivity bookkeeping (#1838)
## Summary

Before, any sub-activity of a reader activity needed to override
`isReaderActivity` to maintain correct bookkeeping through
`ActivityManager::isReaderActivity`. We could easily miss this in any
new sub-activities. Instead, simplify so each reader activity correctly
reports and then `ActivityManager` checks for any reader activity in its
stack.

---

### 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-06 09:57:22 -05:00
Zach Nelson cfe3a948a0 refactor: Simplify XtcReaderActivity with detectPageTurn (#1837)
## Summary

Simplify duplicated code in `XtcReaderActivity` to use
`ReaderUtils::detectPageTurn`. This implementation is now shared with
`EpubReaderActivity` and `TxtReaderActivity`. Also deduplicated the
chapter skip time constant.

---

### 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-06 09:57:06 -05:00
Zach Nelson 4ea938b709 chore: Update SDK to fork in CrossPoint org (#1836)
## Summary

Update the Open X4 SDK to point to the CrossPoint organization fork.
This allows us to take SDK changes without being blocked on the upstream
repository.

---

### 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-06 09:56:35 -05:00
WuTofu 939014d996 fix: incorrect y-axis scale factor in jpeg nearest-neighbor downscaler (#1807)
## Summary

* **What is the goal of this PR?**
Fixes a bug in the jpeg nearest-neighbor downscaler where source image
rows containing visible content may be cropped.

* **What changes are included?**
Split the single `fineScaleFP`/`invScaleFP` scale pair in
`JpegToFramebufferConverter.cpp` into separate X
(`fineScaleFPX`/`invScaleFPX`) and Y (`fineScaleFPY`/`invScaleFPY`)
pairs

## Additional Context
The one case that I encountered in my epub:
<img width="480" height="37" alt="img_6_0"
src="https://github.com/user-attachments/assets/0de3778c-cf3d-42dc-a1ba-4ba729f6b1c4"
/>

The image generated from pxc cache with the latest commit:
<img width="464" height="36" alt="img_6_0_master pxc"
src="https://github.com/user-attachments/assets/92ba06e5-be15-4db6-840e-882bdddb1baf"
/>

With this fix, the bottom outline of each character is still there:
<img width="464" height="36" alt="img_6_0_fixed pxc"
src="https://github.com/user-attachments/assets/4246ce55-5a20-480f-862f-7988f804c1fb"
/>

---

### 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-06 08:57:37 -05:00
Pietro CampagnanoandClaude Sonnet 4.6 3e7d63dab9 style: align action buttons vertically with page title (#1795)
## Summary

* **What is the goal of this PR?** Fix the vertical misalignment between
the "File Manager" title and the action buttons (Upload / New Folder /
Delete Selected).
* **What changes are included?**
- `h2`: `margin-top: 0` → `margin: 0` — removes the default browser
`margin-bottom` that was inflating the height of the header-left flex
container, pushing the calculated flex center below the visual midpoint
of the title text
- `.page-header-left`: `align-items: baseline` → `align-items: center` —
ensures the title and breadcrumbs are center-aligned with each other and
with the buttons

### Before
<img width="1198" height="560" alt="Screenshot From 2026-04-30 22-29-28"
src="https://github.com/user-attachments/assets/b9a43069-cd93-4028-ab21-75be253ed1f0"
/>

### After
<img width="1198" height="560" alt="Screenshot From 2026-04-30 22-48-49"
src="https://github.com/user-attachments/assets/b9977c65-e479-4490-8ec3-15ae99cfccd7"
/>


## Additional Context

Small CSS-only change, no JavaScript or HTML structure touched.

---

### AI Usage

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

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-05 08:48:31 -05:00
Eliz efa4f71a68 feat: Set sleep cover from BMP viewer (#1104) 2026-05-05 12:23:20 +03:00
Stefan Blixten Karlsson f44722a0f2 fix: swedish translation (#1829) 2026-05-05 12:14:48 +03:00
KymAndriyandKymAndriy 40af42683e refactor: change ukrainian translation to adaptation and add missing lines (#1828)
Co-authored-by: KymAndriy <mail@gmail.com>
2026-05-05 12:13:59 +03:00
Zach Nelson 78625afe76 chore: Added RAM to firmware_size_history.py script (#1830)
## Summary

Added RAM delta output to firmware_size_history.py output, e.g.:

```
 % scripts/firmware_size_history.py --commits adcd796 8e18472
[info] Will restore to 'sd-card-fonts' when finished.
[info] Stashing uncommitted changes...
[info] Building 2 commits...

[1/2] adcd7961c9 feat: self-heal from transient WiFi loss, add dBm indicator during WebServerActivity  (#1780)
  Building (env: default)...
  Flash: 5,751,209, RAM: 97,996 bytes

[2/2] 8e184724d1 Merge branch 'master' into sd-card-fonts
  Building (env: default)...
  Flash: 5,762,395, RAM: 97,988 bytes

[info] Restoring 'sd-card-fonts'...
[info] Restoring stashed changes...

Commit            Flash    Delta          RAM    Delta  Title
──────────  ───────────  ───────  ───────────  ───────  ────────────────────────────────────────
adcd7961c9    5,751,209                97,996           feat: self-heal from transient WiFi loss, add dBm indicator during WebServerActivity  (#1780)
8e184724d1    5,762,395  +11,186       97,988       -8  Merge branch 'master' into sd-card-fonts
```

---

### 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-04 22:11:40 -05:00
Jeremy Klein adcd7961c9 feat: self-heal from transient WiFi loss, add dBm indicator during WebServerActivity (#1780)
## Summary
* **What is the goal of this PR?**

I was seeing hangs during File Transfer. Troubleshooting via serial
showed it had to do with the SHUTTING_DOWN state. Strengthened the WiFi
State Machine a bit and added self-healing. Additionally, I found it
obnoxious to need to keep referring to serial for my wifi strength, so a
dBm meter is added opposing the SSID when on the File Transfer page.

* **What changes are included?**

The dBm meter was at risk of causing rapid screen updates (if, say, we
hovered right around a threshold), so I've implemented a basic
hysteresis around this. RISING/FALLING would be more canonical variable
names, but are reserved in the framework.

## Additional Context

My X4 has a terrible antenna, apparently, and I was running into this
failure condition pretty regularly. Wifi "bars" were chosen as a
relatively pan-cultural glyph rather than relying on localization.

Tested on hardware at -83 dBm under sustained EPUB upload (60 books, ~30
MB). Transient losses up to ~14s now ride through; pre-fix the same
losses required a power-cycle after as little as a 2s blip.

---

### 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? Nope. Commits are hand
written. Claude was used to build a local test harness for validation
only during iteration.
2026-05-04 21:19:00 -05:00
eunchurn 5717374e4b feat(update): SD-card firmware update + X3 bootloader compatibility (#1786) 2026-05-04 19:51:20 -04:00
sizezero b8a6b58b5e docs: expand first use of OPDS acronym and provide a wikipedia link (#1824)
No description needed beyond title.
2026-05-04 15:57:02 -05:00
Zach Nelson 333286acfc refactor: Use std::size instead of sizeof/sizeof (#1819)
## Summary

Simplify code calculating compile-time array sizes with
`sizeof(array)/sizeof(element)` to use `std::size`.

---

### 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-04 12:41:27 -05:00
Dave AllieandZach Nelson e026bcb9dc fix: Track block style stack for nested styles (#1582)
## Summary

* Add new block style stack to track accumulated styles
* When dropping into nested block, apply new styles on last BlockStyle
in the stack and push to the back
  * When leaving block, pop the stack
* Fix issue where the image % width always went off screen width, use
current container width based on accumulated styles
* Bump section cache version to regenerate pages

## Additional Context

Fixes https://github.com/crosspoint-reader/crosspoint-reader/pull/1581
more correctly, and includes fix from
https://github.com/crosspoint-reader/crosspoint-reader/pull/1580

  Consider:
```html
<div class="c1">          <!-- marginLeft: 10px -->
  <div class="c2">        <!-- marginLeft: 20px -->
    <p class="c3">text</p>  <!-- marginLeft: 5px -->
  </div>
  <p class="c4">text2</p>   <!-- marginLeft: 5px -->
</div>
```

| Element | Expected | Before #1581 (leaked) | After #1581 (reset) |
This PR (style stack) |
| --- | --- | --- | --- | --- |
| `c3` | 10+20+5 = 35px | 35px | 25px | 35px |
| `c4` | 10+5 = 15px | 35px (wrong, leaked c2) | 5px (wrong, lost c1) |
15px |

This PR replaces the reset-on-close approach with a proper block style
stack. When a block element opens, its resolved style is pushed onto the
stack. When it closes, the stack pops back to the parent's style. This
correctly accumulates nested margins/padding while preventing style
leakage to siblings.

---

### 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

---------

Co-authored-by: Zach Nelson <zach@zdnelson.com>
2026-05-04 10:34:49 -05:00
Dave AllieandZach Nelson a1007a4660 fix: Track block style stack for nested styles (#1582)
## Summary

* Add new block style stack to track accumulated styles
* When dropping into nested block, apply new styles on last BlockStyle
in the stack and push to the back
  * When leaving block, pop the stack
* Fix issue where the image % width always went off screen width, use
current container width based on accumulated styles
* Bump section cache version to regenerate pages

## Additional Context

Fixes https://github.com/crosspoint-reader/crosspoint-reader/pull/1581
more correctly, and includes fix from
https://github.com/crosspoint-reader/crosspoint-reader/pull/1580

  Consider:
```html
<div class="c1">          <!-- marginLeft: 10px -->
  <div class="c2">        <!-- marginLeft: 20px -->
    <p class="c3">text</p>  <!-- marginLeft: 5px -->
  </div>
  <p class="c4">text2</p>   <!-- marginLeft: 5px -->
</div>
```

| Element | Expected | Before #1581 (leaked) | After #1581 (reset) |
This PR (style stack) |
| --- | --- | --- | --- | --- |
| `c3` | 10+20+5 = 35px | 35px | 25px | 35px |
| `c4` | 10+5 = 15px | 35px (wrong, leaked c2) | 5px (wrong, lost c1) |
15px |

This PR replaces the reset-on-close approach with a proper block style
stack. When a block element opens, its resolved style is pushed onto the
stack. When it closes, the stack pops back to the parent's style. This
correctly accumulates nested margins/padding while preventing style
leakage to siblings.

---

### 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

---------

Co-authored-by: Zach Nelson <zach@zdnelson.com>
2026-05-04 10:34:09 -05:00
Zach Nelson 6c4ae7c41a refactor: Avoid vector for page turn rates list (#1818)
## Summary

Small cleanup to avoid a dynamically allocated static structure for auto
page-turn rate values.

---

### 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-04 08:45:44 -05:00
Zach Nelson a5fac320ab refactor: Simplify sort in GfxRenderer::fillPolygon (#1817)
## Summary

Small simplification to node sorting algorithm in
GfxRenderer::fillPolygon.

---

### 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-04 08:45:15 -05:00
MaciekandЕгор Мартынов 2e2ea6a9e8 feat: Page turn button orientation change (#1069)
Co-authored-by: Егор Мартынов <martynovegorOF@yandex.ru>
2026-05-04 08:56:02 +03:00
Julia b692bad10d fix: support legacy XTC file headers where pageTableOffset=48 (#1816)
## Summary

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

Restore compatibility with older XTC files that use the legacy 48-byte
header layout, so they open correctly instead of failing
header/page-table validation.

**What changes are included?**

- Updates the XTC parser to accept legacy files where pageTableOffset
starts at 0x30 instead of the newer full header size.
- Avoids treating legacy header bytes as a valid chapterOffset, so older
files are not misclassified as having chapters.
- Switches XTC file size and seek operations to 64-bit-safe wrapper
calls in HalStorage, which matches the XTC format’s 64-bit offset fields
and keeps page/chapter lookups from narrowing offsets.
- Keeps the existing bounds checks and improves related logging when XTC
page loads fail.

## Additional Context

- This is a narrow compatibility fix for XTC handling. The functional
change is in the XTC parser; the HalStorage changes are supporting
wrapper methods needed by that parser update.
- Risk should be low outside XTC reading, since the HAL changes only
expose existing underlying file APIs rather than changing storage
behavior.

---

### 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-03 20:48:35 -05:00
Zach Nelson aa7a31b3db fix: Read GH release JSON as stream in OTA updater (#1810)
## Summary

The GitHub recent release API returns lots of data, including the full
release notes. For 1.2.0, that data is 30,530 bytes. The existing
OtaUpdater HTTP handler and single-buffer JSON parsing can't reliably
handle that much data in the constrained ESP32 environment.

This change does a few things:
1. Adds a very simple lib/JsonParser/StreamingJsonParser.cpp with
SAX-style callbacks to read JSON data incrementally.
2. Adds a very simple lib/JsonParser/ReleaseJsonParser.cpp building on
StreamingJsonParser, which specifically parses the GitHub release JSON
for the release version, URL, and size.
3. Updates OtaUpdater.cpp to use an instance of ReleaseJsonParser to
incrementally parse the large response it may receive from GitHub.

Building from this commit while overriding my local version to 1.1.9, I
was able to run the OTA update process ~5 times in a row successfully.

Fixes #1561 (second part, after #1805).

---

### 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-03 20:48:06 -05:00
Uri Tauber 22701ccf18 fix: rendering bug of scrollbar in RoundedRaff theme (#1814)
## Summary

Fix small bug I encounter when using RoundedRaff theme.
Without this the scrollbar positioning is a bit off sometimes, and on
the serial output you get hundreds of lines like:
```
[22:31:00] [ERR] [GFX] !! Outside range (472, 834) -> (834, 7)
[22:31:00] [ERR] [GFX] !! Outside range (473, 834) -> (834, 6)
[22:31:00] [ERR] [GFX] !! Outside range (474, 834) -> (834, 5)
[22:31:00] [ERR] [GFX] !! Outside range (471, 835) -> (835, 8)
[22:31:00] [ERR] [GFX] !! Outside range (472, 835) -> (835, 7)
[22:31:00] [ERR] [GFX] !! Outside range (473, 835) -> (835, 6)
[22:31:00] [ERR] [GFX] !! Outside range (474, 835) -> (835, 5)
[22:31:00] [ERR] [GFX] !! Outside range (471, 836) -> (836, 8)
[22:31:00] [ERR] [GFX] !! Outside range (472, 836) -> (836, 7)
[22:31:00] [ERR] [GFX] !! Outside range (473, 836) -> (836, 6)
[22:31:00] [ERR] [GFX] !! Outside range (474, 836) -> (836, 5)
```
2026-05-03 15:54:02 -05:00
Zach Nelson 64ecfe2ef3 refactor: Store only unique localization strings in offset buffers (#1802)
## Summary

Omit any duplicate strings in non-English localization lookup tables.
For non-English lookup offsets, tag bit 15 to indicate the offset
applies to the English localization table.

Saves 18,766 bytes of flash.

---

### 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-02 16:14:12 -05:00
Justin MitchellandJustin Mitchell ba4a361d64 fix: OTA update on x3 and progress bar on x4 and x3 (#1805)
Co-authored-by: Justin Mitchell <1875695+itsthisjustin@users.noreply.github.com>
2026-05-01 19:15:15 -04:00
Pietro CampagnanoandClaude Sonnet 4.6 15ea7027df style: move file type badges into Type column (#1793)
## Summary

The file type was shown twice in the file browser table: as a bold badge
in the **Name** column and as plain text in the **Type** column.

**Before:** badge in Name column + redundant text in Type column
**After:** badge lives exclusively in the Type column

| | Before | After |
|---|---|---|
| Folder | Name: `📁 folder` **FOLDER** · Type: `Folder` | Name: `📁
folder` · Type: **FOLDER** |
| EPUB | Name: `📗 book.epub` **EPUB** · Type: `EPUB` | Name: `📗
book.epub` · Type: **EPUB** |
| Other | Name: `📄 file.txt` · Type: `TXT` | unchanged |

### Before
<img width="1125" height="571" alt="Screenshot From 2026-04-30 22-07-59"
src="https://github.com/user-attachments/assets/794c66d3-5ad5-4536-a142-708989b9dee8"
/>

### After
<img width="1125" height="571" alt="Screenshot From 2026-04-30 22-08-45"
src="https://github.com/user-attachments/assets/f9fb2898-b165-478a-813b-f2290ad3e42c"
/>


## Changes
- Removed `epub-badge` and `folder-badge` from the Name column
- Moved badges into the Type column, replacing the redundant plain text
- Non-EPUB files are unaffected (extension text remains in Type column)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-01 10:59:28 -05:00
Zach Nelson b25389b43e refactor: Move language setting into JSON settings (#1796)
## Summary

Another follow up to #1408. That PR revised the language.bin settings
file to version 2. After merging, it occurred to me that it would make
more sense to have the language inside the overall settings.json file,
easy for users to see and change directly.

This change adds migration from language.bin to settings.json. It only
migrates from the pre-#1408 v1 language.bin format, because v2 has only
been in for a couple of commits and I don't think we need long-term
maintenance code to handle it.

#1408 had a potential long-term maintenance issue for migration. It
assumed that the enum order of languages never changes, only grows. So,
e.g. "Portuguese (Portugal)" could never be added next to "Portuguese
(Brazil)", only appended. This change includes a hardcoded frozen
ordering of the language indices as they existed at 2f969a9, and
migrates to storing a language code string in the settings.json instead
for future reordering flexibility.

Similar to the settings.bin to settings.json migration, which renames
settings.bin to settings.bin.bak, this change renames language.bin to
language.bin.bak after adding the language setting to settings.json.

---

### 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-01 09:39:28 -05:00
Zach Nelson ae865f6d08 fix: Use LOG_ macros in loc functions (#1794)
## Summary

Quick follow up to #1408. We overlooked the fact that the change wasn't
using the LOG_ macros. Also, removed redundant file.close() calls, as in
23aad21.

---

### 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-04-30 15:47:39 -05:00
jpirnayandZach Nelson d53c8b0e0e perf: replace i18n pointer tables with offset tables, strip unused strings (#1408)
## Summary

* **What is the goal of this PR?** Reduce the flash footprint of the
i18n string data and tooling improvements to `gen_i18n.py`.

* **What changes are included?**

### 1. `lib/I18n/I18n.cpp` — use offset-based lookup

`I18n::get()` previously dereferenced a `const char* const*` pointer
array. It now uses a two-field `LangStrings` struct (a flat char blob +
a `uint16_t` offset table) generated for each language:

```cpp
// before
const char* const* strings = getStringArray(_language);
return strings[index];

// after
const LangStrings lang = getLanguageStrings(_language);
return lang.data + lang.offsets[index];
```

Lookup cost is unchanged — still O(1), one array load and one addition.

### 2. `scripts/gen_i18n.py` — new generated layout

Each language's string data is now emitted as:

- **`STRINGS_XX_DATA[]`** — a single `const char[]` blob of all strings
concatenated with `\0` separators.
- **`OFFSETS_XX[]`** — a `uint16_t` array of one byte-offset per `StrId`
into the blob.

Previously each language had a `const char* const STRINGS_XX[]` pointer
array (4 bytes/entry on ESP32-C3).

#### Flash savings

| Table type | Size per language | 19 languages |
|---|---|---|
| `const char*` pointer array (before) | `339 × 4 = 1,356 B` | **25,764
B** |
| `uint16_t` offset table (after) | `339 × 2 = 678 B` | **12,882 B** |
| **Saved** | | **12,882 B (~12.6 KB)** |

String data size is unchanged — 133,092 B across 19 languages.

**Total: 158,856 B → 145,974 B** (pointer tables → offset tables).

### 3. Build-time stripping of unused strings

`gen_i18n.py` now scans the `src/` and `lib/` trees for `STR_*`
references and, during a PlatformIO build, automatically omits the 52
strings that are defined in YAML but never referenced in code. This
further reduces the compiled output from 339 → 287 string keys per
language.

---

## `gen_i18n.py` CLI reference

```
python gen_i18n.py [translations_dir [output_dir]] [options]
```

| Argument / Flag | Default | Description |
|---|---|---|
| `translations_dir` | `lib/I18n/translations` | Path to the
per-language YAML files |
| `output_dir` | `lib/I18n/` | Where to write the generated `.h` /
`.cpp` files |
| `--src-dirs DIR [DIR …]` | `src lib` | Directories scanned for `STR_*`
usage |
| `--strip-unused` | off | Remove unreferenced `STR_*` keys from
generated output |
| `--verbose` / `-v` | off | Print per-key INFO/WARNING messages and
`Generated:` lines |

The PlatformIO build (SCons `pre:` hook) calls `main(strip_unused=True)`
automatically, so unused strings are always stripped from firmware
builds without any manual flag.

**Default run output** (no flags) shows a per-language summary table:
```
Language            Code  Own  Fallback  Unused  Data (B)
------------------  ----  ---  --------  ------  --------
English             EN    339  0         52        5,266
Belarusian          BE    310  29        52        9,673
…

  Total: 339  |  Used in code: 287  |  Never used: 52
  Flash (now):    133,092 B strings  +  12,882 B offset tables (uint16_t)  =  145,974 B
  Flash (before): 133,092 B strings  +  25,764 B pointer tables (ptr32)    =  158,856 B
  Saved by offset tables: 12,882 B
```

---

### AI Usage

Did you use AI tools to help write this code? **YES** (GitHub Copilot)

---------

Co-authored-by: Zach Nelson <zach@zdnelson.com>
2026-04-30 13:57:19 -05:00
Jon Stieglitz 2f969a93b4 feat: context-aware screenshot filenames with book title (#1589)
## Summary

I love the new screenshot feature but I can tell that soon I'm gonna
have a folder full of screenshots and not know what's what. It would be
great if the folder could self organize.

When a screenshot is taken while reading, the filename would now include
the book title, chapter (EPUB), page number, and progress percentage.

Example: My-Great-Book_ch3_p5_42pct_12345.bmp

* **What is the goal of this PR?** (e.g., Implements the new feature for
file uploading.)
* **What changes are included?**

## Additional Context

* Non-reader screens keep the existing screenshot-<millis>.bmp naming.

---

### 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 >**_

I reviewed all the changes but I don't have any experience with this
project so this is my best guess
2026-04-30 13:56:45 -05:00
14e1ce2d04 fix: use esp_http_client for KOSync to prevent TLS OOM on ESP32-C3 (#1381)
## Summary

* **What is the goal of this PR?** Fix KOReader sync failing on HTTPS
servers due to TLS out-of-memory on ESP32-C3
* **What changes are included?**
- Replace `WiFiClientSecure`/`HTTPClient` with `esp_http_client`
(ESP-IDF native API) for all KOSync HTTP requests
- Use 2KB TLS buffers instead of the default 16KB — KOSync payloads are
tiny JSON (<1KB), so this is more than sufficient
- Use `esp_crt_bundle_attach` for proper TLS certificate verification
(replaces `setInsecure()`)
- Strip trailing slashes from server URL to prevent double-slash in API
paths (e.g. `https://server.com//users/auth`)
  - Add `lastHttpCode` static field for diagnostics
  - Add free heap logging to help debug memory issues

### Problem

The ESP32-C3 has ~46KB free heap after WiFi is initialized.
`WiFiClientSecure` allocates 16KB for TLS RX + 16KB for TLS TX = 32KB
just for the TLS buffers, leaving almost no room for the actual TLS
handshake (which needs additional dynamic allocations). This causes
KOReader sync to:

1. Fail silently with network errors on HTTPS servers (including the
default `sync.koreader.rocks`)
2. Occasionally crash with heap exhaustion

### Solution

`esp_http_client` (the ESP-IDF native HTTP client) allows configuring
`buffer_size` and `buffer_size_tx` independently. Setting both to 2KB
(total 4KB) leaves plenty of heap for the TLS handshake while still
being sufficient for KOSync's small JSON payloads.

This also fixes the `setInsecure()` anti-pattern —
`esp_crt_bundle_attach` provides proper certificate verification using
the ESP-IDF's built-in CA bundle, so credentials and reading history are
no longer sent over unverified TLS connections.

### Files changed

| File | Change |
|------|--------|
| `lib/KOReaderSync/KOReaderSyncClient.cpp` | Replace
WiFiClientSecure/HTTPClient with esp_http_client; add ResponseBuffer,
base64 encoder, createClient helper |
| `lib/KOReaderSync/KOReaderSyncClient.h` | Add `lastHttpCode` static
field |
| `lib/KOReaderSync/KOReaderCredentialStore.cpp` | Strip trailing
slashes from base URL |

## Additional Context

Tested on a CrossPoint X4 (ESP32-C3 with 4MB flash). Before this change,
KOSync auth to `sync.koreader.rocks` (HTTPS) would fail ~80% of the
time. After: works reliably.

The `base64Encode` helper is needed because `esp_http_client` doesn't
have a built-in `setAuthorization()` method like `HTTPClient` does. This
is used for the HTTP Basic Auth header required by Calibre-Web-Automated
KOSync servers.

Fixes #581

---

### 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**_ AI
assisted with the esp_http_client migration pattern and base64 encoder.
The root cause analysis (TLS buffer OOM) and solution design were
manual.

---------

Co-authored-by: trilwu <trilwu@users.noreply.github.com>
Co-authored-by: Justin Mitchell <justin@jmitch.com>
2026-04-30 11:19:22 -05:00
5d2e5596b4 feat: X3 gyroscope-based tilt page turning via QMI8658 IMU (#1636)
Co-authored-by: justinian <juicecutlus@gmail.com>
Co-authored-by: Vincent Politzer <vincent@skipwithjoy.com>
2026-04-29 15:29:33 -04:00
spfenwick bc9651b664 fix: use same file name as KOReader for OPDS downloads (#1286)
This allows KOSync to work for files downloaded from OPDS when set to
use filename matching

## Summary

* **What is the goal of this PR?** Brings the names of files downloaded
via OPDS into line with the KOReader OPDS plugin so the files sync when
KOSync is set to filename matching
* **What changes are included?** Change the filename generated by the
OPDS browser from "\<title\> - \<author\>" to "\<author\> - \<title\>"
so it matches KOReader.

## Additional Context

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

Both KOReader and crosspoint sanitise the filename by removing special
characters. This step seems to already use the same rules.

---

### 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-04-28 19:29:05 -05:00
Rob Hooperandrhoopr 741dd89ac1 fix: cap per-side horizontal CSS inset at 2em (#1694)
## Summary

- **What is the goal of this PR?** Fix chapter-opener text collapsing to
1-2 words per line in EPUBs that apply large em-based horizontal CSS
insets.
- **What changes are included?** Caps `marginLeft`, `marginRight`,
`paddingLeft`, and `paddingRight` at 2em in `BlockStyle::fromCssStyle`
(`lib/Epub/Epub/blocks/BlockStyle.h`). Vertical margins/padding are
unchanged - the bug is horizontal-only.

## Additional Context

**Repro:** *Mother Night* by Kurt Vonnegut, Chapter 21 ("My best
friend..."). The chapter-opening element's embedded CSS sets a large
horizontal inset.

- Settings: Embedded Style = On, Justify alignment (default).
- Before: 1-2 words per line with a visible river between them.
- After: body text fills the usable page width like every other
paragraph.

**Why the clamp lives in `fromCssStyle`:** This is the single point
where CSS lengths resolve to pixels, so clamping here keeps both
`effectiveWidth` call sites in `ChapterHtmlSlimParser` (`:1131-1133`
makePages, `:841-844` long-block split) consistent with the
`leftInset()` xOffset. A width-only clamp at either call site would
leave text pushed right by a large xOffset.

**Test plan:**
- [x] Flashed to Xteink X4. Chapter 21 of *Mother Night* renders
normally with Embedded Style on.
- Users with cached layouts of affected books need to delete
`.crosspoint/epub_<hash>/sections/` (or the whole `.crosspoint/`) on the
SD card to pick up the fix.

**Before / After:**
<p float="left">
<img height="400" alt="IMG_0320"
src="https://github.com/user-attachments/assets/de8815ae-f2f4-4845-be1f-449c5a25d191"
/>
<img height="400" alt="IMG_0324"
src="https://github.com/user-attachments/assets/95f5f4cb-8aa1-418e-b611-18e0c342cbef"
/>
</p>

---

### 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 >**_

I used Claude Code (Opus 4.7) to evaluate the codebase and find the
relevant code related to this rendering. From there, it was human
designed, reviewed and tested.

Co-authored-by: rhoopr <>
2026-04-27 19:37:41 +03:00
Stefan Blixten Karlsson ef98d44ef3 fix: python requirements files (#1768)
## Summary
The Python scripts in the current repo have many requirements that are
not mentioned in any requirements.txt files, so I have therefore added
them to the directories that need them.

Question: Should these changes maybe moved into a "root"
requirements.txt file?

---
### 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-04-27 19:28:53 +03:00
b8a51522ca feat(theme): add roundedraff theme and fix sleep cover crop grid artifacts (#918)
## Summary
- Add new `roundedraff` theme.
- Fix sleep screen artifact where book cover showed grid lines in
`Cover` mode with `Crop`.

## Additional Context
## Key changes
- Added `src/components/themes/roundedraff/` theme implementation.
- Updated sleep cover rendering logic in:
  - `src/activities/boot_sleep/SleepActivity.cpp`
  - `src/activities/boot_sleep/SleepActivity.h`
- Improved cover generation/regeneration paths in:
  - `lib/Epub/Epub.cpp`
  - `lib/Epub/Epub.h`
  - `lib/Txt/Txt.cpp`
  - `lib/Txt/Txt.h`

## Verification
- Sleep screen set to Book cover
- Sleep screen cover mode set to Crop
- Confirmed grid artifacts no longer appear on cover sleep screen.

### 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 >**_

---------

Co-authored-by: CaptainFrito <yzq6x5zypy@privaterelay.appleid.com>
Co-authored-by: Zach Nelson <zach@zdnelson.com>
Co-authored-by: Uri Tauber <142022451+Uri-Tauber@users.noreply.github.com>
2026-04-27 19:01:22 +03:00
Uri Tauber 33386953d9 feat: enable pio build cache (#1769)
## Summary

* **What is the goal of this PR?** 
Enables the PlatformIO build cache to speed up compilation.
 
## Additional Context

Significant improvement when switching branches, as unchanged files
won't need re-compilation.
See
[Docs](https://docs.platformio.org/en/latest/projectconf/sections/platformio/options/directory/build_cache_dir.html).

Note: May need to manually delete .cache folder if updating toolchain or
framework.

---

### 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-04-27 16:45:38 +03:00
Arthur Tazhitdinov 02822e01b3 feat: include short SHA in CROSSPOINT_VERSION (#1728)
## Summary

* Includes short SHA in version string for better version tracking, so
it looks like `1.1.0-dev-feat-kosync-xpath-05c6cf8`
* Closes
https://github.com/crosspoint-reader/crosspoint-reader/issues/1247

---

### 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-04-27 15:52:09 +03:00
Stefan Blixten Karlsson 198b9d7786 fix: swedish translations (#1762)
## Summary

* Add swedish translation of strings added by "feat: Support for
multiple OPDS servers (#1209)"

---

### 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-04-27 00:01:36 +03:00
Zach Nelson 15e0d39d2c Revert "feat: Add royalty.dev funding integration" (#1745)
Reverts crosspoint-reader/crosspoint-reader#1741

I don't entirely understand Royalty.dev and want to vet this further.
2026-04-22 16:29:09 -05:00
Arthur TazhitdinovandCopilot 6c4e946e27 chore: git pre-commit hook for format fix (#1730)
## Summary

* Adds pre-commit git hook that automatically runs
./bin/clang-format-fix

---

### 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 >**_

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-04-22 23:20:03 +03:00
pablohc 907e14da28 fix: pressing space barely moves input cursor (#1729) (#1733)
## Summary

* **What is the goal of this PR?** 
Fix the visual keyboard cursor not advancing when typing a space. Typing
one space leaves the cursor at position 0; typing two spaces advances it
only by one space width. This affects all input types except URL.
* **What changes are included?** 
Replace `getTextWidth()` with `getTextAdvanceX()` in four locations
within `KeyboardEntryActivity::render()` for cursor positioning and
line-wrapping calculations.

## Additional Context

* **Root cause**: `getTextWidth()` returns the bounding-box width of the
drawn glyphs. The space glyph has `width=0` and `height=0` (it's
invisible), so `getTextWidth(" ") == 0`. The trailing `advanceX` of the
last character is only flushed when the *next* character is processed,
so a string ending in space reports zero width. `getTextAdvanceX()`
correctly includes the final glyph's advance, matching how `drawText()`
actually positions characters.

---

### 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-04-22 22:18:26 +03:00
Justin Mitchell b9b795bf45 feat: Add royalty.dev funding integration (#1741)
Adds royalty.dev as a funding platform for contributors by adding custom
funding link and badge to README

A maintainer would need to click the "Are you the maintainer?" button on
https://app.royalty.dev/crosspoint-reader/crosspoint-reader to install
the github app which pulls the PRs in and ranks them.

(Disclosure, Royalty is a product I've recently built and am using for
my own OSS and currently gating custom font builds and nightlys on
crosspointreader.com)

This page currently can collect donations but without the integration it
wont notify contributors and it wont rescore fresh PRs.
2026-04-22 13:32:36 -05:00
Stefan Blixten Karlsson b21b10f4b9 fix: Add swedish keyboard translations (#1726)
## Summary

* Add the swedish translations of the text strings added by #1697 

---

### 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-04-22 11:25:39 -05:00
Arthur TazhitdinovandCopilot 1cf2239742 feat: Support for multiple OPDS servers (#1209)
## Summary

* Add support for configuring and using multiple OPDS servers, replacing
the previous single-server limitation. Closes
https://github.com/crosspoint-reader/crosspoint-reader/issues/1178
* New OpdsServerStore singleton (modeled after WifiCredentialStore) that
persists up to 8 OPDS servers to /.crosspoint/opds.json with MAC-based
password obfuscation.
* One-time migration from legacy single-server fields in
CrossPointSettings to the new store on first boot.
* New OpdsServerListActivity for the device UI — works in two modes: a
settings list (add/edit/delete servers) and a picker (select which
server to browse). When only one server is configured, the picker is
skipped automatically.
* Renamed CalibreSettingsActivity → OpdsSettingsActivity for clarity. It
now edits individual OpdsServer entries (name, URL, username, password,
delete).
* OpdsBookBrowserActivity now receives an OpdsServer at construction and
uses its credentials for all fetches/downloads, and shows the server
name in the header.
* HttpDownloader::fetchUrl and downloadToFile accept optional per-call
username/password parameters instead of reading from global settings.
* REST API endpoints on CrossPointWebServer: GET /api/opds, POST
/api/opds, POST /api/opds/delete — passwords are never exposed over the
API (only a hasPassword flag), and omitting the password field on update
preserves the existing one.
* Web UI (SettingsPage.html) with dynamic OPDS server management cards —
add, edit, save, and delete servers from the browser.
<img width="932" height="906" alt="SCR-20260416-stvu"
src="https://github.com/user-attachments/assets/a8f18d84-4204-46a0-bb31-b73d24b3255f"
/>


## Additional Context

* The OpdsServerStore JSON format and obfuscation scheme are identical
to WifiCredentialStore, so the same JsonSettingsIO infrastructure
handles both.
* The web API uses POST /api/opds/delete instead of DELETE /api/opds
because the ESP32 WebServer doesn't support the DELETE method with a
request body.
* Existing single-server configurations are migrated automatically — no
user action required. After migration the legacy CrossPointSettings
fields are cleared so it only runs once.
* The HttpDownloader changes are backward-compatible: the credential
parameters default to empty strings, so existing callers are unaffected.

---

### 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 >**_

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-04-21 18:35:41 -05:00
Zach Nelson c5f82709c0 fix: Replaced Bookerly with Noto Serif for licensing reasons (#1736)
## Summary

Fixes #258. Bookerly is not licensed for use in CrossPoint. Switched to
Google's open Noto Serif font.

---

### 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-04-21 17:51:07 -05:00
Pietro CampagnanoandClaude Sonnet 4.6 867fb7cbe2 style: unify page headers across web UI (#1702)
## Summary

* **Goal**: Align `FilesPage` and `SettingsPage` headers with the
existing `HomePage` pattern.
* **Changes**:
- Added `<h1>📚 CrossPoint Reader</h1>` at the top of `<body>` on both
pages.
- Demoted the page-specific `📁 File Manager` heading on `FilesPage` from
`<h1>` to `<h2>` (inside `.page-header`), so every page has a single
`<h1>` — the app name.
- Moved the accent-colored `border-bottom` from `.page-header` to `h1`,
matching the HomePage CSS.

All three pages now share the same top structure:
`<h1>CrossPoint Reader</h1>` → nav links → page content.

## Screenshots

### Home

**Before and after is the same**
<img width="1153" height="388" alt="home before"
src="https://github.com/user-attachments/assets/4eb1c969-89fd-4965-ba17-5ff956576a8b"
/>

### File Manager

**Before**
<img width="1153" height="388" alt="files before"
src="https://github.com/user-attachments/assets/ba689d6b-56b1-44d1-a151-3f67b0fcdb16"
/>

**After**
<img width="1153" height="388" alt="files after"
src="https://github.com/user-attachments/assets/437db5e4-9102-4a19-b049-7a93e4e403b2"
/>

### Settings

**Before**
<img width="1153" height="388" alt="settings before"
src="https://github.com/user-attachments/assets/39a7e489-f0d3-4ac1-a2d7-98028aae65c0"
/>

**After**
<img width="1153" height="388" alt="settings_after"
src="https://github.com/user-attachments/assets/1c479618-ec8b-4dee-b704-3da07e6c279e"
/>



## Additional Context

* Pure markup + CSS, no JavaScript or behavioral changes.
* Improves HTML semantics (single `<h1>` per page, referring to the
app).
* No new CSS duplication introduced — borrows existing styling from
HomePage.

---

### 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**_

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-21 17:32:13 -05:00
Zach Nelson 5e26baef63 chore: One Italian translation tweak (#1718)
## Summary

Following up on #1685. One tweaked string confirmed by @alan0ford.

---

### 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-04-21 15:20:41 -05:00
CSCMe 8154f88dbe fix: Erroneous navigation with long filenames in footnote links (#1723)
## Summary

* **What is the goal of this PR?** 
* Increase the allowed link length for footnotes
* **What changes are included?** 
* Un-magic-numbers parts of the footnote parser
* Increases max href length

## Additional Context

* Additional RAM usage was not noticable to me, worst case 16 * 128 * 2
= 4KB instead of previously 16 * 88 * 2 ~ 2.8KB, see #1031
* Motivation: My book navigated me to the start of the footnote chapter,
which required manual navigating for every footnote due to ungodly file
name choices which pushed the required href length to 72, which caused
truncating, which caused Crosspoint to not find the anchor (Sacred and
Terrible Air, available via reddit)

---

### 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-04-21 18:34:17 +03:00
Stefan Blixten Karlsson 56d3ab929c feat: show long branch names (#1727)
## Summary

* This change will make better use of the space available for Title and
Subtitle in the Settings (Theme: Lyra).
Instead of having a fixed maxwidth in pixels for subtitle, it will use
the whole line and shorten the longest one if needed.

So instead of: <img width="480" height="140" alt="image"
src="https://github.com/user-attachments/assets/67e4777b-10d0-4a05-b5c7-fb775776c186"
/>

It will now show: <img width="480" height="142" alt="image"
src="https://github.com/user-attachments/assets/2f143f4e-75d7-45c4-b61a-aad56bdf39fb"
/>


---

### 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-04-21 18:33:33 +03:00
Pietro Campagnano 9dab5f471b style: put page name first in browser titles (#1703)
## Summary

* **Goal**: Make browser tab titles, bookmarks, and history entries
distinguishable across pages (including individual folders in the File
Manager).
* **Changes**:
- Reordered `<title>` so the page name comes first, followed by
`CrossPoint Reader`. Browser tabs usually truncate titles from the
right, and bookmark/history lists are sorted alphabetically by title —
putting the differentiating part first keeps it visible in both cases,
instead of every entry starting with `CrossPoint Reader - …`.
- On `FilesPage`, added a small JS hook that updates `document.title`
with the current subfolder leaf when a `path` query parameter is present
— previously all folders shared the same title.

| URL | Title before | Title after |
|-----|--------------|-------------|
| `/` | `CrossPoint Reader` | `CrossPoint Reader` _(unchanged)_ |
| `/settings` | `CrossPoint Reader - Settings` | `Settings - CrossPoint
Reader` |
| `/files` | `CrossPoint Reader - Files` | `Files - CrossPoint Reader` |
| `/files?path=/Books` | `CrossPoint Reader - Files` | `Books - Files -
CrossPoint Reader` |
| `/files?path=/Books/Fantasy` | `CrossPoint Reader - Files` | `Fantasy
- Files - CrossPoint Reader` |

## Additional Context

* Pure client-side change: `<title>` tags plus ~3 lines of JavaScript on
FilesPage. No network, behavior, or memory impact.
* The static `<title>` tag remains as a fallback if JS fails.
* `HomePage` title left as-is since the app name alone is the standard
convention for a root/home page.

---

### 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-04-21 16:38:20 +03:00
mvidelatraduc ce22deab7c chore: Update spanish.yaml (#1717)
## Summary

Make this more transparent in Spanish:

`STR_AUTO_TURN_PAGES_PER_MIN: "Auto Turn (Pages Per Minute)"`

A weakness of the current translation is that the verb used is "to
read", but it's not clear that it is the user who will be reading a
certain amount of pages per minute. I've replaced it with the verb for
"to turn (pages)" which is the (analogous) physical action happening
regardless of who executes it, and the abbreviated adverb to clarify
that this is an automatic process. Lastly, a clarification of what the
number means in a simple proportion: "X pages in a minute". The Spanish
is very good overall, this one string is very hard to do in such a short
space. Even more so given that the number cannot be accompanied by any
units, so it must be clear from the text alone what it means.

## Additional Context

N/A

---

### 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-04-20 19:29:13 -05:00
Pietro CampagnanoandClaude Sonnet 4.6 e28918b24d fix: pluralize folder/file counts correctly in file list summary (#1701)
## Summary

* **Goal**: Fix incorrect pluralization in the file manager web UI
summary line.
* **Changes**: `FilesPage.html` always rendered the plural forms
("folders"/"files") regardless of count. The summary now selects
singular or plural based on each count.

Example:
- Before: `1 folders, 1 files, 12 KB`
- After: `1 folder, 1 file, 12 KB`

## Additional Context

* Cosmetic-only fix — no behavior, performance, or memory impact.
* Change is fully client-side (JavaScript inside a single HTML
template).
* English-only; web UI localization is out of scope here.

---

### 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**_

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-20 15:36:19 -05:00
Brandon PhilipsandClaude Sonnet 4.6 c0ee096841 fix: relative opds paths and query param with copyparty (#1535)
## Summary

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

This PR fixes bugs when using Copyparty as an OPDS server. 

https://github.com/9001/copyparty?tab=readme-ov-file#opds-feeds

OPDS uses a query parameter `?opds` to differentiate between HTML
requests and OPDS requests for the same path. It also uses relative
paths in the responses instead of full paths. Crosspoint didn't handle
these two cases.

* **What changes are included?**

Fixes to the two issues above.

## Additional Context

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

Here is some example XML from my Copyparty instance:
https://gist.github.com/philips/9ecec29dfb69ed0591b032f16e799675

### 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 | PARTIALLY | NO
>**_

Partially. I used Claude code to write the fix. I am not a strong C++
programmer. But, I manually compiled and tested.

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-20 15:35:17 -05:00
pablohc 1a145fe085 fix: keyboard feedback #1644 (#1697)
Addresses reviewer feedback from #1644:
- **Localize keyboard hint strings** — 13 hardcoded English strings
replaced with \`tr()\` macro (\`STR_KB_HINT_*\`), making them
translatable across all 22 languages (fallback to English when not yet
translated)
- **Deduplicate \`Lyra3CoversMetrics\`** — now derives from
\`LyraMetrics\` via lambda copy, overriding only \`homeCoverTileHeight\`
and \`homeRecentBooksCount\` (eliminates ~30 duplicated metric fields)
- **Unify keyboard drawing in \`BaseTheme\`** — \`drawTextField\` and
\`drawKeyboardKey\` overrides removed from \`LyraTheme\`; variability
controlled via \`keyboardKeyCornerRadius\` metric (0=Base, 6=Lyra).
Unified text field padding to 6, adopted Lyra's secondary label draw
order (main first, then secondary)
- **Add URL-optimized keyboard layout** — \`urlLayout\` with \`:\` and
\`/\` replacing \`=\` and \`,\` for easier URL input without switching
to SYM 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-04-20 12:47:57 -05:00
Justin Mitchellandjpirnay 302dea1eea fix: Switch to xpath map for paragraph level syncing in KOSync (#1686)
Switch KOReader sync progress mapping from chapter matching to
XPath-based mapping.

- resolves KOReader positions using real XHTML ancestry paths
- supports paragraph-based upload mapping with text offsets
where needed
- passes the current paragraph index into sync so uploads map
back to KOReader more accurately

No HTTP client changes are included. No reader-state or resume-flow
changes are included.

---------

Co-authored-by: jpirnay <jens@pirnay.com>
2026-04-20 12:47:41 -05:00
Angandcoderabbitai[bot] e8645ed92e docs: fix typos (#1705)
## Summary

Fix typos found via `codespell -S
*.txt,*.yaml,generate_kerning_ligature_epub.py -L
currenty,flate,ser,localy,logicaly,ans,clen,portugues,notin,curren`

## 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**_

---------

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2026-04-20 09:12:40 +03:00
Zach Nelson 64f5ef018a feat: Support for proportional numeral spacing (#1414)
## Summary

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

Reading a book with frequent numbers, I noticed that the spacing between
numeral glyphs was strangely large. This was because Bookerly and Noto
Sans default to tabular figures, where every digit gets an identical
advance width. This is designed for column alignment in spreadsheets,
but in rendering prose it produces visually wide gaps between digits.

This change adds a `--pnum` flag to fontconvert.py that applies the
font's OpenType `pnum` (proportional numerals) feature during
conversion. When active, the converter:
- Parses the GSUB table for pnum SingleSubst lookups
- Resolves substitute glyph indices via fonttools' glyph order
- Loads the proportional alternate glyphs instead of the tabular
defaults
- Includes substitute glyph names in kern pair extraction, so kerning
data that references proportional alternates is captured

Bookerly's proportional alternates also carry digit-digit and
digit-punctuation kerning that the tabular glyphs lack (e.g., at 16pt
7->4 at -1.69px, 7->. at -2.31px, 7->1 at +1.00px).

Noto Sans gains proportional advances but no new kerning (its
proportional glyphs have no kern class data in the font).

OpenDyslexic is unaffected. Its `cmap` already points to proportional
glyphs, so `--pnum` is a no-op. `--pnum` is intentionally omitted from
OpenDyslexic in the build script for deliberately uniform digit spacing
as an accessibility choice.

UI fonts (Ubuntu, notosans_8) also omit `--pnum` to preserve tabular
alignment for page numbers, battery percentages, etc.

| Before | After |
| -- | -- |
| <img
src="https://github.com/user-attachments/files/26042238/screenshot-31673.bmp"
width="300" /> | <img
src="https://github.com/user-attachments/files/26042241/screenshot-124075.bmp"
width="300" /> |

---

### 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-04-18 17:03:04 -05:00
Zach Nelson 3cdfc6c781 chore: Improved Italian translations (#1685)
## Summary

Improved Italian translations provided by @alan0ford, closes #1578.

---

### 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-04-18 16:30:46 -05:00
pablohc 77b2c31635 refactor: redesign on-screen keyboard (#1644)
# Refactor: Redesign On-Screen Keyboard

## Summary

Complete redesign of the on-screen keyboard (used for WiFi password,
KOReader, Calibre URLs) with improved layout, navigation, visual style,
and new input features: **cursor mode** for text navigation, **password
mode** with visibility toggle, and **URL mode** with pre-defined
snippets.

## Screenshots

### Base Theme
|**master** | **PR #1644** |
|----------|-------------|
| <img width="480" height="800" alt="image"
src="https://github.com/user-attachments/assets/49125857-12d0-4020-b872-05d0ddbf1d94"
/> | <img width="480" height="800" alt="image"
src="https://github.com/user-attachments/assets/ad16656b-d66e-43dd-8697-2b85f709d7f8"
/> |

### Lyra Theme
| **master** | **PR #1644** |
|----------|-------------|
| <img width="480" height="800" alt="image"
src="https://github.com/user-attachments/assets/d9901251-9154-48d2-83b4-376d3223d132"
/> | <img width="480" height="800" alt="image"
src="https://github.com/user-attachments/assets/84b45949-ed61-4924-af1b-d570c4c61e13"
/> |

### Keyboard States

| ABC Mode | Symbol Mode | URL Mode |
|----------|-------------|----------|
| <img width="480" height="280" alt="image"
src="https://github.com/user-attachments/assets/6397a82e-50b8-4d03-92e0-f707ed7c9054"
/> | <img width="480" height="280" alt="image"
src="https://github.com/user-attachments/assets/205d34fe-0413-49e9-9db2-30569c297ba0"
/> | <img width="480" height="280" alt="image"
src="https://github.com/user-attachments/assets/801ddeab-e082-4a22-a9df-c66adb1afc16"
/> |

| Cursor Mode | Password Toggle |
|-------------|-----------------|
| <img width="480" height="800" alt="image"
src="https://github.com/user-attachments/assets/841773e1-7a3c-45e5-aa89-e5787255608c"
/> | <img width="480" height="800" alt="image"
src="https://github.com/user-attachments/assets/9487a0b3-3f47-41ed-9cd3-b24e56e342a8"
/> |

## Changes

### Layout (10-column uniform grid)
- Reduced from 13/11/10 columns per row to **10 uniform columns** across
all rows
- Keyboard now uses **90% of screen width** (was ~66%)
- Row 0: Numbers `1-9, 0` with secondary symbols (`!@#$%^&*()`)
- Rows 1-3: Standard QWERTY letters
- Bottom row: `shift` | `#@!` | `___` | `←` | `OK`

### New Symbol Mode (#@!)
- New mode toggle key `#@!` / `abc` switches between letter and symbol
layouts
- Symbol layout: 4 rows (numbers, inverted symbols, paired symbols,
loose symbols)
- Covers all 95 printable ASCII characters
- **No secondary hints, no long-press** in symbol mode (simple and
direct)
- SHIFT key remains visible but **disabled** in symbol mode

### URL Mode
- In `InputType::Url`, the Space key becomes a **URL toggle** button
- Activating URL mode replaces the 4 content rows with a **3×3 grid of
URL snippets**:
  - Col 0 (protocols): `https://`, `http://`, `/opds`
  - Col 1 (hosts/ports): `www.`, `192.168.`, `:8080`
  - Col 2 (domains): `.com`, `.org`, `.net`
- Snippets are inserted as full strings at the cursor position
- URL mode **persists** after inserting a snippet (does not
auto-deactivate)
- Column alignment: col 0 over ABC, col 1 over URL, col 2 over Del
- Up/Down navigation maps `bottomCol - 1` / `urlCol + 1`
- SHIFT disabled in URL mode
- SpecMode (`abc`) exits URL mode back to ABC
- SpecSpace (`URL`) toggles URL mode on/off; selection always stays on
the URL button
- Button styled with `KeyboardKeyType::Mode` for consistent outline

### Cursor Mode
- **Enter**: Long-press Up (500ms) while in keyboard mode
- **Exit**: Short-press Down while in cursor mode (resets
`passwordVisible`, clears toggle position)
- **Navigate**: Left/Right move cursor position within text (one
position per press, no continuous repeat)
- **Visual**:
  - Keyboard mode: underline cursor (2px line + serifs)
  - Cursor mode: inverted block cursor (black fill + white character)
- Block width adapts to the actual character width under cursor (minimum
6px for narrow chars like space)
- Block position includes inter-character kerning offset for correct
alignment (calculated via string-difference: `getTextWidth(before+char)
- getTextWidth(before) - getTextWidth(char)`)
  - End-of-text: thin 6px block
- Password hidden: 3-part drawing (Part 1 + block + Part 3) to prevent
block overflow onto `*` characters
- Toggle position: caret ("I") cursor at saved position, `[abc]`/`[***]`
label with inverted selection
- **Inactive key styling**:
  - BaseTheme: 2px outline rectangle
  - LyraTheme: gray filled rounded rectangle (`Color::LightGray`)
- **Password toggle position**: in cursor mode (Password only), Hold
Right (500ms) enters toggle — caret cursor shows saved position,
`[abc]`/`[***]` label becomes selected. Press Confirm to toggle
`passwordVisible`. Press Left to restore cursor to saved position. Right
from toggle is a no-op. Down from toggle exits to keyboard.
- `cursorPos` persists between keyboard and cursor modes

### Password Mode
- `InputType::Password` enum replaces `bool isPassword` parameter
- Text is masked with `*` except for one revealed character:
  - Keyboard mode: reveals character at `cursorPos - 1`
- Cursor mode: no reveal in display text (block cursor draws actual char
directly)
- **Toggle `[abc]`/`[***]`**: accessible via cursor mode — Hold Right
(500ms) enters toggle position, Confirm toggles visibility, Left exits
back to cursor. Caret ("I") shown at saved position while in toggle.
- `passwordVisible` resets to `false` when exiting cursor mode
- **Long-press Del (1.5s)**: clears all text and resets cursor to 0

### InputType Enum
- Replaced `bool isPassword` constructor parameter with `enum class
InputType { Text, Password, Url }`
- Callers updated: `WifiSelectionActivity`, `KOReaderSettingsActivity`,
`CalibreSettingsActivity`

### Contextual Tips
- `"Tips:"` header followed by context-sensitive hints, centered between
text field underline and keyboard as a block
- ABC mode: `"Hold SELECT for UPPERCASE or secondary char"` (shift ON:
`"lowercase"` variant) + `"Hold DEL to clear all text"` (only if text
not empty)
- ABC + `InputType::Url`: same + `"Press URL for snippets"`
- Symbol mode: `"Hold DEL to clear all text"` (only if text not empty)
- URL mode: `"Press ABC to exit URL mode"` + `"Hold DEL to clear all
text"` (only if text not empty)
- Cursor mode: `"Press DOWN to return to keyboard"`

### Hint Phases (cursor mode, Password only)
- **Phase 1**: `"Hold UP to edit entry"` — shown after 2× DEL press,
auto-hides after 4s, positioned below underline
- **Phase 2**: `"Press < or > to move cursor"` + dynamic password toggle
hint — shown when entering cursor mode, positioned below underline,
visible until exit
- When `!passwordVisible`: `"Hold > then press [abc] to show password"`
  - When `passwordVisible`: `"Hold > then press [***] to hide password"`
  - When in toggle position: `"Press < to return to cursor position"`

### Long-Press Alternative Character
- Holding Confirm (>500ms) inserts the **alternative character** instead
of the primary
- Letters: long-press inserts opposite case (e.g., `a`→`A`, `A`→`a`)
- Numbers/symbols (row 0): long-press inserts secondary (e.g., `0`→`)`,
`)`→`0`)
- Only active in ABC mode; disabled in Symbol mode and URL mode
- **`InputType::Url`**: Hold SELECT on ABC rows 1+ (letters) returns
primary character only (same as short press). Row 0 (symbols) still
returns secondary character on Hold SELECT.

### Shift (2 sticky states)
- Reduced from 3 states (shift/SHIFT/LOCK) to **2 sticky states**
(shift/SHIFT)
- Shift stays active after typing until manually toggled off
- Label: `shift` (off) / `SHIFT` (on)

### SpecialKeyType Enum
- `enum class SpecialKeyType { Shift, Mode, Space, Del, Ok }` replaces
plain `enum` (`SpecShift`, `SpecMode`, etc.) for type safety
- All switch cases updated to `SpecialKeyType::*` with
`static_cast<int>()` for array indexing
- `onExit()` reverted to simple `Activity::onExit()` call (half-refresh
removed)
- **Bottom row column mapping**: navigating up/down between content rows
and bottom row uses `col/2` and `col*2` formulas for consistent
positioning (10 cols ↔ 5 cols)
- **URL mode column mapping**: `bottomCol - 1` / `urlCol + 1` (3 cols ↔
5 cols)
- **Wrap-around**: row 0 → up → bottom row and bottom row → down → row 0
both apply correct column mapping

### Visual Improvements (both Base and Lyra themes)
- **Space key**: underscore-style horizontal line (60% of key width, 3px
thick)
- **Delete key**: arrow `←` drawn with lines (3px thick) instead of
"DEL" text
- **Secondary label** (ABC row 0): small hint in top-right corner with
separation from primary number
- **BaseTheme**: selection uses **inverted fill** (black rect + white
text) instead of `[bracket]` markers
- **BaseTheme**: text field brackets drawn as **stretchable lines** that
adapt to multi-line input (1px normal, 3px cursor mode)
- **LyraTheme**: text field uses **fixed-width underline** (16px
margins, 8px each side) instead of stretchable line (2px normal, 3px
cursor mode)
- **Both themes**: special keys (shift, mode, space, del, OK) have
bordered/bordered-rounded rectangles
- **Font size**: keyboard uses `UI_12_FONT_ID` in both themes (was
`UI_10` in Base)
- **Key height**: 40px in all themes for better proportions
- **Layout unification**: text and password toggle are left-aligned in
all themes (`keyboardCenteredText = false` for Lyra/Lyra3Covers)
- **`primaryOffset` removed**: dead code eliminated from BaseTheme and
LyraTheme `drawKeyboardKey`

### New Theme Metrics
- `keyboardVerticalOffset`: per-theme vertical adjustment of keyboard
position
  - Base: `-13`, Lyra: `-7`
- `keyboardBottomKeySpacing`: independent spacing for bottom row keys
  - Base: `5`, Lyra: `5`
- Bottom-aligned keyboard in both themes for consistent vertical
positioning
- Bottom row total width calculated to match content rows width (10-col
based, consistent across modes)
- 4px extra gap between content rows and bottom row when `bkSpacing > 0`
- `keyboardCenteredText`: `false` for all themes (unified left-aligned
text)

### Defensive Improvements
- **State reset on re-entry**: `onEnter()` resets all mutable state
(`symMode`, `urlMode`, `cursorMode`, `togglePos`, `passwordVisible`,
`shiftState`, `selectedRow`, `selectedCol`, `rightHeld`,
`rightLongHandled`, `savedCursorPos`, `rightStartCursorPos`,
`delPressCount`, `hintVisible`, `hintShowTime`) — prevents stale state
when re-entering the keyboard
- **Bounds checking**: `insertChar`/`insertString` clamp `cursorPos` to
`text.length()` before inserting
- **Empty string guard**: `insertString` returns early on empty string
- **`std::string::npos`**: used instead of `SIZE_MAX` for size_t
sentinel (proper C++ idiom)
- **`<algorithm>` header**: included for `std::max`

## Files Modified

| File | Changes |
|------|---------|
| `src/activities/util/KeyboardEntryActivity.h` | `InputType` enum,
`KeyDef` struct, 10-col layouts, cursor/password/URL/toggle state, hints
(`delPressCount`, `hintVisible`, `hintShowTime`), held vars
(`rightHeld`, `rightLongHandled`, `savedCursorPos`,
`rightStartCursorPos`), `mapColContentBottom` helper |
| `src/activities/util/KeyboardEntryActivity.cpp` | Complete rewrite:
layout rendering, symbol/cursor/password/URL modes, toggle position,
long-press, contextual tips, hint phases, block cursor kerning
alignment, defensive bounds checks, state reset |
| `src/components/themes/BaseTheme.h` | `KeyboardKeyType` enum, new
`drawTextField`/`drawKeyboardKey` signatures, `keyboardVerticalOffset`,
`keyboardBottomKeySpacing` metrics |
| `src/components/themes/BaseTheme.cpp` | Redesigned `drawTextField`
(stretchable brackets), `drawKeyboardKey` (inverted selection,
space/delete graphics, secondary label, inactive selection), removed
`primaryOffset` dead code |
| `src/components/themes/lyra/LyraTheme.h` | Override signatures,
`keyboardVerticalOffset`, `keyboardBottomKeySpacing`,
`keyboardKeyHeight` adjustments |
| `src/components/themes/lyra/LyraTheme.cpp` | `drawTextField` (fixed
underline), `drawKeyboardKey` (rounded rects for special keys,
space/delete graphics, secondary label, inactive selection), removed
`primaryOffset` dead code |
| `src/components/themes/lyra/Lyra3CoversTheme.h` |
`keyboardCenteredText = false`, `keyboardVerticalOffset = -7`, inherits
Lyra overrides |
| `src/activities/network/WifiSelectionActivity.cpp` | `bool isPassword`
→ `InputType::Password` |
| `src/activities/settings/KOReaderSettingsActivity.cpp` | `bool
isPassword` → `InputType::Text`/`InputType::Password`/`InputType::Url` |
| `src/activities/settings/CalibreSettingsActivity.cpp` | `bool
isPassword` → `InputType::Text`/`InputType::Password`/`InputType::Url` |

## Backward Compatibility

- **API change**: Constructor parameter changed from `bool isPassword`
to `InputType inputType` (default `InputType::Text`)
- **All callers updated**: WiFi, KOReader, and Calibre integrations
migrated to new `InputType` enum

## Testing

### Input & Text Handling
- [x] Empty input → press OK (submit empty string)
- [x] Back button → cancel (no text returned)
- [x] Pre-filled initial text (e.g., editing existing WiFi password)
- [x] Password mode: text masked with `*` characters, one character
revealed
- [x] Delete on empty text (no crash)
- [x] Very long text near maxLength limit
- [x] URL with path and port (~60 chars)
- [x] Multi-line text wrapping in input field
- [x] Space insert in middle of text (cursor mode)
- [x] Delete last character repeatedly
- [ ] Type all 95 printable ASCII characters

### Mode Switching
- [x] ABC → #@! preserves typed text and cursor position
- [x] #@! → ABC preserves typed text and cursor position
- [x] Shift state preserved when switching modes
- [x] Switch modes multiple times rapidly

### Shift Behavior
- [x] Shift OFF → type letter → inserts lowercase, shift stays OFF
- [x] Shift ON → type letter → inserts uppercase, shift stays ON
- [x] Shift ON → type number → inserts symbol, shift stays ON
- [x] Shift ON → navigate rows → shift stays ON
- [x] Shift ON → switch to #@! → shift shows "shift" (disabled)
- [x] Shift ON → switch to ABC → shift state preserved
- [x] Shift ON → switch to URL → shift shows "shift" (disabled)
- [x] Shift disabled in URL mode: pressing shift does nothing

### Long-Press
- [x] Long-press letter with shift OFF → inserts uppercase
- [x] Long-press letter with shift ON → inserts lowercase
- [x] Long-press number → inserts secondary symbol
- [x] Long-press symbol (row 0) → inserts opposite (number)
- [x] Long-press key without secondary (e.g., `-`, `=` in rows 2-3) →
inserts primary character on release
- [x] Long-press on special keys (shift, mode, space, del, ok) → no
alternative inserted
- [x] Long-press in #@! mode → no effect (disabled)
- [x] Long-press in URL mode → no effect (disabled)
- [x] Long-press number in row 0 with InputType::Url → inserts secondary
symbol (same as non-URL)
- [x] Short press after cancelled long-press → normal behavior
- [x] Long-press at maxLength → no character inserted
- [x] Long-press Del (1.5s) → clears all text

### Cursor Mode
- [x] Long-press Up → enters cursor mode
- [x] Short-press Down → exits cursor mode (resets passwordVisible)
- [x] Left/Right navigate within text
- [x] Left at position 0 → no movement
- [x] Right at end of text → no movement in Text mode, enters toggle in
Password mode (Hold Right)
- [x] Block cursor visual: correct width for character, thin block at
end
- [x] Underline cursor visual (keyboard mode): correct position with
serifs
- [x] Inactive key styling: outline (Base) or gray fill (Lyra) on
selected key
- [x] Typing with cursor mid-text → inserts at cursor position
- [x] Deleting with cursor mid-text → deletes character before cursor
- [x] Exit cursor mode → type at cursor position (inserts mid-text, not
at end)
- [x] Exit cursor mode from toggle → cursor at saved position (not end
of text)

### Password Mode
- [x] Masked text with one revealed character at `cursorPos - 1`
- [x] Cursor mode: block shows actual character, display text all `*`
- [x] Toggle `[abc]`/`[***]`: Hold Right (500ms) in cursor mode enters
toggle, Confirm toggles visibility, Left exits back to cursor
- [x] Exiting cursor mode resets `passwordVisible` to false
- [x] Long-press Del clears all text

### URL Mode
- [x] URL toggle activates/deactivates URL mode
- [x] URL button stays selected after toggle (both on and off)
- [x] Deactivating URL mode returns to ABC (not SYM)
- [x] 3×3 snippet grid displays correctly
- [x] Column alignment: col 0 over ABC, col 1 over URL, col 2 over Del
- [x] Snippet insertion: inserts full string at cursor position
- [x] URL mode persists after snippet insertion
- [x] Shift disabled in InputType::Url
- [x] SpecMode (`abc`) exits URL mode to ABC
- [x] Up/Down navigation between URL grid and bottom row

### Re-entry State Reset
- [x] Enter keyboard → activate URL mode → exit → re-enter → URL mode
OFF
- [x] Enter keyboard → switch to SYM → exit → re-enter → ABC mode
- [x] Enter keyboard → enter cursor mode → exit → re-enter → keyboard
mode
- [x] Enter keyboard → enter toggle pos → exit → re-enter → togglePos
OFF
- [x] Enter keyboard → activate shift → exit → re-enter → shift OFF
- [x] Enter password keyboard → toggle password visible → exit →
re-enter → password hidden

### Navigation
- [x] Left/right wrap-around within content rows
- [x] Left/right wrap-around within bottom row
- [x] Up from row 0 → bottom row (correct column mapping)
- [x] Down from bottom row → row 0 (correct column mapping)
- [x] Up from bottom row → last content row (correct column)
- [x] Down from last content row → bottom row (correct column)
- [x] Navigate horizontally in bottom row, then up → correct content
column
- [x] Navigate horizontally in bottom row, then down (wrap) → correct
content column

### Visual (both themes)
- [x] Secondary hints only on ABC row 0
- [x] No secondary hints in #@! mode or URL mode
- [x] No secondary hints on letter rows (1-3)
- [x] Space bar: horizontal line centered, not touching edges
- [x] Delete: arrow `←` drawn correctly
- [x] Selected key: inverted colors (black fill, white text)
- [x] All special keys have border rectangles
- [x] Fixed underline in text field (Both themes)
- [x] Mode key label: `#@!` in ABC mode, `abc` in symbol mode, `abc` in
URL mode
- [x] URL key label: `URL` (only in InputType::Url), styled same as
other bottom keys
- [x] Shift label: `shift` when OFF, `SHIFT` when ON, `shift` when
disabled (SYM/URL)
- [x] Both themes: bottom row total width matches content rows width
- [x] URL snippet grid centered over ABC/URL/Del buttons

### Device & Theme Coverage
- [ ] Base Theme on X3
- [x] Base Theme on X4
- [ ] Lyra Theme on X3
- [x] Lyra Theme on X4
- [ ] Lyra Extended Theme on X3
- [x] Lyra Extended Theme on X4

### Toggle Position
- [x] Hold Right > 500ms in cursor mode (Password) → enters toggle,
caret visible at saved position
- [x] Short-press Right in cursor mode (Password) → advances cursor 1
position, does not jump to toggle
- [x] Short-press Left in cursor mode (Password) → moves cursor left 1
position, from toggle returns to saved position
- [x] Confirm in toggle → toggles `passwordVisible`
- [x] Left from toggle → returns to saved position, caret disappears,
block cursor appears
- [x] Right from toggle → no-op
- [x] Down from toggle → exits to keyboard, cursor at saved position
- [x] Hold Right in cursor mode (InputType::Text) → no effect
- [x] Hold Right in cursor mode (InputType::Url) → no effect
- [x] Hold Right < 500ms released in cursor mode (Password) → short
press, advances cursor 1
- [x] No continuous repeat when holding Left or Right in cursor mode

### Caret Visual in Toggle
- [x] In toggle: caret "I" visible at saved cursor position
- [x] Character under cursor visible (no gap) in password not-visible
mode
- [x] Character under cursor visible in password visible mode
- [x] `[abc]`/`[***]` label with inverted selection in toggle

### Contextual Tips
- [x] `"Tips:"` header centered above contextual hints
- [x] Single tip → `"Tips:"` + one line
- [x] Multiple tips → `"Tips:"` + multiple lines, all centered as block
- [x] No tips shown when not applicable (e.g., ABC with empty text and
non-URL)
- [x] `"UPPERCASE"` shown when shift OFF
- [x] `"lowercase"` shown when shift ON
- [x] `"secondary char"` shown for InputType::Url

### Hint Phases
- [x] 2× DEL → Phase 1 appears ("Hold UP to edit entry")
- [x] Phase 1 auto-hides after 4s
- [x] Phase 2 appears when entering cursor mode ("Press < or > to move
cursor")
- [x] Phase 2 shows "Hold > then press [abc] to show password" when
`!passwordVisible`
- [x] Phase 2 shows "Hold > then press [***] to hide password" when
`passwordVisible`
- [x] Phase 2 shows "Press < to return to cursor position" when in
toggle
- [x] Phase 2 disappears when exiting cursor mode

### Long-Press `InputType::Url` Behavior
- [x] Hold SELECT on letter rows (rows 1+) with InputType::Url → same
character as short press
- [x] Hold SELECT on row 0 with InputType::Url → secondary character
works normally

### Number Row Reorder
- [x] Number row order: 1-9, 0 left to right
- [x] `(` and `)` are adjacent (positions 8 and 9) via secondary labels
- [x] Long-press on row 0 returns correct secondary symbols in new order
- [x] SYM row 1: `(` and `)` also adjacent (positions 8 and 9)

### Block Cursor Alignment
- [x] Block cursor correctly positioned for consecutive spaces (kerning
offset applied)
- [x] Block cursor correctly positioned for mixed characters (letters,
numbers, symbols)
- [x] Block width minimum 6px for narrow characters (space) — visible as
block, not thin line
- [x] Password hidden: 3-part drawing prevents block overflow onto `*`
characters
- [x] Password visible: block post-loop draws correctly on continuous
text (no 3-part needed)
- [x] End-of-text block: thin 6px block at correct position

### Integration
- [x] WiFi password entry (connect to network)
- [ ] KOReader username, password, and sync server URL
- [ ] Calibre OPDS URL, username, and password
- [ ] Calibre OPDS URL: empty → opens with "https://" prefilled
- [ ] Calibre OPDS URL: type "http://" or "https://" only → saved as
empty
- [ ] Calibre OPDS URL: type full URL → saved correctly
- [ ] Calibre OPDS URL: existing URL → opens with existing URL (not
"https://" prefill)
2026-04-18 16:12:56 -05:00
Justin Mitchell fedcb2f53d fix: boot looping when opening large XTC files (#1648)
Opening XTC files with a high page count (e.g. *The Magic Mountain* at
4,187 pages) causes an immediate `abort()` crash and reboot loop. The
device becomes unusable until the book is removed from the SD card.

**Crash log:**
```
abort() was called at 0x4214a5fb on core 0
```

### Root cause

During `XtcParser::open()`, the parser calls
`m_pageTable.resize(pageCount)` to load the entire page table into RAM.
Each `PageInfo` entry is 16 bytes, so:

- 4,187 pages x 16 bytes = **66,992 bytes (~65KB)** as a single
contiguous heap allocation

On the ESP32-C3 with ~380KB total RAM (no PSRAM), this allocation fails
after firmware, fonts, and the activity system are already loaded.
Because the firmware is compiled with `-fno-exceptions`, the failed
`new` inside `std::vector::resize()` calls `abort()` instead of
throwing.

This affects any XTC file with roughly 3,000+ pages, depending on heap
state at the time of loading.

## Solution

Replace the bulk page table allocation with on-demand reads from the SD
card. Instead of loading all page table entries into a vector at file
open, we now:

1. Read only the **first** page table entry at open time (to get default
page dimensions)
2. Read a **single** 16-byte entry from the SD card each time a page is
loaded

This reduces page table memory usage from `pageCount * 16` bytes to
**zero bytes**, regardless of how many pages the file contains.

### Changes

| File | What changed |
|------|-------------|
| `XtcParser.h` | Removed `std::vector<PageInfo> m_pageTable`. Added
`readPageTableEntry()` for on-demand reads. |
| `XtcParser.cpp` | Replaced `readPageTable()` with
`readFirstPageInfo()`. Updated `getPageInfo()`, `loadPage()`, and
`loadPageStreaming()` to seek and read individual entries from the file.
|

## Trade-offs

### Performance

Each page turn now requires one additional SD card seek + 16-byte read
to look up the page table entry before reading the page data itself.

- SD card sequential read latency: ~0.1-0.5ms for a 16-byte read
- E-ink full display refresh: ~1,000-2,000ms

I personally can't see any performance difference while reading and the
trade off of not boot looping seems to make this well worth it.

### Memory

| Metric | Before | After |
|--------|--------|-------|
| Page table RAM (4,187 pages) | ~65KB | 0 bytes |
| Page table RAM (1,000 pages) | ~16KB | 0 bytes |
| Page table RAM (max 65,535 pages) | ~1MB (impossible) | 0 bytes |
2026-04-18 14:22:34 -05:00
Zach Nelson a888978f95 chore: Clarify X3 RTC in SCOPE.md (#1687)
## Summary

Clarify that X3 device has a reliable RTC chip, so clock feature can be
in-scope depending on device.

---

### 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-04-17 18:39:06 -05:00
Jensen Kuras 23f60a3407 chore: Updating sleep screen dimensions for X3 (#1688)
## Summary

Updating the docs so it has the accurate dimensions for sleep screens on
the X3 and the X4.

---

### 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-04-17 18:09:21 -05:00
2c5a47f9d7 refactor: change ukrainian translation to adaptation (#1684)
## Summary

* **What is the goal of this PR?** 
Make adaptation instead of pure translation.

* **What changes are included?**
Fix issues where text could not fit in line.
Fix didn't cover `KOReader` settings

**Additional Reviever**
@mirus-ua

---

### 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: Kym_Adnriy <kym_andr@gmail.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2026-04-17 14:29:16 -05:00
Zach Nelson ce1756e36f refactor: Added shared XML parser teardown helper (#1438)
## Summary

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

Added `destroyXmlParser()` helper to replace the repeated 4-line parser
cleanup block (stop, clear callbacks, free, null) that was copyied
across 6 XML parser files.

---

### 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-04-16 19:42:05 -05:00
Jan IvanovandJan Ivanov 3b12c083bc fix: make footnotes consider orientation for gutters (#1665)
## Summary

* **What is the goal of this PR?** 
Noticed that the footnotes selection screen does not add proper margins
to accommodate screen orientation
 
* **What changes are included?**
Copied some code over from `EpubReaderChapterSelectionActivity` to
calculate the proper margins in `CW` and `Inverted` orientation

| Before | After |
|--------|--------|
| <img width="578" height="435" alt="image"
src="https://github.com/user-attachments/assets/0518a0c6-13d2-48a1-9283-90c83861e4c2"
/> | <img width="578" height="435" alt="image"
src="https://github.com/user-attachments/assets/ac34365c-72d0-4f07-85a6-17e966b28909"
/> |
| <img width="328" height="435" alt="image"
src="https://github.com/user-attachments/assets/0614f19b-1000-4efe-8ef9-b533d2763a53"
/> | <img width="328" height="435" alt="image"
src="https://github.com/user-attachments/assets/ce9add2f-88e8-4032-a59c-efb55f366604"
/> |

---

### 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: Jan Ivanov <jan.ivanov@sirma.com>
2026-04-17 01:37:41 +03:00
zgredexandPatryk Radtke c4f5c8e931 fix: prevent wallpaper clustering with 16-entry recency buffer (#1606)
## Problem

Custom sleep wallpapers feel repetitive — the same image appearing
multiple times in a short session. Only the single most-recently-shown
index was stored (`uint8_t lastSleepImage`), so on collections of 3–5
images, two-pick cycles were common. On larger collections, any image
could reappear within the next few picks.

## Solution

Add a 16-entry circular recency buffer to `CrossPointState` that
excludes recently shown wallpapers from selection.

**Recency buffer** (`recentSleepImages[16]`, `recentSleepPos`,
`recentSleepFill` — 34 bytes DRAM):
- Tracks the last 16 shown wallpaper indices
- On each pick, rerolls up to 20 times if the candidate was recently
shown
- Window auto-shrinks to `numFiles - 1` for small collections
(guarantees a non-repeat is always possible)
- `isRecentSleep()` clamps to `recentSleepFill` to avoid false positives
on unwritten buffer slots
- State persisted to `state.json` so the buffer survives sleep/wake
cycles

**Migration**:
- Binary (`state.bin`): old `lastSleepImage` field seeded into the new
buffer if valid
- JSON (`state.json`): legacy `lastSleepImage` key detected and seeded
into the buffer when upgrading from older firmware

## Index type fix

`randomFileIndex` upgraded from `uint8_t` to `uint16_t` — silently
truncated for collections larger than 255 wallpapers.

## Repeat probability: before vs after

Chance of seeing a recently-shown image on the next pick. After:
`(numFiles - 16) / numFiles` once collection exceeds the window; 0%
while ≤17.

| Collection | Before | After |
|---|---|---|
| 3 | 50% | 0% |
| 5 | 75% | 0% |
| 10 | 89% | 0% |
| 17 | 94% | 0% |
| 18 | 94% | 6% |
| 20 | 95% | 20% |
| 25 | 96% | 36% |
| 30 | 97% | 47% |
| 50 | 98% | 68% |
| 100 | 99% | 84% |

## Memory impact

| Addition | Size |
|---|---|
| `recentSleepImages[16]` | 32 bytes DRAM |
| `recentSleepPos` + `recentSleepFill` | 2 bytes DRAM |
| **Total** | **34 bytes DRAM** |

## Files changed

- `src/CrossPointState.h` — recency buffer fields + `isRecentSleep()` /
`pushRecentSleep()` declarations
- `src/CrossPointState.cpp` — `isRecentSleep()` / `pushRecentSleep()`
implementations + binary migration path
- `src/JsonSettingsIO.cpp` — JSON serialisation of buffer state + JSON
migration
- `src/activities/boot_sleep/SleepActivity.cpp` — retry loop with
recency check

Co-authored-by: Patryk Radtke <patryk@Patryks-MacBook-Pro.local>
2026-04-17 00:05:21 +03:00
jpirnay 0c5dee3c62 refactor: Refactor drawArc / fillArc for faster execution (#1540)
## Summary

* **What is the goal of this PR?** Replace the o(r^2) routines with a
o(r) scanline logic - will make fillArc roughly 50% faster and drawArc
roughly 5x faster. Still probably unnoticeable.
* **What changes are included?**

## Additional Context


---

### 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-04-16 22:57:03 +03:00
Jon Vexler 81ae9dd779 feat: smooth battery percentage for x4 (#1635)
## Summary

Battery percentage is calculated from the voltage, which is not totally
stable. We smooth the battery percentage using a moving average.

## Additional Context
issue discussion:
https://github.com/crosspoint-reader/crosspoint-reader/issues/1444

---

### 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-04-16 00:28:43 -05:00
jpirnay 40e4c96906 refactor: replace picojpeg with JPEGDEC for cover art conversion (#1517)
## Summary

- Removes the vendored `picojpeg` library and rewrites
`JpegToBmpConverter` to use the already-present `JPEGDEC` (bitbank2)
dependency
- Eliminates the redundancy of having two JPEG decoders in the firmware
- All BMP output (headers, fixed-point scaling, Atkinson/Floyd-Steinberg
dithering) is identical to before — cached cover BMPs are unaffected

## Size impact

| | Before | After | Delta |
|---|---|---|---|
| Flash | 5,754,089 bytes (87.8%) | 5,744,777 bytes (87.7%) | **−9,312
bytes** |
| RAM | 95,212 bytes (29.1%) | 92,852 bytes (28.3%) | **−2,360 bytes** |

## Implementation notes

- `bmpDrawCallback` receives MCU-sized blocks from JPEGDEC (up to 16
rows × MCU-width), accumulates them into a pre-allocated `mcuBuf`, and
applies the same scaling + dithering logic once each MCU row is complete
- File I/O uses a file-scope static `FsFile*` (safe in single-threaded
embedded context) via JPEGDEC's open/read/seek callbacks — same pattern
as `JpegToFramebufferConverter`
- Added a 52 KB free-heap guard before allocating the JPEGDEC object
(~17 KB)
- `lib/picojpeg/` deleted (2,087 lines of C removed)

## Test plan

- [ ] Build compiles without warnings
- [ ] Cover art BMP cache regenerates correctly for EPUB books
- [ ] Home screen thumbnails (1-bit BMP path) render correctly
- [ ] Custom-size thumbnails (`jpegFileToBmpStreamWithSize`) render
correctly

🤖 Generated with [Claude Code](https://claude.com/claude-code)
2026-04-15 23:20:24 -05:00
Stefan Blixten Karlsson 80772ff6b8 fix: footnote link text (#1666)
## Summary

Previouls where ALL whitespace (and square brackes) removed from the
footnote link text, however some link texts are multiworded, like "`turn
to 252`" which were truncated into "`turnto252`", (an example from the
first book "Flight from the Dark" of the Lone Wolf book series by Joe
Dever, see [link](https://www.projectaon.org/en/Main/FlightFromTheDark))

* This change will only remove whitespaces from the beginning and end of
the string
so "` [ 12 ] `" will become "`12`" just like before, and "` turn to 252
`" will become "`turn to 252`".

---

### 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-04-15 21:47:10 -05:00
Stefan Blixten Karlsson 57fc6555f2 fix: missing swedish translations (#1667)
## Summary

* Add missing swedish translations

---

### 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-04-15 19:05:09 -05:00
jpirnay ed54f97909 fix: Fix ghosting on exit of BMPViewer (#1432)
## Summary

* **What is the goal of this PR?** After displaying an image via the
filebrowser ghosting artifacts remained on the screen
* **What changes are included?**

## 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-04-16 00:52:29 +02:00
jpirnay 45cd00889e chore: clang-format.fix.ps1 script: Add .venv to list of path exclusions (#1515)
## Summary

* **What is the goal of this PR?** Dont format files in .venv directory
* **What changes are included?**

## 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? _**< YES | PARTIALLY | NO
>**_
2026-04-14 18:50:37 -05:00
Егор Мартынов cced77783f feat: add orientation-aware popups for reader activities (#1428)
## Summary

Make popups (like "Going to sleep") respect the current screen
orientation when shown from reader activities.

**What changes are included?**

- Apply reader orientation in SleepActivity before showing popup when
`lastSleepFromReader` is true
- Make popup Y-position proportional to screen height (7.5% for
BaseTheme, 16.5% for LyraTheme) instead of hardcoded pixel values,
ensuring correct positioning in both portrait and landscape modes.
- Add `isReaderActivity()` override to all reader sub-screens (menu,
chapter selection, percent selection, footnotes, QR display, KOReader
sync), so sleep popups rotate correctly when entering sleep from any
reader context.

## Additional Context

<img
src="https://github.com/user-attachments/assets/47d88c2c-ffc5-41a7-b3f2-af272ea0150e"
width="400" height="240">

---

### 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 Opus
4.5)
2026-04-14 17:21:55 -05:00
Zach Nelson 1bd7a1de67 refactor: Deduplicate battery drawing code and fix Lyra charging indicator (#1437)
## Summary

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

Following up on #1427:
- Extracted shared battery drawing logic, including lightning bolt, to
reduce duplication.
- In Lyra with segmented battery the lightning bolt was hard to see, so
when charging Lyra now uses a solid battery fill.

---

### 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-04-14 17:05:06 -05:00
Zach Nelson 23aad213fc refactor: Removed redundant FsFile close() calls (#1434)
## Summary

**What is the goal of this PR?** (e.g., Implements the new feature for
file uploading.)

`DESTRUCTOR_CLOSES_FILE=1` is set in platformio.ini, which makes SdFat's
FsBaseFile destructor call close() automatically when a file goes out of
scope.

Three categories of file close calls remain untouched:
1. Close before Storage.remove() on the same path: ScreenshotUtil.cpp
closes the file before deleting it on write error. The remove might fail
if the file is still open.
2. Close before reopening the same variable: Epub.cpp writes a temp
NCX/nav file, closes it, then reopens it for reading. The
RecentBooksStore.cpp close before saveToFile() is the same pattern, it
rewrites the same file.
3. Close on member variables: BookMetadataCache.cpp (bookFile,
spineFile, tocFile), Section.cpp (file), XtcParser.cpp (m_file),
ZipFile.cpp (file). These persist beyond any single function scope, so
the destructor timing doesn't match the intended close point.

---

### 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-04-14 16:41:05 -05:00
Zach NelsonandUri Tauber 075ad7d021 fix: Use font metrics for combining mark positioning (#1310)
## Summary

**What is the goal of this PR?** (e.g., Implements the new feature for
file uploading.)

Combining diacritical marks (U+0300–U+036F) were positioned using a
heuristic that centered them at the midpoint of the base glyph's
**advance width**. This worked acceptably for Bookerly but produced
visibly off-center marks for Noto Sans due to a fundamental difference
in how the two fonts design their combining mark metrics.

Builds on the work of #1037.

## The problem

The two built-in body fonts encode combining mark `left` offsets with
very different conventions:

| Mark | Bookerly `left` | Noto Sans `left` |
|---|---|---|
| U+0301 (acute) | -2 | -10 |
| U+0300 (grave) | -5 | -15 |
| U+0302 (circumflex) | -5 | -5 |
| U+0323 (dot below) | -2 | -11 |

Noto Sans uses large negative `left` values because its marks are
designed for placement at the post-advance cursor position, with `left`
pulling the bitmap back over the base glyph. Bookerly uses small offsets
because its marks sit closer to the glyph origin. The old `advance/2`
centering split the difference poorly — it happened to land close to
correct for Bookerly but placed Noto Sans marks roughly 6px left of
center on a typical lowercase letter.

There was also a bug in the vertical gap heuristic. It unconditionally
computed a `raiseBy` value to prevent above-baseline marks from
colliding with tall base glyphs, but it applied the same logic to
**below-baseline** marks like cedilla (U+0327), dot below (U+0323), and
ogonek (U+0328). For those marks, the math produced a large positive
raise (e.g., 24px for dot-below on 'a'), launching them above the
x-height instead of keeping them below the baseline.

## The fix

**Horizontal positioning**: Instead of centering at `advance/2`, align
the mark bitmap's visual midpoint directly over the base glyph bitmap's
visual midpoint. This uses the base glyph's actual `left` and `width`
rather than its advance width, producing correct results regardless of
how the font encodes its mark offsets.

**Vertical positioning**: The raise heuristic now checks `markTop -
markHeight > 0` and skips below-baseline marks entirely, leaving them at
their font-designed position.

**Consolidation**: The shared math is extracted into two `constexpr`
helpers (`combiningMark::centerOver` and
`combiningMark::raiseAboveBase`) in `EpdFontData.h`, eliminating the
previously triplicated inline calculations across `drawText`,
`drawTextRotated90CW`, and `getTextBounds`. The `MIN_COMBINING_GAP_PX`
constant is also centralized as `combiningMark::MIN_GAP_PX`.

| Before | After |
| -- | -- |
| <img
src="https://github.com/user-attachments/files/25752257/before-noto.bmp"
width="250" /> | <img
src="https://github.com/user-attachments/files/25752258/after-noto.bmp"
width="250" /> |
| <img
src="https://github.com/user-attachments/files/25752259/before-bookerly.bmp"
width="250" /> | <img
src="https://github.com/user-attachments/files/25752260/after-bookerly.bmp"
width="250" /> |

---

### 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 to analyze
differences between Noto Sans and Bookerly font metrics**_

---------

Co-authored-by: Uri Tauber <142022451+Uri-Tauber@users.noreply.github.com>
2026-04-14 14:35:50 -05:00
Xuan-Son Nguyen 243ae8b408 feat: show crash reason on boot (#1453)
## Summary

If the system reboots from crash, display the reason and tell user to
include `crash_report.txt` file.

To test this, simply add an `assert(false)` somewhere inside the code.


![screenshot-74453.bmp](https://github.com/user-attachments/files/26160373/screenshot-74453.bmp)

---

### 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-04-13 15:23:45 -05:00
zgredexandPatryk Radtke 4e9c7a787f feat: show full path bar in file browser (#1411)
![obraz](https://github.com/user-attachments/assets/f848a381-7c45-4724-912b-56168791bcf3)


## Summary

Adds a full path display at the bottom of the file browser with a
separator line matching the header style. Path uses the small font,
left-truncates to always show the deepest folder when path is too long.
Toggleable via Settings > System > Show Full Path (default 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? _**< PARTIALLY >**_

---------

Co-authored-by: Patryk Radtke <patryk@Patryks-MacBook-Pro.local>
2026-04-13 15:12:49 -05:00
Russell Neches cc23aaa9c2 docs: Update README with firmware flashing instructions (#1654)
Added instructions for flashing firmware using esptool.

## Summary

Instructions for flashing a specific firmware from the terminal using
`esptool.py`.

## Additional Context

Not everyone has Chrome or Chromium installed. An alternative to the
web-based firmware utility shouldn't be hard to find.

---

### AI Usage

Did you use AI tools to help write this code? _**PARTIALLY**_ : `dmesg`
command analog for MacOS
2026-04-13 13:50:31 -05:00
Diana 05f8e6e12d fix: webserver /delete API backward compatibility (#1475)
## Summary

* **What is the goal of this PR?** (e.g., Implements the new feature for
file uploading.)

This fixes #682 by restoring API compatibility for external users, eg
deleting on-device books in bulk via calibre using the crosspoint plugin
works now.

* **What changes are included?**

The `/delete` webserver API now accepts the old `path` argument and maps
it to a JSON array element. It is made an error to provide both
arguments.

## Additional Context

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

I have compiled, flashed, and tested this on master at commit
0245972132. Before this change, calibre
cannot delete books, either individually or in bulk.

After this change, it can do so successfully.

---

### 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-04-13 08:59:07 +03:00
Ian Chasse 405ce0c3c8 feat: Rework "Cover + Custom" sleep screens to show covers only when currently reading (#1256)
## Summary

The cover/custom setting was a bit misleading to me. Usually after you
start reading a book you never see your customs again unless a cover
fails to render for some reason. With this, you can easily show your
custom images by just sleeping from the menus instead of the reader

## 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-04-12 21:03:23 -04:00
CSCMe 9bc5111c77 fix: increase loadable epub size (#1638)
## Summary

* **What is the goal of this PR?** Slightly increase the OOM limit when
loading epubs
* **What changes are included?** Switched vectors for parsing epubs to
deques, allowing use of more memory

## Additional Context

* Increases loadable epub size from 2000+ chapter/ToC entries loadable
to 5000+ chapter/ToC entries
* #1574, but without the complicated stuff

---

### 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**_

### Testing
| Commit | Book | Time |
|----------|------|-------|
| 83cd96bc2f | 1000.epub | ~30 sec |
| 83cd96bc2f | 5000.epub | crash |
| PR | 1000.epub | ~29 sec |
| PR | 5000.epub | ~2 min 20 sec |

=> No actual loading time regressions

[tested_epubs.zip](https://github.com/user-attachments/files/26645243/tested_epubs.zip)
2026-04-12 18:41:28 -05:00
bdeshi 9c11f3e4a2 feat: enable manual screen refresh on power button short press (#1626)
## Summary

Adds an option to allow manual screen refresh on power button short
press.

## Additional Context

there's an option to refresh the screen after a set number of pages. but
sometimes a manual refresh is needed to clear up stale ink pixels. this
works everywhere both in and out of reading mode.

resolves #550 

---

### 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**_, to
understand the project structure.
2026-04-12 22:31:26 +03:00
fa2a3d2539 feat: add OPDS search support & next/prev page navigation (#1462)
## Summary

**What is the goal of this PR?**
Adds OPDS search support, allowing users to search a catalog directly
from the book browser when the server exposes an OpenSearch template.

**What changes are included?**
- `OpdsParser`: parses the OpenSearch template URL from feed-level
`<link rel="search">` elements and exposes it via `getSearchTemplate()`
- `OpdsBookBrowserActivity`: fetches and stores the search template
after each feed load; shows a Search hint on the Left button when a
template is available; launches the existing `KeyboardEntryActivity` for
query input; URL-encodes the query and fetches the result feed
- Absolute search result URLs are handled correctly in `fetchFeed`
(skips prepending the server base URL)
- A `consumeConfirm` guard prevents the Confirm release that submits the
keyboard from immediately triggering a book download on the first
browsing frame after search results load

## Additional Context

- Search is silently unavailable if the server does not advertise an
OpenSearch template — no UI change in that case
- Tested against a Calibre-Web OPDS endpoint which exposes `<link
rel="search" type="application/opensearchdescription+xml">`
- The inline URL encoder in `performSearch` was necessary as
`StringUtils` has no such utility; worth considering extracting to
`StringUtils` in a follow-up
- No new dependencies introduced

---

### 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: kira <rammah@tuta.io>
Co-authored-by: Justin Mitchell <justin@jmitch.com>
2026-04-12 12:24:27 -04:00
Mraulio 5c12f2f01e fix: avoid skipping chapter after screenshot (#1625)
## Summary

* **What is the goal of this PR?** 
Fixes skipping chapter when it's enabled in Settings and you take a
Screenshot.
* **What changes are included?**
A simple return if Power and Down were released before the skipChapter.

## Additional Context

* There is an Issue related #1595.
* Checked that it kept the Power button funcionality for Next page and
Suspend.

---

### 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-04-11 20:28:00 +03:00
Jan IvanovandJan Ivanov 8d6b35b8e7 fix: back navigation from BMPViewer (#1597)
## Summary

This fixes navigating back from the BMP Viewer to the FileBrowser which
was broken when moving to the new ActivityManager

This is fixed by making FileBrowserActivity able to take a full file
path on enter and splitting the basePath and fileName from it and
navigating to the correct place.

fixes:
https://github.com/crosspoint-reader/crosspoint-reader/issues/1553

duplicates:
https://github.com/crosspoint-reader/crosspoint-reader/pull/910 to some
extend but mine has the file cursor at the correct file instead of the
first one in the folder

## 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? _**PARTIALLY**_

---------

Co-authored-by: Jan Ivanov <jan.ivanov@sirma.com>
2026-04-11 20:20:46 +03:00
Justin Mitchell 83cd96bc2f fix: Wild pointer crash in JPEGDEC MCU_SKIP handling (#1627)
Adds a PlatformIO pre-build script to patch JPEGDEC library. When
decoding progressive JPEGs with AC coefficients, MCU_SKIP (-8) causes
array index 0xFFFFF8, creating a wild pointer ~33MB past the sMCUs
array. The patch redirects pMCU to sMCUs[0] when MCU_SKIP is active,
preventing store-access faults while maintaining correct behavior for
JPEG_SCALE_EIGHTH decoding. Devices with larger framebuffers (like the
x3) (792×528 = 52,272 bytes vs 800×480 = 48,000 bytes) have less free
heap, shifting the allocation and changing where the wild pointer lands.

Commit 8628297 guarded the DC coefficient write (pMCU[0]) with if (iMCU
>= 0), which prevents crashes for progressive JPEGs whose first scan is
DC-only (iScanEnd == 0). However, if the first scan includes AC
coefficients (iScanEnd > 0), the AC decode loop still writes through the
wild pointer and crashes.
2026-04-10 21:13:59 +01:00
14ec53a335 feat: Added Slovenian translation (#1551)
Adde

## Summary

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

Adding Slovenian translation

* **What changes are included?**

Just new slovenian.yaml file with translated strings

## Additional Context

I kindly ask to include it in next release.

---

### 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: Andrej Kralj <andrej.kralj@gmail.com>
Co-authored-by: Uri Tauber <142022451+Uri-Tauber@users.noreply.github.com>
2026-04-10 11:00:32 -05:00
Zach Nelson 5ba85290ab refactor: Deduplicated BMP header writing in Xtc (#1439)
## Summary

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

Replaced manual 1-bit BMP header logic in `Xtc::generateCoverBmp()` and
`Xtc::generateThumbBmp()` with calls to the existing `createBmpHeader()`
utility. Added a `BmpRowOrder` enum and param to support the top-down
row order of XTC cover images.

---

### 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-04-10 00:35:43 +03:00
Uri Tauber 104f391a29 fix: two small memory leaks (#1628)
## Summary

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

Fix two issues pointed out by `CodeRabbit` in #1433:

1. Free the output buffer in `readFileToMemory` on early error paths
(prevents memory leaks).
2. Check `out.write()` return value in the STORED path (same as in the
DEFLATED path).

I can confirm both issues were real (not hallucinations). Both fixes are
minimal — no behavior change on success.

---

### AI Usage

Did you use AI tools to help write this code? _**< NO >**_
2026-04-09 22:16:19 +03:00
Zach Nelson b3b43bb373 refactor: RAII scoped open/close for ZipFile (#1433)
## Summary

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

Added `ScopedOpenClose` RAII guard to eliminate repetitive
`wasOpen`/`close()` boilerplate across all ZipFile methods.

---

### 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-04-09 21:28:31 +03:00
CaptainFrito 5349e81723 feat: Display file extensions in File Browser (#1019)
## Summary

![IMG_8136
Medium](https://github.com/user-attachments/assets/e33a8a71-4126-4bdc-8d3a-32132acba712)
![IMG_8137
Medium](https://github.com/user-attachments/assets/2289fbee-71e8-446c-ad28-9dd4a9a21396)
![IMG_8138
Medium](https://github.com/user-attachments/assets/0e912f72-9be1-4708-a909-4dab192d1aea)


## Additional Context

Do we want a setting to toggle this?

---

### AI Usage

Did you use AI tools to help write this code? _**NO**_
2026-04-09 09:54:46 -05:00
Justinian 825ef56ad8 feat: X3 grayscale antialiasing improvements (#1607)
## Summary
Improves text antialiasing quality on the Xteink X3 (SSD1677) display to
bring it closer to X4 rendering quality. Addresses white lines through
letter strokes and ghosting artifacts during page turns and screen
transitions.

## Changes

### Display Driver (open-x4-sdk)
- Dedicated X3 grayscale LUTs with tuned VDL drive strengths for dark
gray (2 time units) and light gray (3 time units), with active GND hold
on non-gray transitions to prevent floating source crosstalk
- Tight scan timing: TP2/TP3 reduced to 1 (total gate-on 7 units vs 17),
minimizing parasitic charge leakage that caused white lines through
letter strokes
- Fast diff BB reinforcement: Added mild VDH reinforcing pulse to
lut_x3_bb_full so black pixels are actively driven during differential
refreshes, clearing gray residue/ghosting
- displayGrayBuffer() updated to use the dedicated gray LUT bank instead
of full refresh LUTs for X3

### Rendering Pipeline
- Re-enabled light gray rendering for X3 text and images, now safe with
dedicated gray LUTs providing proper 4-level gray
- Removed isLightGrayRestricted() gating that was limiting X3 to 3-level
gray
- Runtime display dimensions in DirectPixelWriter and ScreenshotUtil,
replaced hardcoded constants with runtime getters to support X3 792x528
resolution

### Note
The open-x4-sdk submodule references a commit on
juicecultus/community-sdk. A corresponding PR to
open-x4-epaper/community-sdk should be merged first so the submodule ref
resolves on upstream.

## Testing
Tested on physical X3 hardware. White lines through letters
significantly reduced, in-book ghosting improved via BB reinforcement,
antialiasing visually closer to X4 quality.

## AI Disclosure
Yes, AI was used to assist with the development of these changes.

---------
2026-04-08 21:58:59 -04:00
jpirnay ed0811c898 fix: Fix failing very first wifi connection attempt (#1521)
## Summary

* **What is the goal of this PR?** The very first Wifi connection
attempt with saved credentials failed, subsequent attempts succeeded.
This PR fixes the first-attempt-issue.
* **What changes are included?**

## Additional Context
Claude analysis for WifiSelectionActivity

In attemptConnection() ,there's no WiFi.disconnect() before WiFi.begin()
— unlike the scan path earlier which does do a disconnect first.

The root cause on ESP32 is the built-in auto-connect feature: the ESP32
WiFi stack saves credentials to NVS flash and automatically starts
trying to connect on boot before your application code runs. When your
attemptConnection() then calls WiFi.begin(), the stack is already in a
transitional CONNECTING state, and the new begin() call either gets
ignored or collides with the in-progress attempt.

The fix is to add WiFi.disconnect(true) + a short delay in
attemptConnection() before calling WiFi.begin(), and optionally call
WiFi.persistent(false) to stop the ESP32 from auto-connecting on its
own.

---

### 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-04-08 21:26:12 +03:00
nscheung d29b8ee2f9 feat: Adjust Navigation at End of Book (#1425)
## Summary

* **What is the goal of this PR? (e.g., Implements the new feature for
file uploading.)**
Currently, pressing forward at the end of a book loops back to the last
page. This change will instead sends you to the home page instead.

* **What changes are included?**
Applies the change to the three supported format: EPUB, XTC, TXT

## Additional Context

* This is more of a QOL improvement than a new feature. If there's
interest, we could extend this to track a completed state for ebooks.

---

### 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-04-08 21:09:28 +03:00
martin brook b898d53f7b chore: drop JPEGDEC patch in favour of upstream fix (#1465)
## Summary

The progressive JPEG fixes (AC table skip, MCU_SKIP guard) from PR #1136
have been fixed upstream in bitbank2/JPEGDEC@8628297. Pin to that commit
and remove the pre-build patch script.

## Additional Context

Tested on Strange Pictures epub

---

### 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-04-08 09:22:25 +01:00
CSCMe c656673b9a refactor: logPrintf and predefined log level strings (#1546)
## Summary

* More consistent string formatting in logPrintf
* Moved [ and ] from log level strings into new format string
* Behaviour change: Early exit if user string format fails

## Additional Context

* Should not have performance implications, debug monitor etc work as
before
* Clamp may be unnecessary due to information snprintf currently never
being able to exceed max buffer length, but not having it would bug me

---

### 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, to reason
about correctness**_
2026-04-07 22:25:08 -05:00
Zach Nelson 1398aeb1ed fix: Use differential rounding for consistent inter-glyph spacing (#1413)
## Summary

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

A tweak to the fixed-point x-advance and kerning calculations to ensure
that the spacing between any two glyphs is always calculated
consistently.

I noticed that sometimes I'd see common character pairs like "oo" more
than once on a page, and the distance between the two snapped to
different pixels depending on the running accumulated error for the line
of text.

This change uses a differential rounding approach where each glyph's
x-advance plus the kerning relative to the next glyph are combined in
fixed-point precision, then snapped to a pixel to draw the next glyph.
This results in a consistent inter-glyph spacing any time the same two
glyphs show up adjacent to each other, regardless of the accumulated
error across the line.

---

### 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-04-07 22:20:50 -05:00
Justin Mitchell 6cd19f5619 fix: epub images not rendering correctly on x3 (#1572)
Replace hardcoded DISPLAY_WIDTH, DISPLAY_HEIGHT, and DISPLAY_WIDTH_BYTES
constants with runtime values from display object to support multiple
device models (X3 and X4) with different screen dimensions.
2026-04-07 19:16:59 -05:00
Mirus cff3e12a0a fix: Update Ukrainian translations for footnotes (issue 1409) (#1585)
## Summary

* **What is the goal of this PR?** 
Solve the issue
https://github.com/crosspoint-reader/crosspoint-reader/issues/1409
* **What changes are included?**
Updated translation for footnotes

## 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

Did you use AI tools to help write this code? _**NO**_
2026-04-07 09:21:32 -05:00
Andrei Ignatev fa3c7d96a0 fix: correct Russian auto-turn translations (#1566)
## Summary

* **What is the goal of this PR?**
Fix inaccurate Russian UI text for the reader auto-turn feature and make
the affected Russian labels consistent with how adjacent UI strings are
formatted.

* **What changes are included?**
Updated Russian translations in `lib/I18n/translations/russian.yaml`:
- changed `Auto Turn` text from wording that implied screen rotation to
wording that means automatic page turning
- adjusted a few Russian prefix/separator strings to include spacing
where the UI concatenates labels with dynamic values

## Screenshots

| Before| After |
|--------|--------|
| <img width="480" height="800" alt="image"
src="https://github.com/user-attachments/assets/db416dd2-6174-4a46-bd3a-6f52d1cc01cb"
/>| <img width="480" height="800" alt="image"
src="https://github.com/user-attachments/assets/dd9055e0-d4f1-459d-bd40-60fc4970d458"
/>|



---

### 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-04-07 09:21:12 -05:00
Zach Nelson f429f9035c refactor: Use default member initializers for JpegContext and PngContext (#1435)
## Summary

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

Replace verbose constructor initializer lists with in-class default
member initializers in JpegContext and PngContext

---

### 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-04-07 09:13:21 -05:00
Zach Nelson 11984f8fef refactor: Use C++20 'requires' in ActivityResult constructor (#1420)
## Summary

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

Replace SFINAE std::enable_if_t with a C++20 `requires` clause for
clearer constraint expression and better compiler diagnostics on
mismatch.

---

### 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-04-07 08:30:52 -05:00
Zach Nelson 1c13331189 fix: Support hyphenation for EPUBs using ISO 639-2 language codes (#1461)
## Summary

EPUBs that use ISO 639-2 three-letter language codes in their
`dc:language` metadata (e.g. `<dc:language>eng</dc:language>`) got no
hyphenation. The hyphenator registry only matched ISO 639-1 two-letter
codes (`"en"`, `"fr"`, etc.), so `"eng"` produced a null hyphenator and
every word in the book was treated as unhyphenatable.
Added a normalization step in `hyphenatorForLanguage` that maps ISO
639-2 codes (both bibliographic and terminological variants) to their
two-letter equivalents before the registry lookup.

Discovered via *Project Hail Mary* (Random House), which uses
`<dc:language>eng</dc:language>`.

---

### 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-04-07 00:30:24 +01:00
545 changed files with 191362 additions and 178548 deletions
+33
View File
@@ -0,0 +1,33 @@
# CrossPoint Reader: Claude Code skills
Project skills for Claude Code. Claude auto-discovers them and loads one when the
task matches its `description`; you do not invoke them by hand. They encode how
this project wants C/C++ written: the judgment calls and self-review gates that
keep the firmware small, stable, and reviewable.
These are written for capable agents, not beginners. They are principle- and
decision-focused on purpose. They deliberately avoid line-number citations,
which drift; they anchor on durable names (APIs, types, macros, files).
This is separate from `.skills/SKILL.md`, the GitHub coding-agent guide that
mirrors CLAUDE.md. CLAUDE.md stays the always-loaded rule set; these skills are
the applied decision procedures that load on demand and add the judgment layer
CLAUDE.md does not carry.
| Skill | Loads when you are... |
|---|---|
| `heap-discipline` | allocating memory: new/malloc/vector/string, buffers, caches |
| `control-flow-clarity` | writing branching logic, state flags, modes, if/else ladders |
| `hal-and-abstractions` | touching storage, input, display, settings, i18n, rendering |
| `scope-discipline` | adding a feature, activity, lib, setting, or dependency |
| `refactor-for-review` | refactoring, cleaning up, or preparing a change for PR |
Each skill ends with a self-review checklist Claude runs against its own diff
before handing it back. Reviewing a PR? Those checklists double as a fast rubric.
## Maintaining these
Edit the `SKILL.md` under each directory. Keep them tight. Do not restate
CLAUDE.md; add the judgment CLAUDE.md cannot afford to carry. Trigger quality
lives in the `description` field: it must name the situations that should pull
the skill in, in the words a contributor's task would use.
@@ -0,0 +1,56 @@
---
name: control-flow-clarity
description: Branching and state-modeling clarity in C/C++. Use when writing or refactoring logic that branches on discrete values: if/else-if ladders, status flags, mode or state ints, or anything that should be an enum plus an exhaustive switch. Covers enum class over magic ints, exhaustive switch over nested if, early-return guard clauses, table dispatch, and when each is the right call.
---
# Control-Flow Clarity
The goal: a reviewer verifies correctness by reading, not by tracing. Branching
that mirrors the problem's shape is self-evident; branching that encodes it in
ad-hoc ints and nesting forces the reader to reconstruct intent.
## Core moves
- **Model a closed set of states/modes as an `enum class`, not ints or bools.**
A variable kept honest by a comment ("0 = hidden, 1 = showing, 2 = confirm")
is a latent bug. Make it an enum and the comment becomes the type.
- **Dispatch on an enum with an exhaustive `switch`, no `default`.** This
codebase relies on it: omitting `default` lets the compiler flag the
unhandled case when someone adds an enum value. A `default:` that swallows the
unknown case throws that safety away. Add `default` only when "every other
value does nothing" is a deliberate, documented decision.
- **Replace nested `if/else-if` ladders that branch on one discriminant with a
`switch`.** If each branch only maps input to a value, prefer a lookup table
(`static constexpr` array) over both.
- **Prefer early-return guard clauses over nested success bodies.** Handle the
error/empty/skip cases first and return; keep the main path at the left
margin.
## When NOT to switch
- Branches test unrelated conditions, not one discriminant: a guarded `if`
sequence is honest; a switch would be forced.
- Two outcomes on a genuine boolean: keep the `if`.
- The discriminant is an open or unbounded set (arbitrary ints, strings): table
or map, not a switch.
## Enum hygiene
- `enum class` by default for type safety. Plain `enum` only when values must
implicitly convert (e.g. a value that doubles as a UI dropdown index), and
then give it a trailing `_COUNT` sentinel for safe bounds/iteration, matching
the existing settings enums.
- Name the discriminant after what it selects, not its storage:
`Orientation orientation`, not `uint8_t mode`.
- No magic numeric codes for states. If you write a comment mapping numbers to
meanings, you owe an enum.
## Self-review
- [ ] No int/bool standing in for a closed set of modes; it is an `enum class`.
- [ ] Enum dispatch is an exhaustive `switch` with no catch-all `default` (or
the `default` is a documented deliberate choice).
- [ ] No nested if/else-if ladder on a single discriminant that should be a
switch or a table.
- [ ] Error/skip cases are early-return guards; the happy path is not buried.
- [ ] No magic numbers where a named enum or `constexpr` would state the intent.
@@ -0,0 +1,59 @@
---
name: hal-and-abstractions
description: Layering and abstraction discipline for the firmware. Use when touching storage, input, display, settings, i18n, or rendering, or any code that could reach into the SDK. Covers routing through the HAL (HalStorage / HalGPIO / HalDisplay) instead of raw SDK classes, MappedInputManager logical buttons instead of raw GPIO indices, UITheme/GUI for all rendering, the singleton macros, tr() for user-facing text, and where a new abstraction boundary belongs.
---
# HAL and Abstractions
CLAUDE.md lists the HAL classes and the SdFat-concurrency reason they exist.
This is when and how to route through them, and where to draw a new boundary.
## Route through the layer, always
- **SD card I/O:** `Storage` (HalStorage) and `HalFile`. Never `SdFat`,
`FsFile`, `SdSpiCard`, `FsBaseFile`, or `SDCardManager` directly. The HAL
serializes every SD access through one mutex; bypassing it races the SPI state
machine and panics FreeRTOS (CLAUDE.md has the failure mode). This is a
correctness boundary, not a style preference.
- **Display:** `HalDisplay` over `EInkDisplay`. **Input:** `HalGPIO` over
`InputManager`.
- **Rendering:** everything through the `GUI` macro (UITheme) and the renderer's
oriented metrics. No hardcoded fonts, colors, coordinates, or 800/480
literals; ask the renderer for width/height and use the oriented viewable
area.
- **Input in activities:** `MappedInputManager::Button` logical enums
(`Button::Confirm`, `Button::PageForward`, ...). Never raw `HalGPIO::BTN_*`
indices outside `ButtonRemapActivity`. Logical buttons survive user remapping
and orientation; raw indices do not.
- **Shared state:** the singleton macros (`SETTINGS`, `APP_STATE`, `GUI`,
`Storage`, `I18N`), not threaded pointers.
## User-facing text
Every string a user reads goes through `tr(STR_*)`. Add the key to the English
YAML, regenerate with `scripts/gen_i18n.py`, then use the `StrId`. Log lines
(`LOG_*`) stay hardcoded.
## Drawing a new boundary
When you need an SDK capability the HAL does not expose yet, **add the method to
the HAL; do not reach around it.** The new method inherits the mutex, logging,
and error contract the rest of the HAL carries. A one-off direct SDK call in an
activity is exactly the layering violation the mutex discipline cannot tolerate.
Keep abstractions thin. A wrapper that only renames an SDK call without adding
the mutex, logging, or an error contract is dead weight. Add a layer only when
it carries one of those contracts or hides a real implementation choice.
## Self-review
- [ ] No direct SdFat / FsFile / SDCardManager / EInkDisplay / InputManager use
outside `lib/hal`.
- [ ] File access uses `HalFile`; no `.close()` on a local handle
(DESTRUCTOR_CLOSES_FILE); members closed in `onExit`.
- [ ] Input uses `MappedInputManager::Button`, not raw `BTN_*` indices.
- [ ] Rendering goes through GUI/UITheme and oriented metrics; no 800/480 or
hardcoded fonts/coords.
- [ ] User-facing strings use `tr(STR_*)`; new keys added to YAML and
regenerated.
- [ ] Any new SDK capability is exposed as a HAL method, not called inline.
+65
View File
@@ -0,0 +1,65 @@
---
name: heap-discipline
description: Memory allocation discipline for the ESP32-C3 (~380KB RAM, no PSRAM, single 48KB framebuffer). Use whenever writing or reviewing code that allocates: new / malloc / std::vector / std::string, buffers, caches, or anything held across a loop or an activity lifecycle. Covers makeUniqueNoThrow vs raw new/malloc, fragmentation avoidance, reserve-before-push_back, alloc-once-reuse, stack vs heap sizing, and the chunked grayscale buffer pattern.
---
# Heap Discipline (ESP32-C3)
CLAUDE.md states the allocation rules. This is the procedure you run while
writing the code and the gate you run before handing it back.
The constraint that makes every call matter: ~380KB RAM, no PSRAM, one 48KB
framebuffer. **Fragmentation, not total usage, is what kills this device.**
Free-heap can read fine while the largest free block is too small for the next
allocation. Optimize for not leaving holes, not just for using fewer bytes.
## Allocation decision procedure
Ask in order; stop at the first yes.
1. **Stack?** Local, bounded, under ~256 bytes total: plain array/struct. No
heap, no fragmentation. Keep frames lean; the task stack is small.
2. **Compile-time constant?** `static constexpr` lives in flash, costs zero DRAM.
3. **Allocated once and reused for an activity's lifetime?** Allocate in
`onEnter`, hold in a member, release in `onExit`. Never per-frame, never
per-iteration.
4. **Dynamic and fallible?** `makeUniqueNoThrow<T>(...)` /
`makeUniqueNoThrow<T[]>(n)` from `lib/Memory/Memory.h`. Null-check, `LOG_ERR`
with the size, return false. It frees on every exit path.
5. **A C/SDK API takes ownership and frees it itself?** Only then raw
`new (std::nothrow)` / `malloc`, with a comment naming who frees it.
Bare `new` / `new[]` is never correct here: under `-fno-exceptions` it calls
`abort()` on OOM instead of returning null.
## Fragmentation rules
- `std::vector`: `reserve(n)` before any `push_back` loop. Each growth is
alloc-copy-free (three heap ops) and leaves a hole. Unknown n: estimate high.
- No repeated `new`/`delete` or growing containers inside a loop or render path.
Hoist the allocation out of the loop.
- Large contiguous blocks fragment worst. Full-screen-class buffers use the
chunked `storeBwBuffer` / `restoreBwBuffer` path in `GfxRenderer` so they
never demand one contiguous 48KB block. Reuse that path. Do not malloc a
second full-screen buffer.
- `std::string` / Arduino `String`: acceptable on cold paths (file I/O, one-shot
setup). Banned on hot/render paths. Build text with a stack `char[]` +
`snprintf`; if a `std::string` is unavoidable, `reserve` it first.
## Justify every allocation
Per CLAUDE.md's evidence rule: when you add a heap allocation, state in one line
why stack/static/reuse was rejected and the worst-case size. If you cannot name
the size, you cannot budget it, and you should not allocate it.
## Self-review before handoff
- [ ] No bare `new`/`new[]`. Every fallible alloc is `makeUniqueNoThrow`, or a
raw alloc with an explicit owner comment.
- [ ] Every allocation is null-checked with `LOG_ERR` before the error return.
- [ ] No allocation inside a loop or render path that could be hoisted.
- [ ] Every `push_back` loop has a preceding `reserve`.
- [ ] Anything allocated in `onEnter` is released in `onExit`; member `HalFile`
closed there too.
- [ ] No second full-screen buffer; grayscale uses store/restoreBwBuffer.
- [ ] Each new allocation carries a one-line size + why-not-stack/static note.
@@ -0,0 +1,59 @@
---
name: refactor-for-review
description: Producing small, single-concern, reviewable changes. Use when refactoring, cleaning up, restructuring, decomposing, or preparing a change for PR, especially in this multi-contributor AI-assisted codebase that is prone to sprawl diffs. Covers one-concern-per-commit, extracting helpers without widening scope, not bundling unrelated edits, decomposing oversized activities, comment hygiene, and a pre-handoff self-review checklist.
---
# Refactor for Review
This is a multi-contributor, AI-assisted codebase, and the dominant failure mode
is the sprawl diff: a one-line intent that touches thirty files. The goal is a
change a reviewer can verify in one sitting. Cleaner structure that makes the
next change easier is the win, not lines added.
## One concern per change
- A commit/PR does one thing. A bug fix is not also a rename is not also a
reformat. If you spot an unrelated improvement mid-change, leave it or capture
it separately; do not fold it in.
- When the working tree has bundled two changes, separate them with the
copy-affected-files-aside, reset, re-apply one concern, restore the rest
pattern, not by committing the tangle.
- Refactor and behavior change do not ride together. A pure refactor must not
alter behavior; a behavior change should not drag a refactor along. If both
are needed: two commits, refactor first.
## Keep the diff narrow
- Extract a helper to remove real duplication or to name a concept, not to chase
abstraction. Three-plus copies, or a block that needs a name to be understood:
extract. Two similar lines: leave them.
- No "while I'm here" scope creep. A signature or type change that ripples to
many call sites is its own PR: map every caller first, update them in one
topological pass, and land it separately, not as a rider on a feature.
- Match the surrounding code: comment density, naming, idiom. The diff should
read like the file, not like a different author.
## Decompose oversized units
An activity or function that has outgrown one screen of responsibility (multiple
unrelated state machines, or a file far larger than its siblings) is a
decomposition candidate. Extract a cohesive sub-responsibility into its own
unit, as a standalone behavior-preserving refactor, verified on its own, never
mixed into a feature change.
## Comments earn their place
Comments explain why: an invariant, a defense, a past incident, a non-obvious
constraint. Never what the next line already says. Delete narration, phase-marker
comments ("now we loop over..."), and restated function names. If a comment and
the code it sits on say the same thing, the comment is the thing to cut.
## Self-review before handoff
- [ ] The change does exactly one thing; nothing unrelated rode along.
- [ ] Refactor and behavior change are not mixed in one commit.
- [ ] No "while I'm here" creep; rename/signature ripples are split out.
- [ ] Extractions remove real duplication or name a real concept, not
speculative abstraction.
- [ ] New comments say why, not what; no narration or phase markers.
- [ ] A reviewer can understand the diff without running it.
+54
View File
@@ -0,0 +1,54 @@
---
name: scope-discipline
description: Feature-scope discipline for a dedicated e-reader (not a Swiss Army knife). Use when adding a feature, a new activity, a new lib, a setting, or a dependency, or when a request would grow the firmware's surface. Covers the SCOPE.md test, the RAM-cost vs reading-benefit gate, preferring no-code or existing-mechanism solutions, awareness of the existing activity surface, and how to push back on out-of-scope asks.
---
# Scope Discipline
The mission: do one thing exceptionally well, focused reading on constrained
hardware. `SCOPE.md` is the source of truth for what is in and out. Read it
before adding surface. This is the gate to run before writing a new feature.
## The gate
Before adding a feature, activity, lib, setting, or dependency, answer in order:
1. **Is it in `SCOPE.md`?** Explicitly out: interactive apps (notepad,
calculator, games), active connectivity (RSS, news, browser), media/audio
playback. If it is out, say so and stop.
2. **Does it materially improve focused reading?** If the benefit is
"nice to have" or serves a different use case, it is out. This is not a PDA.
3. **What does it cost in RAM and in the largest-free-block budget?** A feature
that adds steady-state RAM or a large transient allocation needs a reading
benefit that clearly outweighs it. Quantify with `firmware_size_history.py`
and `script_profile_mem.sh` rather than guessing.
4. **Can it be done with no new code?** Prefer an existing activity, an existing
setting, or a doc over a new code path. The cheapest feature is the one
already built.
If a request fails the gate, push back with the specific reason and the
`SCOPE.md` basis, and offer the in-scope alternative. Make the call and say why;
do not just hand over a menu.
## Surface awareness
The firmware already carries dozens of activities. Each new one is permanent
RAM, permanent maintenance, and another thing every future refactor must not
break. Default to extending an existing activity or setting before adding a new
screen. New top-level surface needs a real justification, not "it would be
convenient."
## Settings are not free
A new setting is a field to persist, migrate, validate, translate, and render,
plus combinatorial test burden. Add one only when users genuinely need the
choice; otherwise pick a sensible fixed default.
## Self-review
- [ ] Checked against `SCOPE.md`; not on the out-of-scope list.
- [ ] Stated the concrete reading benefit, not a generic "useful."
- [ ] Named the RAM/size cost (measured, not guessed) and why the benefit wins.
- [ ] Checked whether an existing activity/setting/doc already covers it.
- [ ] New setting (if any) is justified by a real user need, not added
"just in case."
+26
View File
@@ -0,0 +1,26 @@
#!/usr/bin/env bash
set -euo pipefail
# Run formatter from repository root regardless of current directory.
REPO_ROOT="$(git rev-parse --show-toplevel)"
cd "${REPO_ROOT}"
# Capture the files already staged for commit so we only re-stage those
# paths after formatting.
staged_files=()
while IFS= read -r -d '' file; do
staged_files+=("${file}")
done < <(git diff --cached --name-only -z --diff-filter=ACMR)
# Intentionally format all currently modified tracked C/C++ files.
# The helper handles no-op cases and exits 0 when nothing matches.
echo "Running clang-format fix before commit..."
./bin/clang-format-fix
# Ensure formatting changes are included in the pending commit without
# staging unrelated tracked modifications from other files in the
# working tree.
if ((${#staged_files[@]})); then
git add -- "${staged_files[@]}"
fi
+1 -2
View File
@@ -1,2 +1 @@
github: [daveallie]
ko_fi: daveallie
custom: ["https://app.royalty.dev/crosspoint-reader/crosspoint-reader"]
+13
View File
@@ -0,0 +1,13 @@
blank_issues_enabled: false
contact_links:
- name: Scope or Roadmap Question (start here if unsure)
url: https://github.com/crosspoint-reader/crosspoint-reader/discussions
about: |
Not sure if your idea fits CrossPoint's scope? Start a Discussion before filing an issue.
See SCOPE.md and ROADMAP.md for what is in, out, and currently paused.
- name: Read the Scope document
url: https://github.com/crosspoint-reader/crosspoint-reader/blob/master/SCOPE.md
about: The authoritative list of what CrossPoint will and will not accept.
- name: Read the Roadmap
url: https://github.com/crosspoint-reader/crosspoint-reader/blob/master/ROADMAP.md
about: Current phase, what is being closed out, and what comes next.
@@ -0,0 +1,80 @@
name: Feature Request
description: Propose a new feature, enhancement, or change to CrossPoint
title: "Short, descriptive title of the request"
labels: ["enhancement", "needs-scope-review"]
body:
- type: markdown
attributes:
value: |
Thanks for taking the time to propose a change to CrossPoint!
**Before you continue, please read [SCOPE.md](../blob/master/SCOPE.md) and [ROADMAP.md](../blob/master/ROADMAP.md).**
CrossPoint is intentionally narrow. Most rejected proposals are rejected for scope reasons that are already
documented. The checklist below exists to save both of us time.
If you are not sure whether your idea fits, open a [Discussion](../../discussions) first instead of filing
this issue.
- type: checkboxes
id: scope-check
attributes:
label: Scope Self-Check (required)
description: Please confirm each of the following. If any are unchecked, your issue will likely be closed.
options:
- label: I have read SCOPE.md and ROADMAP.md.
required: true
- label: This is **not** a new theme or theming change (themes are temporarily closed pending the move to SD-loaded themes).
required: true
- label: This is **not** a new external network connector (sync engine, cloud storage, remote file access, OPDS extensions beyond what exists, or any new "talk to a server" feature).
required: true
- label: This is **not** an interactive app (game, calculator, notepad), writing/authoring tool, RSS/news/browser feature, media playback feature, or PDF rendering.
required: true
- label: The stock firmware does **not** already handle this well.
required: true
- label: No other popular CrossPoint fork already handles this well (or, if one does, I explain below why CrossPoint still needs it).
required: true
- type: textarea
id: problem
attributes:
label: Problem this solves
description: What user-facing problem or reading-experience gap does this address? Be concrete.
placeholder: e.g., "When reading in landscape, paragraph breaks are inconsistent because..."
validations:
required: true
- type: textarea
id: stock-gap
attributes:
label: Why the stock firmware (and other forks) do not already solve this
description: Explain which existing solutions you checked and why they fall short. This is the core scope filter.
validations:
required: true
- type: textarea
id: proposal
attributes:
label: Proposed change
description: A short description of what you would build or change. Focus on user impact; implementation details can come later.
validations:
required: true
- type: textarea
id: tradeoffs
attributes:
label: Memory / flash / complexity cost
description: |
CrossPoint runs on 380KB of RAM. Roughly how much DRAM, flash, or code complexity does this add?
"Don't know" is a valid answer, but please attempt an estimate.
placeholder: e.g., "Adds ~2KB flash for the new font tables, no DRAM impact at runtime."
validations:
required: false
- type: textarea
id: additional-context
attributes:
label: Additional context
description: Anything else relevant (links, screenshots, related discussions).
validations:
required: false
+20 -2
View File
@@ -3,16 +3,34 @@
* **What is the goal of this PR?** (e.g., Implements the new feature for file uploading.)
* **What changes are included?**
## Scope Check
CrossPoint is intentionally narrow. See [SCOPE.md](../blob/master/SCOPE.md) and [ROADMAP.md](../blob/master/ROADMAP.md).
Please confirm:
- [ ] I have read SCOPE.md and ROADMAP.md.
- [ ] This PR is **not** a new built-in theme (themes are temporarily closed pending the move to SD-loaded themes).
- [ ] This PR is **not** a new external network connector (sync engine, cloud storage, remote file access, etc.).
- [ ] This PR is **not** an interactive app, writing tool, RSS/news/browser, media playback, or PDF feature.
- [ ] The stock firmware does not already handle this well, **and** no other popular CrossPoint fork already does
(or, if one does, I explain why CrossPoint still needs it below).
- [ ] If this PR touches `freeink-sdk/`, `lib/hal/`, the bootloader, OTA, or recovery code, I have coordinated with
the relevant maintainer.
**If this PR was opened against the previous (broader) scope and was already in flight under Phase 0, link the
relevant Discussion or issue so reviewers can see the history.**
## Additional Context
* Add any other information that might be helpful for the reviewer (e.g., performance implications, potential risks,
* Add any other information that might be helpful for the reviewer (e.g., performance implications, potential risks,
specific areas to focus on).
* Memory / flash impact, if known.
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing, please be transparent about their usage as it
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 | PARTIALLY | NO >**_
+47 -1
View File
@@ -53,6 +53,13 @@ jobs:
- name: Install PlatformIO Core
run: uv pip install --system -U https://github.com/pioarduino/platformio-core/archive/refs/tags/v6.1.19.zip
- name: Cache PlatformIO packages
uses: actions/cache@v4
with:
path: ~/.platformio
key: pio-${{ runner.os }}-${{ hashFiles('platformio.ini') }}
restore-keys: pio-${{ runner.os }}-
- name: Run cppcheck
run: pio check --fail-on-defect low --fail-on-defect medium --fail-on-defect high
@@ -76,10 +83,21 @@ jobs:
- name: Install PlatformIO Core
run: uv pip install --system -U https://github.com/pioarduino/platformio-core/archive/refs/tags/v6.1.19.zip
- name: Cache PlatformIO packages
uses: actions/cache@v4
with:
path: ~/.platformio
key: pio-${{ runner.os }}-${{ hashFiles('platformio.ini') }}
restore-keys: pio-${{ runner.os }}-
- name: Build CrossPoint
# Both envs in ONE pio invocation: a second `pio run` can wipe the whole
# .pio/build tree (project-checksum mismatch after the S3 toolchain
# installs into the restored cache), deleting the default firmware.bin
# before the artifact upload.
run: |
set -euo pipefail
pio run | tee pio.log
pio run -e default -e sticky | tee pio.log
- name: Extract firmware stats
@@ -102,6 +120,33 @@ jobs:
path: .pio/build/default/firmware.bin
if-no-files-found: error
unit-tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
with:
submodules: recursive
- name: Install build tools
run: |
sudo apt-get update
sudo apt-get install -y cmake ninja-build
- name: Cache googletest source
uses: actions/cache@v4
with:
path: build/test/_deps/googletest-src
key: ${{ runner.os }}-googletest-${{ hashFiles('test/CMakeLists.txt') }}
- name: Configure
run: cmake -S test -B build/test -G Ninja -DCMAKE_BUILD_TYPE=Release
- name: Build
run: cmake --build build/test
- name: Run tests
run: ctest --test-dir build/test --output-on-failure -j
# This job is used as the PR required actions check, allows for changes to other steps in the future without breaking
# PR requirements.
test-status:
@@ -110,6 +155,7 @@ jobs:
- build
- clang-format
- cppcheck
- unit-tests
if: always()
runs-on: ubuntu-latest
steps:
+106
View File
@@ -0,0 +1,106 @@
name: Build & Publish SD Card Fonts
# Fonts change rarely — run manually when font sources or the conversion
# pipeline are updated. Publishes .cpfont files + fonts.json manifest as
# GitHub Release assets on the crosspoint-fonts repo so font releases don't
# clutter the firmware releases page.
#
# Requires a repository secret FONTS_REPO_TOKEN — a fine-grained PAT (or
# classic PAT) with contents:write permission on the target fonts repo.
on:
workflow_dispatch:
env:
FONTS_REPO: crosspoint-reader/crosspoint-fonts
jobs:
build-fonts:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
with:
submodules: recursive
- uses: actions/setup-python@v6
with:
python-version: '3.14'
- name: Install font tools
run: pip install freetype-py fonttools pyyaml
- name: Install system dependencies
run: sudo apt-get update && sudo apt-get install -y libfreetype6-dev
- name: Read version constants
id: versions
run: |
cd lib/EpdFont/scripts
echo "binary=$(python3 -c 'from cpfont_version import CPFONT_VERSION; print(CPFONT_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
run: python3 lib/EpdFont/scripts/build-sd-fonts.py --clean --verbose -j 1
- name: Flatten output for release assets
run: |
mkdir -p dist
find lib/EpdFont/scripts/output -name '*.cpfont' -exec cp {} dist/ \;
- name: Compute release tags
id: tags
env:
GH_TOKEN: ${{ secrets.FONTS_REPO_TOKEN }}
run: |
BASE="sd-fonts-m${{ steps.versions.outputs.metadata }}-b${{ steps.versions.outputs.binary }}"
echo "base=$BASE" >> "$GITHUB_OUTPUT"
# Find the highest existing revision for this m/b pair
LAST=$(gh release list --repo "${{ env.FONTS_REPO }}" \
--json tagName --jq \
'[.[] | select(.tagName | startswith("'"${BASE}-r"'")) | .tagName | split("-r")[1] | tonumber] | max // 0')
NEXT=$((LAST + 1))
echo "revision=$NEXT" >> "$GITHUB_OUTPUT"
echo "versioned=${BASE}-r${NEXT}" >> "$GITHUB_OUTPUT"
- name: Generate manifest
run: |
python3 scripts/generate-font-manifest.py \
--input dist \
--base-url "https://github.com/${{ env.FONTS_REPO }}/releases/download/${{ steps.tags.outputs.base }}/" \
--output dist/fonts.json \
--descriptions-from lib/EpdFont/scripts/sd-fonts.yaml
- name: Publish versioned release to fonts repo
env:
GH_TOKEN: ${{ secrets.FONTS_REPO_TOKEN }}
run: |
VERSIONED="${{ steps.tags.outputs.versioned }}"
TITLE="SD Card Fonts (${VERSIONED#sd-fonts-})"
gh release create "$VERSIONED" dist/* \
--repo "${{ env.FONTS_REPO }}" \
--title "$TITLE" \
--notes "Pre-built \`.cpfont\` font files for CrossPoint Reader.
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."
- name: Update stable tag for device downloads
env:
GH_TOKEN: ${{ secrets.FONTS_REPO_TOKEN }}
run: |
BASE="${{ steps.tags.outputs.base }}"
# Delete the old stable release for this m/b pair (the versioned releases are kept)
gh release delete "$BASE" --repo "${{ env.FONTS_REPO }}" --yes 2>/dev/null || true
gh release create "$BASE" dist/* \
--repo "${{ env.FONTS_REPO }}" \
--title "SD Card Fonts (${BASE#sd-fonts-})" \
--notes "Current font build for manifest v${{ steps.versions.outputs.metadata }}, binary format v${{ steps.versions.outputs.binary }}. Devices with this firmware version download from this release.
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 > Manage fonts** on the device."
+7
View File
@@ -25,6 +25,13 @@ jobs:
- name: Install PlatformIO Core
run: uv pip install --system -U https://github.com/pioarduino/platformio-core/archive/refs/tags/v6.1.19.zip
- name: Cache PlatformIO packages
uses: actions/cache@v4
with:
path: ~/.platformio
key: pio-${{ runner.os }}-${{ hashFiles('platformio.ini') }}
restore-keys: pio-${{ runner.os }}-
- name: Build CrossPoint
run: pio run -e gh_release
+7
View File
@@ -25,6 +25,13 @@ jobs:
- name: Install PlatformIO Core
run: uv pip install --system -U https://github.com/pioarduino/platformio-core/archive/refs/tags/v6.1.19.zip
- name: Cache PlatformIO packages
uses: actions/cache@v4
with:
path: ~/.platformio
key: pio-${{ runner.os }}-${{ hashFiles('platformio.ini') }}
restore-keys: pio-${{ runner.os }}-
- name: Extract env
run: |
echo "SHORT_SHA=${GITHUB_SHA::7}" >> $GITHUB_ENV
+10
View File
@@ -2,6 +2,8 @@
.idea
.DS_Store
.vscode
open-x4-sdk
fs_
lib/EpdFont/fontsrc
lib/I18n/I18nKeys.h
lib/I18n/I18nStrings.h
@@ -15,3 +17,11 @@ build
.history/
/.venv
*.local*
*.cpfont
lib/EpdFont/scripts/downloaded_fonts/
lib/EpdFont/scripts/instanced_fonts/
lib/EpdFont/scripts/output/
# Claude Code: share project skills with the team, keep local agent state
# (worktrees, scheduled-task locks, settings.local, scout CLEANUP.md) out.
.claude/*
!.claude/skills/
+4 -3
View File
@@ -1,3 +1,4 @@
[submodule "open-x4-sdk"]
path = open-x4-sdk
url = https://github.com/open-x4-epaper/community-sdk.git
[submodule "freeink-sdk"]
path = freeink-sdk
url = https://github.com/Free-Ink/freeink-sdk.git
branch = main
+86 -37
View File
@@ -7,7 +7,7 @@ Mission: Provide a lightweight, high-performance reading experience focused on E
* Role: Senior Embedded Systems Engineer (ESP-IDF/Arduino-ESP32 specialized).
* Primary Constraint: 380KB RAM is the hard ceiling. Stability is non-negotiable.
* Evidence-Based Reasoning: Before proposing a change, you MUST cite the specific file path and line numbers that justify the modification.
* Anti-Hallucination: Do not assume the existence of libraries or ESP-IDF functions. If you are unsure of an API's availability for the ESP32-C3 RISC-V target, check the open-x4-sdk or official docs first.
* Anti-Hallucination: Do not assume the existence of libraries or ESP-IDF functions. If you are unsure of an API's availability for the ESP32-C3 RISC-V target, check the freeink-sdk source or the FreeInk SDK docs (https://freeink.org/llms.txt for an LLM-readable index) first.
* No Unfounded Claims: Do not claim performance gains or memory savings without explaining the technical mechanism (e.g., DRAM vs IRAM usage).
* Resource Justification: You must justify any new heap allocation (new, malloc, std::vector) or explain why a stack/static alternative was rejected.
* Verification: After suggesting a fix, instruct the user on how to verify it (e.g., monitoring heap via Serial or checking a specific cache file).
@@ -58,6 +58,7 @@ find src -name "*.cpp" -o -name "*.h" | xargs clang-format -i
6. `constexpr` First: Compile-time constants and lookup tables must be `constexpr`, not just `static const`. This moves computation to compile time, enables dead-branch elimination, and guarantees flash placement. Use `static constexpr` for class-level constants.
7. `std::vector` Pre-allocation: Always call `.reserve(N)` before any `push_back()` loop. Each growth event allocates a new block (2×), copies all elements, then frees the old one — three heap operations that fragment DRAM. When the final size is unknown, estimate conservatively.
8. SPIFFS Write Throttling: Never write a settings file on every user interaction. Guard all writes with a value-change check (`if (newVal == _current) return;`). Progress saves during reading must be debounced — write on activity exit or every N page turns, not on every turn. SPIFFS sectors have a finite erase cycle limit.
9. `new` is not nothrow on ESP32: With `-fno-exceptions`, bare `new` that fails calls `abort()` — it does NOT return `nullptr`. Always use `new (std::nothrow)` and null-check the result, or use `makeUniqueNoThrow<T>()` from `lib/Memory/Memory.h`. Never write bare `new` for any fallible allocation.
---
@@ -104,8 +105,17 @@ These flags in `platformio.ini` fundamentally affect firmware behavior:
-DUSE_UTF8_LONG_NAMES=1 // SD card long filename support
-DMINIZ_NO_ZLIB_COMPATIBLE_NAMES=1 // Avoid zlib name conflicts
-DXML_GE=0 // Disable XML general entities (security)
-DDESTRUCTOR_CLOSES_FILE=1 // FsFile destructor auto-closes (SdFat)
```
**DESTRUCTOR_CLOSES_FILE implications**:
- SdFat's `FsBaseFile` destructor calls `close()` automatically when the object goes out of scope
- **Do NOT add explicit `file.close()` calls** for local `FsFile` variables — the destructor handles it
- Explicit `close()` is still required in these cases:
1. **Close before delete**: Must close before `Storage.remove()` on the same path
2. **Close before reopen**: Must close before reopening the same `FsFile` variable (e.g., write then reopen for read, or rewrite the same path)
3. **Member variables**: `FsFile` members persist beyond any single function scope, so close at the intended release point (e.g., in `onExit()`)
**SINGLE_BUFFER_MODE implications**:
- Only ONE framebuffer exists (not double-buffered)
- Grayscale rendering requires temporary buffer allocation (`renderer.storeBwBuffer()`)
@@ -117,7 +127,7 @@ These flags in `platformio.ini` fundamentally affect firmware behavior:
* lib/hal/: Hardware Abstraction Layer (HalDisplay, HalGPIO, HalStorage)
* lib/I18n/: Internationalization (translations in `translations/*.yaml`, generated string tables)
* src/activities/: UI logic using the Activity Lifecycle (onEnter, loop, onExit)
* open-x4-sdk/: Low-level SDK (EInkDisplay, InputManager, BatteryMonitor, SDCardManager)
* freeink-sdk/: Low-level SDK (EInkDisplay, InputManager, BatteryMonitor, SDCardManager)
* .crosspoint/: SD-based binary cache for EPUB metadata and pre-rendered layout sections
### Hardware Abstraction Layer (HAL)
@@ -142,14 +152,19 @@ These flags in `platformio.ini` fundamentally affect firmware behavior:
#include <HalStorage.h>
// Use Storage singleton (defined via macro)
FsFile file;
HalFile file;
if (Storage.openFileForRead("MODULE", "/path/to/file.bin", file)) {
// Read from file
file.close(); // Explicit close required
// No file.close() needed — DESTRUCTOR_CLOSES_FILE=1 handles it at scope exit
}
```
**Usage**: See example above. Uses `FsFile` (SdFat), NOT Arduino `File`.
**Usage**: Use `HalFile` (the mutex-wrapping handle), NOT raw SdFat `FsFile` or Arduino `File`. Do NOT add `file.close()` for local variables (see DESTRUCTOR_CLOSES_FILE above).
**SdFat is not thread-safe; all SD access MUST go through HalStorage**:
- SdFat's `SdSpiCard` tracks SPI bus state with an unsynchronized `m_spiActive` bool. Two tasks calling SdFat concurrently can confuse that state machine and end with one task calling `SPIClass::endTransaction()` against a paramLock the *other* task is holding. That trips FreeRTOS's `xTaskPriorityDisinherit` assert (`tasks.c:5156, pxTCB == pxCurrentTCBs[0]`) and panics the system. See SdFat issue #518.
- `HalStorage` serializes everything via `storageMutex`. Downstream code uses `HalFile` (declared in `<HalStorage.h>`); every method call (read, write, seek, close) takes the mutex. `HalFile`'s destructor also takes the mutex before letting the underlying SdFat `FsFile` close.
- **Never** call into `SdFat` / `SdSpiCard` / `FsBaseFile` / `SDCardManager` / raw `FsFile` directly — that bypasses the mutex.
---
@@ -167,7 +182,7 @@ if (Storage.openFileForRead("MODULE", "/path/to/file.bin", file)) {
### Memory Safety and RAII
* Smart Pointers: Prefer std::unique_ptr. Avoid std::shared_ptr (unnecessary atomic overhead for a single-core RISC-V).
* RAII: Use destructors for cleanup, but call file.close() or vTaskDelete() explicitly for deterministic resource release.
* RAII: Use destructors for cleanup. Call `vTaskDelete()` explicitly for deterministic task release. Do NOT call `file.close()` on local `FsFile` variables — `DESTRUCTOR_CLOSES_FILE=1` handles it at scope exit (see Critical Build Flags).
### ESP32-C3 Platform Pitfalls
@@ -257,43 +272,78 @@ When a template is necessary, limit instantiations: use explicit template instan
**Rules**: NO exceptions, NO abort(), ALWAYS log before error return
### Acceptable malloc/free Patterns
### Heap Buffer Allocation
**Source**: [src/activities/home/HomeActivity.cpp:166](../src/activities/home/HomeActivity.cpp), [lib/GfxRenderer/GfxRenderer.cpp:439-440](../lib/GfxRenderer/GfxRenderer.cpp)
**Prefer `makeUniqueNoThrow` over `malloc`.** Both are nothrow (return `nullptr` on OOM rather than calling `abort()`), but `malloc` requires a manual `free` on every return path — a common source of leaks. `makeUniqueNoThrow<uint8_t[]>(size)` from `lib/Memory/Memory.h` frees automatically when it goes out of scope.
Despite "prefer stack allocation," malloc is acceptable for:
1. **Large temporary buffers** (> 256 bytes, won't fit on stack)
2. **One-time allocations** during activity initialization
3. **Bitmap rendering buffers** (variable size, used briefly)
**Pattern**:
**Preferred pattern**:
```cpp
// Allocate
auto* buffer = static_cast<uint8_t*>(malloc(bufferSize));
#include <Memory.h>
auto buffer = makeUniqueNoThrow<uint8_t[]>(bufferSize);
if (!buffer) {
LOG_ERR("MODULE", "malloc failed: %d bytes", bufferSize);
return false; // Handle allocation failure
LOG_ERR("MODULE", "OOM: %d bytes", bufferSize);
return false;
}
// Use buffer
processData(buffer, bufferSize);
processData(buffer.get(), bufferSize);
// freed automatically — no manual free needed, no leak on early return
```
// Free immediately after use
free(buffer);
buffer = nullptr;
**`malloc` or `new (std::nothrow)` are still acceptable** when the buffer must be passed to a C API that takes ownership and frees it itself (e.g., certain SDK callbacks). In that case follow the manual pattern:
```cpp
auto* buffer = static_cast<uint8_t*>(malloc(bufferSize)); // or new (std::nothrow) uint8_t[bufferSize]
if (!buffer) {
LOG_ERR("MODULE", "OOM: %d bytes", bufferSize);
return false;
}
sdkApiThatTakesOwnership(buffer, bufferSize); // SDK calls free() / delete[]
```
**Rules**:
- **ALWAYS check for nullptr** after malloc
- **Free immediately** after use (don't hold across multiple operations)
- **Set to nullptr** after free (avoid use-after-free)
- **Document size**: Comment why stack allocation was rejected
- **Prefer `makeUniqueNoThrow`** — automatic cleanup eliminates leak risk on error paths
- **ALWAYS check for nullptr** after any allocation and `LOG_ERR` before returning false
- **Raw allocation only** when a C API takes ownership; document why in a comment
**Examples in codebase**:
- Memory utilities: [Memory.h](../lib/Memory/Memory.h) (`makeUniqueNoThrow`)
- Cover image buffers: [HomeActivity.cpp:166](../src/activities/home/HomeActivity.cpp)
- Text chunk buffers: [TxtReaderActivity.cpp:259](../src/activities/reader/TxtReaderActivity.cpp)
- Bitmap rendering: [GfxRenderer.cpp:439-440](../lib/GfxRenderer/GfxRenderer.cpp)
- OTA update buffer: [OtaUpdater.cpp:40](../src/network/OtaUpdater.cpp)
### Heap Allocation with `new`: Always Use `makeUniqueNoThrow`
**CRITICAL**: With `-fno-exceptions`, bare `new` on OOM calls `abort()` — it does NOT return `nullptr`. Always use `makeUniqueNoThrow` from `lib/Memory/Memory.h`, which wraps `new (std::nothrow)` and returns a `std::unique_ptr` that is null on OOM and automatically frees on scope exit.
**Preferred pattern**:
```cpp
#include <Memory.h>
auto obj = makeUniqueNoThrow<MyClass>(args);
if (!obj) { LOG_ERR("MOD", "OOM: MyClass"); return false; }
auto buf = makeUniqueNoThrow<uint8_t[]>(size);
if (!buf) { LOG_ERR("MOD", "OOM: %d bytes", size); return false; }
// Pass to C APIs via .get(); unique_ptr frees automatically on return
someApi(buf.get(), size);
```
**`new (std::nothrow)` directly is acceptable** when the object must be passed to a C API that takes ownership and calls `delete` itself:
```cpp
auto* obj = new (std::nothrow) MyClass(args);
if (!obj) { LOG_ERR("MOD", "OOM: MyClass"); return false; }
sdkApiThatTakesOwnership(obj); // SDK calls delete
```
**Rules**:
- **Prefer `makeUniqueNoThrow`** — automatic cleanup eliminates leak risk on error paths
- **NEVER use bare `new`** — always `makeUniqueNoThrow` or `new (std::nothrow)`
- **ALWAYS `LOG_ERR` before returning false** on OOM
- **Use `.get()`** to pass the raw pointer to C-style APIs; ownership stays with the `unique_ptr`
- **`new (std::nothrow)` directly only** when a C API takes ownership; document why in a comment
**Examples in codebase**:
- Memory utilities: [Memory.h](../lib/Memory/Memory.h) (`makeUniqueNoThrow`)
---
@@ -376,13 +426,13 @@ void enterNewActivity(Activity* activity) {
- Activity navigation = `delete` old activity + `new` create next activity
- Any memory allocated in `onEnter()` MUST be freed in `onExit()`
- FreeRTOS tasks MUST be deleted in `onExit()` before activity destruction
- File handles MUST be closed in `onExit()`
- Member `FsFile` handles MUST be closed in `onExit()` (local `FsFile` variables auto-close via destructor)
**Activity Pattern**:
```cpp
void onEnter() { Activity::onEnter(); /* alloc: buffer, tasks */ render(); }
void loop() { mappedInput.update(); /* handle input */ }
void onExit() { /* free: vTaskDelete, free buffer, close files */ Activity::onExit(); }
void onExit() { /* free: vTaskDelete, free buffer, close member FsFiles */ Activity::onExit(); }
```
**Critical**: Free resources in reverse order. Delete tasks BEFORE activity destruction.
@@ -405,9 +455,8 @@ void onExit() { /* free: vTaskDelete, free buffer, close files */ Activity::on
**Source**: [src/main.cpp:40-115](../src/main.cpp)
**All fonts are loaded as global static objects** at firmware startup:
- Bookerly: 12, 14, 16, 18pt (4 styles each: regular, bold, italic, bold-italic)
- Noto Serif: 12, 14, 16, 18pt (4 styles each: regular, bold, italic, bold-italic)
- Noto Sans: 12, 14, 16, 18pt (4 styles each)
- OpenDyslexic: 8, 10, 12, 14pt (4 styles each)
- Ubuntu UI fonts: 10, 12pt (2 styles)
**Total**: ~80+ global `EpdFont` and `EpdFontFamily` objects
@@ -847,8 +896,8 @@ rm -rf /path/to/sd/.crosspoint/epub_<hash>/sections/
**Source**: `lib/Epub/Epub/Section.cpp`, `lib/Epub/Epub/BookMetadataCache.cpp`
**Current Versions** (as of docs/file-formats.md):
- `book.bin`: **Version 5** (metadata structure)
- `section.bin`: **Version 12** (layout structure)
- `book.bin`: **Version 7** (metadata structure)
- `section.bin`: **Version 25** (layout structure)
**Version Increment Rules**:
1. **ALWAYS increment version** BEFORE changing binary structure
@@ -858,7 +907,7 @@ rm -rf /path/to/sd/.crosspoint/epub_<hash>/sections/
**Example** (incrementing section format version):
```cpp
// lib/Epub/Epub/Section.cpp
static constexpr uint8_t SECTION_FILE_VERSION = 13; // Was 12, now 13
static constexpr uint8_t SECTION_FILE_VERSION = 26; // Was 25, now 26
// Add new field to structure
struct PageLine {
@@ -869,4 +918,4 @@ struct PageLine {
---
Philosophy: We are building a dedicated e-reader, not a Swiss Army knife. If a feature adds RAM pressure without significantly improving the reading experience, it is Out of Scope.
Philosophy: We are building a dedicated e-reader, not a Swiss Army knife. If a feature adds RAM pressure without significantly improving the reading experience, it is Out of Scope.
+227 -119
View File
@@ -1,104 +1,196 @@
# CrossPoint Reader
Firmware for the **Xteink X4** e-paper display reader (unaffiliated with Xteink).
Built using **PlatformIO** and targeting the **ESP32-C3** microcontroller.
[![Fund contributors](https://img.shields.io/badge/%F0%9F%91%91_Fund_contributors-royalty.dev-BB953A?style=for-the-badge&labelColor=1a1a1a)](https://app.royalty.dev/crosspoint-reader/crosspoint-reader)
CrossPoint Reader is a purpose-built firmware designed to be a drop-in, fully open-source replacement for the official
Xteink firmware. It aims to match or improve upon the standard EPUB reading experience.
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.
![](./docs/images/cover.jpg)
**Now running on:** ESP32C3-based Xteink [X4](https://www.xteink.com/products/xteink-x4) and [X3](https://www.xteink.com/products/xteink-x3).
## Motivation
![CrossPoint Reader running on Xteink device](./docs/images/cover.jpg)
E-paper devices are fantastic for reading, but most commercially available readers are closed systems with limited
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 truely unlock the device's
potential.
> If you're planning to buy an Xteink device, consider purchasing an **X3/X4 Developer Edition** through https://crosspointreader.com. CrossPoint receives a small share of each sale, helping fund development costs.
CrossPoint Reader aims to:
* 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**.
## What can CrossPoint do?
This project is **not affiliated with Xteink**; it's built as a community project.
- **Reader engine**: EPUB 2/3 rendering with embedded-style option, image handling, hyphenation, kerning, chapter navigation, footnotes, bookmarks, dictionary lookups ([StarDict](docs/dictionary.md)), go-to-percent, auto page turn, orientation control, focus reading, KOReader progress sync and more.
## Features & Usage
- **Various formats**: native handling for `.epub`, `.xtc/.xtch`, `.txt`, and `.bmp`.
- [x] EPUB parsing and rendering (EPUB 2 and EPUB 3)
- [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
- **Screenshots.**
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).
- **Custom fonts**: install your favorite fonts on the SD card.
See [the user guide](./USER_GUIDE.md) for instructions on operating CrossPoint, including the
[KOReader Sync quick setup](./USER_GUIDE.md#365-koreader-sync-quick-setup).
- **Tilt page turn (X3 only)**.
For more details about the scope of the project, see the [SCOPE.md](SCOPE.md) document.
- **Library workflow**: folder browser, hidden-file toggle, long-press delete, recent books, SD-cache management.
## Installing
- **Wireless workflows**:
- 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 Wi-Fi), 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
### Web (latest firmware)
- **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.
1. Connect your Xteink X4 to your computer via USB-C and wake/unlock the device
2. Go to https://xteink.dve.al/ and click "Flash CrossPoint firmware"
- **Localization**: 24 UI languages and counting. RTL support.
To revert back to the official firmware, you can flash the latest official firmware from https://xteink.dve.al/, or swap
back to the other partition using the "Swap boot partition" button here https://xteink.dve.al/debug.
### Coming soon:
### Web (specific firmware version)
- More themes.
1. Connect your Xteink X4 to your computer via USB-C
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
- Much more! stay tuned.
To revert back to the official firmware, you can flash the latest official firmware from https://xteink.dve.al/, or swap
back to the other partition using the "Swap boot partition" button here https://xteink.dve.al/debug.
---
## 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**.
## 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):
```bash
pip install esptool
```
2. Download `firmware.bin` from the [releases page](https://github.com/crosspoint-reader/crosspoint-reader/releases).
3. Connect your device via USB-C.
4. Find the device port. On Linux, run `dmesg` after connecting. On macOS:
```bash
log stream --predicate 'subsystem == "com.apple.iokit"' --info
```
5. Flash:
```bash
esptool.py --chip esp32c3 --port /dev/ttyACM0 --baud 921600 write_flash 0x10000 /path/to/firmware.bin
```
Adjust `/dev/ttyACM0` to match your system.
### Manual
See [Development](#development) below.
See [Development quick start](#development-quick-start) below.
## Development
---
## Custom SD-card fonts
Convert your own TTF/OTF files into `.cpfont` files that load from the SD card. No firmware reflash is needed.
1. Go to https://crosspointreader.com/fonts and open the "SD-card font builder" form.
2. Upload up to four styles (regular, bold, italic, bold-italic), set the family name, point sizes, and Unicode range.
3. Download the generated `.cpfont` files.
4. Copy them to your SD card under `/fonts/YourFont/` (or `/.fonts/YourFont/` to hide the folder).
5. Select the font on the device from the font settings.
Conversion runs the firmware repo's `lib/EpdFont/scripts/fontconvert_sdcard.py` script unmodified, so output matches a local host build.
---
## 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)
- [Touch and UI development](./docs/contributing/touch-and-ui.md) - FreeInkUI components for new screens, the touch bridge for existing ones, and build envs for the non-Xteink touch devices
---
## Development quick start
### Prerequisites
* **PlatformIO Core** (`pio`) or **VS Code + PlatformIO IDE**
* Python 3.8+
* USB-C cable for flashing the ESP32-C3
* Xteink X4
- [pioarduino](https://github.com/pioarduino/pioarduino) or VS Code + pioarduino plugin
- Python 3.8+
- `clang-format` 21
- 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
cd crosspoint-reader
# Or, if you've already cloned without --recursive:
# if cloned without --recursive:
git submodule update --init --recursive
```
### Flashing your device
### Nix/NixOS
Connect your Xteink X4 to your computer via USB-C and run the following command.
Nix/NixOS users can enter the development shell with either `nix develop` (flakes) or `nix-shell`:
```sh
```bash
nix develop -f nix
# or
nix-shell nix
```
To flash a connected ESP32-C3 device, enable PlatformIO's udev rules in your NixOS configuration:
```nix
services.udev.packages = with pkgs; [ platformio-core.udev ];
```
After rebuilding the system configuration, reconnect the device or reload udev rules.
### Build / flash / monitor
```bash
pio run --target upload
```
### Contributor pre-PR checks
```bash
./bin/clang-format-fix
pio check -e default
pio run -e default
```
### Debugging
After flashing the new features, its recommended to capture detailed logs from the serial port.
@@ -108,7 +200,9 @@ First, make sure all required Python packages are installed:
```python
python3 -m pip install pyserial colorama matplotlib
```
after that run the script:
After that run the script:
```sh
# For Linux
# This was tested on Debian and should work on most Linux systems.
@@ -117,63 +211,77 @@ python3 scripts/debugging_monitor.py
# For macOS
python3 scripts/debugging_monitor.py /dev/cu.usbmodem2101
```
Minor adjustments may be required for Windows.
## Internals
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.
### Data caching
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:
```
.crosspoint/
├── epub_12471232/ # Each EPUB is cached to a subdirectory named `epub_<hash>`
│ ├── progress.bin # Stores reading progress (chapter, page, etc.)
│ ├── cover.bmp # Book cover image (once generated)
│ ├── book.bin # Book metadata (title, author, spine, table of contents, etc.)
│ └── sections/ # All chapter data is stored in the sections subdirectory
│ ├── 0.bin # Chapter data (screen count, all text layout info, etc.)
│ ├── 1.bin # files are named by their index in the spine
│ └── ...
└── epub_189013891/
```
Deleting the `.crosspoint` directory will clear the entire cache.
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).
## Contributing
Contributions are very welcome!
If you are new to the codebase, start with the [contributing docs](./docs/contributing/README.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 goverance 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**.
## Internals
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
was making CrossPoint.
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.
### Data caching
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:
```text
.crosspoint/
├── epub_<hash>/ # one directory per book, named by content hash
│ ├── progress.bin # reading position (chapter, page, etc.)
│ ├── cover.bmp # generated cover image
│ ├── book.bin # metadata: title, author, spine, TOC
│ ├── css_rules.cache # parsed CSS rule cache
│ ├── img_* # rendered image cache files
│ └── sections/ # per-chapter layout cache
│ ├── 0.bin
│ ├── 1.bin
│ └── ...
├── settings.json # device settings
├── state.json # resume/runtime state
└── recent.json # recent books list
```
Removing `/.crosspoint` clears all cached metadata and forces a full regeneration on next open. Book deletes, overwrites, and moves done through the firmware or web UI clear or re-key matching caches; manual SD-card edits may leave stale cache directories behind.
For more details on the internal file structures, see the [file formats document](./docs/file-formats.md).
---
## Contributing
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.
Everyone here is a volunteer, so please be respectful and patient. For governance and community expectations, see [GOVERNANCE.md](./GOVERNANCE.md).
---
## Community forks
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:
- [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.~~ (Unmaintained)
- [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.
- [t5s3-reader](https://github.com/ShallowGreen123/t5s3-reader) — Crosspoint port for LilyGo T5 ePaper S3 / T5S3 4.7-inch e-paper device.
**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.
+88
View File
@@ -0,0 +1,88 @@
# CrossPoint Reader Roadmap
This roadmap describes how CrossPoint is moving through the tighter scope defined in [SCOPE.md](SCOPE.md). It is
intentionally phased: Phase 0 closed out the commitments already in flight before locking down to the stricter
"fill gaps the stock firmware leaves" delineator.
Phases are sequential. We do not start the next phase until the prior one is wrapped or explicitly carried over.
---
## Phase 0 - Close Out Legacy Scope Items — **COMPLETE**
**Goal:** Land the work that was already in motion under the prior, broader scope so contributors are not left
hanging, and so we enter the stricter phases with a clean slate.
**Landed in Phase 0:**
* **RTL support PRs.** The in-flight right-to-left work was reviewed, iterated, and merged.
* **Dictionary PR.** The offline dictionary lookup work was reviewed and merged.
* **Bookmarks** feature. First-class navigation markers in EPUBs.
* ~~**Transparent sleep screens.**~~ Shelved; not picked back up under the stricter phases.
Phase 0 is closed. The tighter scope in [SCOPE.md](SCOPE.md) is now fully enforced. "But it was on the old roadmap"
is not a valid argument for accepting a PR.
---
## Phase 1 - Consolidation, Footprint, and Multi-Device Support — **IN PROGRESS**
**Goal:** Reduce memory and flash usage, clean up the codebase, and land the SDK / HAL generalization work so
CrossPoint runs cleanly on ESP32-based e-reader hardware beyond Xteink (X3 / X4), including ESP32-S3 class devices.
**Focus areas:**
* DRAM and heap fragmentation reduction across the reader core.
* Flash footprint reduction (dead code, redundant strings, oversized tables).
* Refactors that tighten the HAL / SDK boundary.
* Pluggable per-device SDK layers (display, input, storage, battery) and per-device build configuration without
forking the reader core.
* Documentation for adding a new ESP32 e-reader target.
* E-ink driver refinement (ghosting, partial update behavior).
**Closed during this phase:** new themes built into firmware, new external network connectors (sync engines, cloud
storage, remote file access).
---
## Phase 2 - Languages, Fonts, and Themes
**Goal:** With the codebase smaller and portable, make reading great in every language: multi-language support,
better font support with custom fonts, UI translations, and themes loaded from the SD card instead of consuming
flash.
**Focus areas:**
* Multi-language reading support (underserved languages, complex script support where realistic on ESP32 hardware).
* Better font support and custom fonts.
* UI languages and localization.
* Moving themes off-firmware to SD-loaded assets (see SCOPE.md Section 6).
* **Moving hyphenation files off-firmware.** Hyphenation rules vary per language and the files are large (German
alone is ~200KB). Today these eat flash budget that should be available for the reader core. The plan is to build
a downloader analogous to the existing font downloader and store the dictionaries on SD / SPIFFS, loading on
demand. This unlocks better hyphenation for long-word languages (German, Finnish, Norwegian, etc.) without paying
the flash cost up front.
This phase depends on Phase 1 cleanup landing first; otherwise we generalize a moving target.
---
## Out of Roadmap
The following are explicitly *not* on the roadmap. They may live in other CrossPoint forks; they will not be picked
up here:
* Interactive apps (games, calculators, notepads).
* Writing / authoring tools.
* Active connectivity features (RSS, news, browsers).
* PDF rendering as a first-class format.
See [SCOPE.md](SCOPE.md) for the full rationale.
---
## How This Roadmap Changes
* Phase boundaries are decided by maintainers, not by individual PRs.
* If a phase needs to be extended or an item carried over, that is documented here with a short note.
* Proposals for new phases or reordering should go through a Discussion first.
+79 -37
View File
@@ -1,58 +1,100 @@
# Project Vision & Scope: CrossPoint Reader
The goal of CrossPoint Reader is to create an efficient, open-source reading experience for the Xteink X4. We believe a
dedicated e-reader should do one thing exceptionally well: **facilitate focused reading.**
The goal of CrossPoint Reader is to create an efficient, open-source reading experience for ESP32-based e-reader devices. Xteink hardware (X3, X4) is where the project started and remains a primary target, but CrossPoint is explicitly broadening to support the wider ecosystem of small ESP32 e-ink readers. We believe a dedicated e-reader should do one thing exceptionally well: **facilitate focused reading.**
## 1. Core Mission
To provide a lightweight, high-performance firmware that maximizes the potential of the X4, prioritizing legibility and
usability over "swiss-army-knife" functionality.
To provide a lightweight, high-performance firmware that maximizes the potential of ESP32-based e-reader hardware, prioritizing legibility, performance, and usability over "swiss-army-knife" functionality.
## 2. Scope
CrossPoint is **not** a kitchen-sink firmware, and it is **not** Xteink-only. We want clean, maintainable code that the community can build on, and that runs across the range of ESP32 e-reader devices (ESP32-C3, ESP32-S3, and adjacent variants). Every accepted change should make that goal easier, not harder. Device-specific code should live behind the HAL / SDK boundary so the reader core stays portable.
## 2. Guiding Principle: Fill Gaps the Stock Firmware Leaves
CrossPoint exists to do the things the stock firmware does poorly or not at all. New work is evaluated against that delineator:
* **Does the stock firmware already do this well?** We should hit that bar or surpass it
* **Is another popular CrossPoint fork already solving this well?** If yes, we generally defer to that fork if it's not part of the core reading experience. e.g. stats
* **Does this directly improve the reading experience or the firmware's long-term maintainability?** If no, it is out of scope.
## 3. Current Focus
We are intentionally narrowing scope to consolidate the codebase as we open it up to more ESP32 e-reader devices.
During this period, the priorities are:
* **Memory footprint:** Reducing DRAM usage and heap fragmentation. The ESP32-C3 is the tightest target and sets the ceiling, but the gains benefit every ESP32 variant we run on.
* **Flash footprint:** Trimming binary size to leave room for additional device targets and features.
* **Code cleanup:** Refactoring, removing dead code, tightening abstractions, and improving readability.
* **Reading experience:** EPUB parsing and rendering, typography, hyphenation, line spacing, font handling, and legibility improvements.
### Temporarily Closed Areas
PRs in the following areas will be closed until this notice is lifted. Adding these now makes the cleanup and multi-device work materially harder:
* **New themes.** The existing theming surface is frozen.
* **New external network connectors.** This includes sync engines, cloud storage clients, OPDS extensions beyond what exists, remote file access, and any new "talk to a server" feature. We now have our own CrossPoint KOSync server which gives us a way to sync to 3rd party systems like Hardcover at an API level instead of bloating the firmware. If you're interested in helping here, the sync server is also open source.
If you are unsure whether your idea falls into one of these categories, open a Discussion first.
## 4. Scope
### In-Scope
*These are features that directly improve the primary purpose of the device.*
*Features that directly improve the core reading experience or the firmware's maintainability.*
* **User Experience:** E.g. User-friendly interfaces, and interactions, both inside the reader and navigating the
firmware. This includes things like button mapping, book loading, and book navigation like bookmarks.
* **Document Rendering:** E.g. Support for rendering documents (primarily EPUB) and improvements to the rendering
engine.
* **Format Optimization:** E.g. Efficiently parsing EPUB (CSS/Images) and other documents within the device's
capabilities.
* **Typography & Legibility:** E.g. Custom font support, hyphenation engines, and adjustable line spacing.
* **E-Ink Driver Refinement:** E.g. Reducing full-screen flashes (ghosting management) and improving general rendering.
* **Library Management:** E.g. Simple, intuitive ways to organize and navigate a collection of books.
* **Local Transfer:** E.g. Simple, "pull" based book loading via a basic web-server or public and widely-used standards.
* **Language Support:** E.g. Support for multiple languages both in the reader and in the interfaces.
* **Reference Tools:** E.g. Local dictionary lookup. Providing quick, offline definitions to enhance comprehension
without breaking focus.
* **EPUB Rendering & Optimization:** Improvements to the rendering engine, CSS/image handling, and parsing performance.
* **Typography & Legibility:** Custom font support, hyphenation, line and paragraph spacing, margins.
* **E-Ink Driver Refinement:** Reducing full-screen flashes (ghosting management) and improving general rendering.
* **Reading UX:** Bookmarks, progress tracking, button mapping, page navigation, and other in-reader interactions.
* **Library Management:** Simple, intuitive ways to organize and navigate a local book collection.
* **Memory, Flash, and Code Quality:** Refactors and cleanups that reduce resource use or improve maintainability, even without a user-visible feature.
### Out-of-Scope
*These items are rejected because they compromise the device's stability or mission.*
*Rejected because they compromise the device's stability, maintainability, or core mission.*
* **Interactive Apps:** No Notepads, Calculators, or Games. This is a reader, not a PDA.
* **Active Connectivity:** No RSS readers, News aggregators, or Web browsers. Background Wi-Fi tasks drain the battery
and complicate the single-core CPU's execution.
* **Media Playback:** No Audio players or Audio-books.
* **Complex Annotation:** No typed out notes. These features are better suited for devices with better input
capabilities and more powerful chips.
* **Interactive Apps:** No notepads, calculators, or games. These belong in other forks and are not part of CrossPoint's focus.
* **Writing / Authoring Tools:** No typed notes, journals, or editors. Input hardware and RAM are wrong for this, and other forks already explore this space.
* **Active Connectivity:** No RSS readers, news aggregators, or web browsers. Background Wi-Fi drains the battery and complicates the single-core CPU.
* **PDF Rendering:** PDFs are fixed-layout documents, so rendering them requires displaying pages as images rather than reflowable text, resulting in constant panning and zooming that makes for a poor reading experience on e-ink. Out of scope on the current hardware class.
### In-scope — Technically Unsupported
## 5. Calls to Action
*These features align with CrossPoint's goals but are impractical on the current hardware or produce poor UX.*
These are the areas where contributor help is most valuable right now. If you want to take one of these on, open a Discussion or issue first so we can coordinate.
* **Clock Display:** The ESP32-C3's RTC drifts significantly during deep sleep; making the clock untrustworthy after any sleep cycle. NTP sync could help, but CrossPoint doesn't connect to the internet on every boot.
### Theme System: Move Themes Off-Firmware
* **PDF Rendering:** PDFs are fixed-layout documents, so rendering them requires displaying pages as images rather than reflowable text — resulting in constant panning and zooming that makes for a poor reading experience on e-ink.
We want to abstract themes out of the firmware entirely so they no longer consume flash, and instead load from the SD card. This directly supports the current focus on flash footprint and code cleanup.
## 3. Idea Evaluation
* **Status:** [@itsthisjustin](https://github.com/itsthisjustin) plans to take this on eventually but is very open to someone else claiming it sooner.
* **Why it matters:** Every built-in theme costs flash that we would rather spend on rendering, fonts, or future device support. SD-loaded themes also let users customize without rebuilding firmware. It also leads to SD font loading for better language support in the UI.
* **How to claim:** Comment on the relevant Discussion (or open one) before starting.
While I appreciate the desire to add new and exciting features to CrossPoint Reader, CrossPoint Reader is designed to be
a lightweight, reliable, and performant e-reader. Things which distract or compromise the device's core mission will not
be accepted. As a guiding question, consider if your idea improve the "core reading experience" for the average user,
and, critically, not distract from that reading experience.
### Identifying Other Stock-Firmware Gaps
> **Note to Contributors:** If you are unsure if your idea fits the scope, please open a **Discussion** before you start
> coding!
We want help cataloguing things the stock firmware (and other popular CrossPoint forks) handle poorly or not at all, so future work has a clear target list. Particularly interested in:
* **RTL (right-to-left) text support:** Arabic, Hebrew, Persian, and similar scripts.
* **Languages with poor stock and fork coverage:** Especially those that need shaping, complex layout, or non-Latin font work that nobody is handling well today.
* **Other gaps:** Rendering edge cases, accessibility issues, input quirks, anything stock does badly and existing forks have not fixed.
If you can read or use the device in one of these languages, your feedback (even without code) is genuinely useful. Open a Discussion with concrete examples (screenshots, sample EPUBs, expected vs actual behavior) and we will prioritize from there.
## 6. Funding and Contributor Sustainability
CrossPoint uses [Royalty.dev](https://royalty.dev) (yes, a product built by [@itsthisjustin](https://github.com/itsthisjustin)) to fund contributors. There has been some tension in the community around this, so the intent is being clarified here directly.
**Why we do this:**
* To maintain long-term interest from contributors and maintainers, in direct response to substantial community requests for a way to give back.
* To motivate contributors to invest in the *core* project rather than spinning up competing forks.
* To help pay for new ESP32 devices so we can port CrossPoint to additional hardware.
* To give the project a credible long-term path to sustainability.
**How it works:**
* Funds are distributed automatically to contributors based on impact to the codebase and tenure on the project.
* Over **$600** was raised in the first few hours after opening up funding, which is a signal the demand is real.
* The exact scoring methodology is published at <https://app.royalty.dev/transparency>.
**This is not fixed in stone.** The weighting, eligibility, and distribution rules can be tweaked as we learn what works for this project. If you have concerns or suggestions about how funds are allocated, open a Discussion. The goal is a system that fairly recognizes the people doing the work, not a perfect one on day one.
+363 -69
View File
@@ -5,6 +5,7 @@ Welcome to the **CrossPoint** firmware. This guide outlines the hardware control
- [CrossPoint User Guide](#crosspoint-user-guide)
- [1. Hardware Overview](#1-hardware-overview)
- [Button Layout](#button-layout)
- [Taking a Screenshot](#taking-a-screenshot)
- [2. Power \& Startup](#2-power--startup)
- [Power On / Off](#power-on--off)
- [First Launch](#first-launch)
@@ -14,29 +15,47 @@ Welcome to the **CrossPoint** firmware. This guide outlines the hardware control
- [3.3 Browse Files Screen](#33-browse-files-screen)
- [3.4 Recent Books Screen](#34-recent-books-screen)
- [3.5 File Transfer Screen](#35-file-transfer-screen)
- [3.5.1 Calibre Wireless Transfers](#351-calibre-wireless-transfers)
- [3.5.1 Calibre Wireless Transfers](#351-calibre-wireless-transfers)
- [Installing the Plugin in Calibre](#installing-the-plugin-in-calibre)
- [Configuring the CrossPoint Plugin in Calibre](#configuring-the-crosspoint-plugin-in-calibre)
- [Uploading Books](#uploading-books)
- [Removing a Book](#removing-a-book)
- [3.6 Settings](#36-settings)
- [3.6.1 Display](#361-display)
- [3.6.2 Reader](#362-reader)
- [3.6.3 Controls](#363-controls)
- [3.6.4 System](#364-system)
- [3.6.5 KOReader Sync Quick Setup](#365-koreader-sync-quick-setup)
- [3.6.5 OPDS Servers (Multiple Libraries)](#365-opds-servers-multiple-libraries)
- [3.6.6 Web Settings (Wi-Fi + OPDS)](#366-web-settings-wi-fi--opds)
- [3.6.7 KOReader Sync Quick Setup](#367-koreader-sync-quick-setup)
- [Option A: CrossPoint Sync Server (`sync.crosspointreader.com`, default)](#option-a-crosspoint-sync-server-synccrosspointreadercom-default)
- [Option B: Legacy Public KOReader Server (`sync.koreader.rocks`)](#option-b-legacy-public-koreader-server-synckoreaderrocks)
- [Option C: Self-Hosted Server (Docker Compose)](#option-c-self-hosted-server-docker-compose)
- [Syncing While Reading](#syncing-while-reading)
- [3.7 Sleep Screen](#37-sleep-screen)
- [Cover settings](#cover-settings)
- [Custom images](#custom-images)
- [3.8 Custom Fonts (SD Card)](#38-custom-fonts-sd-card)
- [4. Reading Mode](#4-reading-mode)
- [Page Turning](#page-turning)
- [Chapter Navigation](#chapter-navigation)
- [Auto Page Turn](#auto-page-turn)
- [Tilt Page Turn (X3 only)](#tilt-page-turn-x3-only)
- [Footnote Navigation](#footnote-navigation)
- [System Navigation](#system-navigation)
- [Supported Languages](#supported-languages)
- [5. Chapter Selection Screen](#5-chapter-selection-screen)
- [5. Reader Menu](#5-reader-menu)
- [5.1 Chapter Selection](#51-chapter-selection)
- [5.2 Bookmarks](#52-bookmarks)
- [6. Current Limitations \& Roadmap](#6-current-limitations--roadmap)
- [7. Troubleshooting Issues \& Escaping Bootloop](#7-troubleshooting-issues--escaping-bootloop)
## 1. Hardware Overview
The device utilises the standard buttons on the Xteink X4 (in the same layout as the manufacturer firmware, by default):
### Button Layout
| Location | Buttons |
| --------------- | ---------------------------------------------------- |
| **Bottom Edge** | **Back**, **Confirm**, **Left**, **Right** |
@@ -45,6 +64,7 @@ The device utilises the standard buttons on the Xteink X4 (in the same layout as
Button layout can be customized in the **[Controls Settings](#363-controls)**.
### Taking a Screenshot
When the Power Button and Volume Down button are pressed at the same time, it will take a screenshot and save it in the folder `screenshots/`.
Alternatively, while reading a book, press the **Confirm** button to open the reader menu and select **Take screenshot**.
@@ -81,11 +101,12 @@ See [Reading Mode](#4-reading-mode) below for more information.
### 3.3 Browse Files Screen
The Browse Files screen acts as a file and folder browser.
The Browse Files screen acts as a file and folder browser. The full path to the current directory is shown at the top of the screen. File extensions are displayed alongside each filename, and directories are shown with brackets (e.g. `[folder-name]`). Hidden directories (those beginning with `.`) are also visible.
* **Navigate List:** Use **Left** (or **Volume Up**), or **Right** (or **Volume Down**) to move the selection cursor up and down through folders and books. You can also long-press these buttons to scroll a full page up or down.
* **Open Selection:** Press **Confirm** to open a folder or read a selected book.
* **Delete Files:** Hold and release **Confirm** to delete the selected file. You will be given an option to either confirm or cancel deletion. Folder deletion is not supported.
* **Open Selection:** Press **Confirm** to open a folder or start reading a selected book. Selecting a `.bmp` file will open the image viewer.
* **Delete Files or Folders:** Hold and release **Confirm** to delete the selected file or folder. You will be given an option to either confirm or cancel. Multiple files can be selected for deletion in a single operation.
* **Rename or Move:** Files can be renamed or moved to a different folder from within the browse screen.
### 3.4 Recent Books Screen
@@ -93,24 +114,62 @@ The Recent Books screen lists the most recently opened books in a chronological
### 3.5 File Transfer Screen
The File Transfer screen allows you to upload new e-books to the device. When you enter the screen, you'll be prompted with a WiFi selection dialog and then your X4 will start hosting a web server.
The File Transfer screen allows you to upload and manage files on the device. When you enter the screen, choose **Join a Network**, **Calibre Wireless**, or **Create Hotspot**. The reader then starts the web server for the selected mode.
See the [webserver docs](./docs/webserver.md) for more information on how to connect to the web server and upload files.
See the [web server docs](./docs/webserver.md) for more information on how to connect to the web server and upload files.
The web interface also supports **WebDAV**, allowing you to mount the device as a network drive and manage files directly from your computer's file manager.
Download links for files already on the device are available in the web interface, so you can retrieve books or screenshots over Wi-Fi without connecting a cable.
A **Wi-Fi signal strength indicator** (dBm) is displayed on-screen during joined-network web server sessions.
> [!TIP]
> Advanced users can also manage files programmatically or via the command line using `curl`. See the [webserver docs](./docs/webserver.md) for details.
> Advanced users can also manage files programmatically or via the command line using `curl`. See the [web server docs](./docs/webserver.md) for details.
> [!TIP]
> If your EPUBs have compatibility issues, you can run the built-in **EPUB Optimizer** directly from the device to clean up and reprocess books for better rendering.
### 3.5.1 Calibre Wireless Transfers
CrossPoint supports sending books from Calibre using the CrossPoint Reader device plugin.
1. Install the plugin in Calibre:
- Head to https://github.com/crosspoint-reader/calibre-plugins/releases to download the latest version of the crosspoint_reader plugin.
- Download the zip file.
- Open Calibre → Preferences → Plugins → Load plugin from file → Select the zip file.
2. On the device: File Transfer → Connect to Calibre → Join a network.
3. Make sure your computer is on the same WiFi network.
4. In Calibre, click "Send to device" to transfer books.
#### Installing the Plugin in Calibre
If you don't already have the plugin installed:
1. Head to https://github.com/crosspoint-reader/calibre-plugins/releases to download the latest version of the crosspoint_reader plugin.
2. Download the zip file.
3. Open Calibre → Preferences → Plugins → Load plugin from file → Select the zip file.
4. Restart Calibre.
#### Configuring the CrossPoint Plugin in Calibre
1. In Calibre select Preferences.
2. In the Preferences dialog select Plugins.
3. In Plugins search for "crosspoint".
4. Click on "Customize plugin".
5. Update the value for "Host" to match the IP for your device.
6. Leave the other settings as they are.
7. [optional] Modify the "Upload path" to point to a subfolder other than the root "/" folder. Enter this as a path relative to the root folder. Example: `/mybooks`
8. Restart Calibre.
<img width="420" height="385" alt="Image" src="https://github.com/user-attachments/assets/01fc7e33-a9a7-48ba-9e26-2e68d1f9daec" />
#### Uploading Books
To upload a book using the CrossPoint plugin in Calibre:
1. On the device: File Transfer -> Calibre Wireless, then join a network.
2. Select one or more books.
3. Right-click on that selection.
4. Select "Send to Device" > "Send to main memory"
The CrossPoint plugin will connect to your device, create a folder for the book's author in the root folder (or the folder you configured for the plugin), then copy the book into that folder.
<img width="783" height="310" alt="Image" src="https://github.com/user-attachments/assets/741b0909-2e1d-4f16-8af0-2c43fbda5ce6" />
#### Removing a Book
Books cannot be removed from your device through Calibre. Use the web interface instead.
### 3.6 Settings
@@ -119,94 +178,224 @@ The Settings screen allows you to configure the device's behavior. There are a f
#### 3.6.1 Display
- **Sleep Screen**: Which sleep screen to display when the device sleeps:
- "Dark" (default) - The default dark Crosspoint logo sleep screen
- "Light" - The same default sleep screen, on a white background
- "Custom" - Custom images from the SD card; see [Sleep Screen](#37-sleep-screen) below for more information
- "Cover" - The book cover image (Note: this is experimental and may not work as expected)
- "None" - A blank screen
- "Cover + Custom" - The book cover image, falls back to "Custom" behavior
- "Cover + Custom" - The book cover image while actively reading, falls back to "Custom" behavior otherwise
- "Quick resume" - The text of the last page read will be displayed on the sleep screen and a moon icon is shown on the edge of the screen. Waking up the device will return to the same page of the opened book. This is useful for quickly resuming reading without waiting for the device to fully wake up and load the book.
- **Sleep Screen Cover Mode**: How to display the book cover when "Cover" sleep screen is selected:
- "Fit" (default) - Scale the image down to fit centered on the screen, padding with white borders as necessary
- "Crop" - Scale the image down and crop as necessary to try to fill the screen (Note: this is experimental and may not work as expected)
- **Sleep Screen Cover Filter**: What filter will be applied to the book cover when "Cover" sleep screen is selected:
- "None" (default) - The cover image will be converted to a grayscale image and displayed as it is
- "Contrast" - The image will be displayed as a black & white image without grayscale conversion
- "Inverted" - The image will be inverted as in white & black and will be displayed without grayscale conversion
- **Quick Resume on Timeout**: Whether to enable the "Quick Resume" sleep screen when the device goes to sleep due to inactivity (System > Time to Sleep). This is useful for quickly resuming reading without waiting for the device to fully wake up and load the book. This overwrites the Sleep Screen Cover Mode when enabled.
- **Status Bar**: Configure the status bar displayed while reading:
- "None" - No status bar
- "No Progress" - Show status bar without reading progress
- "Full w/ Percentage" - Show status bar with book progress (as percentage)
- "Full w/ Book Bar" - Show status bar with book progress (as bar)
- "Book Bar Only" - Show book progress (as bar)
- "Full w/ Chapter Bar" - Show status bar with chapter progress (as bar)
- **Hide Battery %**: Configure where to suppress the battery percentage display in the status bar; the battery icon will still be shown:
- "Never" (default) - Always show battery percentage
- "In Reader" - Show battery percentage everywhere except in reading mode
- "Always" - Always hide battery percentage
- **Refresh Frequency**: Set how often the screen does a full refresh while reading to reduce ghosting; options are every 1, 5, 10, 15, or 30 pages.
- **UI Theme**: Set which UI theme to use:
- "Classic" - The original Crosspoint theme
- "Lyra" - The new theme for Crosspoint featuring rounded elements and menu icons
- "Lyra Extended" - Lyra, but displays 3 books instead of 1 on the **[Home Screen](#31-home-screen)**
- "RoundedRaff" - A rounded theme with additional visual styling
- **Sunlight Fading Fix**: Configure whether to enable a software-fix for the issue where white X4 models may fade when used in direct sunlight:
- "OFF" (default) - Disable the fix
- "ON" - Enable the fix
> [!NOTE]
> A battery charging indicator is shown on the battery icon whenever the device is actively charging.
#### 3.6.2 Reader
- **Reader Font Family**: Choose the font used for reading:
- "Bookerly" (default) - Amazon's reading font
- "Noto Serif" (default) - Google's serif font
- "Noto Sans" - Google's sans-serif font
- "Open Dyslexic" - Font designed for readers with dyslexia
- **Reader Font Size**: Adjust the text size for reading; options are "Small", "Medium" (default), "Large", or "X Large".
- **Reader Line Spacing**: Adjust the spacing between lines; options are "Tight", "Normal" (default), or "Wide".
- **Reader Screen Margin**: Controls the screen margins in Reading Mode between 5 and 40 pixels in 5-pixel increments.
- **Reader Paragraph Alignment**: Set the alignment of paragraphs; options are "Justified" (default), "Left", "Center", or "Right".
- **Embedded Style**: Whether to use the EPUB file's embedded HTML and CSS stylisation and formatting; options are "ON" or "OFF".
- **Hyphenation**: Whether to hyphenate text in Reading Mode; options are "ON" or "OFF".
- **Reading Orientation**: Set the screen orientation for reading EPUB files:
- "Portrait" (default) - Standard portrait orientation
- "Landscape CW" - Landscape, rotated clockwise
- "Inverted" - Portrait, upside down
- "Landscape CCW" - Landscape, rotated counter-clockwise
- **Extra Paragraph Spacing**: Set how to handle paragraph breaks:
- "ON" - Vertical space will be added between paragraphs in Reading Mode
- "OFF" - Paragraphs will not have vertical space added, but will have first-line indentation
- **Dictionary**: Select the StarDict dictionary used for word lookups while reading, or "None" to disable lookups. *(Only shown when at least one dictionary folder exists under `/dictionaries/` on the SD card — see [docs/dictionary.md](docs/dictionary.md) for setup and usage.)*
- **Text Anti-Aliasing**: Whether to show smooth grey edges (anti-aliasing) on text in reading mode. Note this slows down page turns slightly.
- **Images**: Whether to display embedded images (JPG/PNG) found in EPUB files; options are "ON" (default) or "OFF".
- **Focus Reading**: Bolds the first part of each word to create visual fixation points, similar to Bionic Reading. This can help improve reading speed and focus; options are "ON" or "OFF" (default).
#### 3.6.3 Controls
- **Remap Front Buttons**: A menu for customising the function of each bottom edge button.
- **Side Button Layout (reader)**: Swap the order of the up and down volume buttons from "Prev/Next" (default) to "Next/Prev". This change is only in effect when reading.
- **Side Button Layout (reader)**: Swap the order of the up and down volume buttons from "Prev/Next" (default) to "Next/Prev". You can also disable them entirely. This change is only in effect when reading.
- **Long-press Chapter Skip**: Set whether long-pressing page turn buttons skips to the next/previous chapter:
- "Chapter Skip" (default) - Long-pressing skips to next/previous chapter
- "Page Scroll" - Long-pressing scrolls a page up/down
- **Long-press Menu**: Selects the function bound to holding the menu button (Confirm) while reading an EPUB. **Cycles through the available functions** each time the setting is selected — additional functions may be added in future releases, so this is not a binary on/off toggle. A short press of Confirm always opens the reader menu as normal:
- "Bookmark" (default) - Hold Confirm (~0.4 second) to drop a bookmark at the current page.
- "KOSync" - Hold Confirm (~1 second) to launch KOReader sync directly.
- "Dictionary" - Hold Confirm (~0.4 second) to start dictionary word selection on the current page (see [docs/dictionary.md](docs/dictionary.md)).
- "Disabled" - Long-press is ignored; only short-press opens the reader menu.
- **Short Power Button Click**: Controls the effect of a short click of the power button:
- "Ignore" (default) - Require a long press to turn off the device
- "Sleep" - A short press puts the device into sleep mode
- "Page Turn" - A short press in reading mode turns to the next page; a long press turns the device off
- "Footnotes" - A short press in reading mode opens the footnotes submenu; if only one footnote is present on the page, the referenced page is opened directly. The short press on the power button can be used to select the footnote in the submenu, and to go back to the original page after finish reading the footnote (like the back button).
- "Refresh" - A short press triggers a manual full-screen refresh, useful for clearing ghosting
- **Quick-return from footnotes**: Toggles on and off the quick return functionality from the footnotes. When the functionality it's active, a short press of the power button will act as the back button from the footnotes page.
#### 3.6.4 System
- **Time to Sleep**: Set the duration of inactivity before the device automatically goes to sleep; options are 1, 5, 10 (default), 15 or 30 minutes.
- **Time to Sleep**: Set the duration of inactivity before the device automatically goes to sleep; options are 1, 3, 5, 10 (default), 15 or 30 minutes.
- **Wi-Fi Networks**: Connect to Wi-Fi networks for file transfers and firmware updates.
- **KOReader Sync**: Options for setting up KOReader for syncing book progress. **Smart sync** is the default for new configurations and auto-resolves simple push/pull decisions. Existing credential files retain **Ask every time** when migrated; you can switch Sync Behavior at any time if you prefer manual confirmation.
- **OPDS Servers**: Manage one or more OPDS [(Open Publication Distribution System)](https://en.wikipedia.org/wiki/Open_Publication_Distribution_System) libraries for browsing and downloading books. See [OPDS Servers (Multiple Libraries)](#365-opds-servers-multiple-libraries) below.
- **WiFi Networks**: Connect to WiFi networks for file transfers and firmware updates.
- **KOReader Sync**: Options for setting up KOReader for syncing book progress.
- **OPDS Browser**: Configure OPDS server settings for browsing and downloading books. Set the server URL (for Calibre Content Server, add `/opds` to the end), and optionally configure username and password for servers requiring authentication. Note: Only HTTP Basic authentication is supported. If using Calibre Content Server with authentication enabled, you must set it to use Basic authentication instead of the default Digest authentication.
- **Clear Reading Cache**: Clear the internal SD card cache.
- **Check for updates**: Check for Crosspoint firmware updates over WiFi.
- **Language**: Set the system language (see **[Supported Languages](#supported-languages)** for more information).
#### 3.6.5 KOReader Sync Quick Setup
- **Check for updates**: Check for Crosspoint firmware updates over Wi-Fi. Firmware can also be updated without a USB connection by placing a `firmware.bin` file on the SD card.
- **Language**: Set the UI language. CrossPoint supports 24 languages: English, Spanish, French, German, Czech, Brazilian Portuguese, Russian, Swedish, Romanian, Catalan, Ukrainian, Belarusian, Italian, Polish, Finnish, Danish, Dutch, Turkish, Kazakh, Hungarian, Lithuanian, Slovenian, Valencian, and Hebrew.
- **Manage Fonts**: Browse, download, and manage custom font families installed from the SD card. See [Custom Fonts (SD Card)](#38-custom-fonts-sd-card) for more information.
#### 3.6.5 OPDS Servers (Multiple Libraries)
CrossPoint supports saving multiple OPDS servers and switching between them when browsing catalogs.
1. Open **Settings -> System -> OPDS Servers**.
2. Select **Add Server** to create a new entry, or select an existing server to edit it.
3. Configure these fields:
- **Server Name**: Optional display name (for example, "Home Calibre" or "Public Catalog").
- **OPDS Server URL**: Full catalog root URL (for Calibre Content Server, usually ends with `/opds`).
- **Username / Password**: Optional credentials for authenticated servers.
4. Use **Delete Server** inside a server entry to remove it.
Behavior notes:
- You can store up to 8 OPDS servers.
- OPDS authentication supports HTTP Basic auth. If you use Calibre Content Server with authentication enabled, set it to Basic (not Digest).
You can also manage OPDS servers from the web interface while in File Transfer mode:
1. Connect to the device web UI.
2. Open `http://<device-ip>/settings`.
3. Use the **OPDS Servers** card to add, edit, or delete entries.
For web-based Wi-Fi network management, see [Web Settings (Wi-Fi + OPDS)](#366-web-settings-wi-fi--opds).
#### 3.6.6 Web Settings (Wi-Fi + OPDS)
While in **File Transfer** mode, the web settings page includes management cards for both **Wi-Fi Networks** and **OPDS Servers**.
1. On device: open **File Transfer** and connect through **Join a Network** or **Create Hotspot**.
2. In a browser, open `http://<device-ip>/settings` or `http://crosspoint.local`.
3. In **Wi-Fi Networks**, add, edit, or delete saved network entries (SSID + optional password).
4. In **OPDS Servers**, add, edit, or delete OPDS catalogs.
Behavior notes:
- Passwords are never shown back in the web UI after saving.
- Leaving Password blank while editing keeps the existing saved password unchanged.
- The web UI can save hidden-network SSIDs, but connecting to hidden networks still depends on the device-side Wi-Fi connection flow.
#### 3.6.7 KOReader Sync Quick Setup
CrossPoint can sync reading progress with KOReader-compatible sync servers.
It also interoperates with KOReader apps/devices when they use the same server and credentials.
##### Option A: Free Public Server (`sync.koreader.rocks`)
##### Option A: CrossPoint Sync Server (`sync.crosspointreader.com`, default)
1. Register a user once (only if needed):
When **Sync Server URL** is left empty, CrossPoint uses the free CrossPoint sync server at `https://sync.crosspointreader.com`. It speaks the standard KOReader sync protocol (so KOReader apps can use it too) and additionally stores an exact spine/page position for lossless CrossPoint-to-CrossPoint sync.
1. On each CrossPoint device:
- Go to **Settings -> System -> KOReader Sync**.
- Set **Username** and **Password** (enter the plain password; CrossPoint computes MD5 internally, and use the same values on all devices).
- Leave **Sync Server URL** empty (or set it to `https://sync.crosspointreader.com`).
- On the first device, run **Sign Up** once to create the account directly from the device. On every other device, just run **Authenticate**.
Accounts are per server. Existing `sync.koreader.rocks` credentials do not exist on the CrossPoint server; either sign up again with the same username/password or use Option B to keep using the legacy server.
##### Option B: Legacy Public KOReader Server (`sync.koreader.rocks`)
Use this if you already sync KOReader devices against the official public server.
1. On each CrossPoint device:
- Go to **Settings -> System -> KOReader Sync**.
- Set **Sync Server URL** to `https://sync.koreader.rocks` (required; an empty URL now points at the CrossPoint server instead).
- Set **Username** and **Password** to your existing KOReader Sync credentials.
- Run **Authenticate**.
2. If you do not have an account yet, run **Sign Up** on the device, or register once with curl:
```bash
USERNAME="user"
@@ -219,21 +408,9 @@ curl -i "https://sync.koreader.rocks/users/create" \
--data "{\"username\":\"$USERNAME\",\"password\":\"$PASSWORD_MD5\"}"
```
Already have KOReader Sync credentials? Skip registration; basic sync only requires using the same existing username/password on all devices.
When this returns `HTTP 402` with `{"code":2002,"message":"Username is already registered."}`, pick a different username or use that existing account.
2. On each CrossPoint device:
- Go to **Settings -> System -> KOReader Sync**.
- Set **Username** and **Password** (enter the plain password; CrossPoint computes MD5 internally, and use the same values on all devices).
- Set **Sync Server URL** to `https://sync.koreader.rocks`, or leave it empty (both use the same default KOReader sync server).
- Run **Authenticate**.
3. While reading, press **Confirm** to open the reader menu, then select **Sync Progress**.
- Choose **Apply Remote** to jump to remote progress.
- Choose **Upload Local** to push current progress.
##### Option B: Self-Hosted Server (Docker Compose)
##### Option C: Self-Hosted Server (Docker Compose)
1. Start a sync server:
@@ -273,7 +450,7 @@ curl -H "Accept: application/vnd.koreader.v1+json" "http://<server-ip>:17200/hea
```
3. Register a user once.
CrossPoint authenticates against KOReader Sync (`koreader/kosync`) using an MD5 key, so register using the MD5 of your password:
CrossPoint authenticates against KOReader Sync (`koreader/kosync`) using an MD5 key, so register using the MD5 of your password:
> [!WARNING]
> Sending a reusable MD5-derived password over plain HTTP is insecure.
@@ -295,29 +472,36 @@ curl -i "http://<server-ip>:17200/users/create" \
If this returns `HTTP 402` with `{"code":2002,"message":"Username is already registered."}`, the account already exists.
4. On each CrossPoint device:
- Go to **Settings -> System -> KOReader Sync**.
- Set **Username** and **Password** (enter the plain password; CrossPoint computes MD5 internally, and use the same values on all devices).
- Set **Sync Server URL** to `http://<server-ip>:17200`.
- Run **Authenticate**.
If you use the HTTPS listener, use `https://<server-ip>:7200` (`curl -k` only for self-signed certificate testing).
5. While reading, press **Confirm** to open the reader menu, then select **Sync Progress**.
- Choose **Apply Remote** to jump to remote progress.
- Choose **Upload Local** to push current progress.
##### Syncing While Reading
Once any of the options above is set up, press **Confirm** while reading to open the reader menu, then select **Sync Progress**. Alternatively, set **Settings -> Controls -> Long-press Menu** to **KOSync** and hold Confirm to launch sync directly.
- With **Sync Behavior** set to **Ask every time**, choose **Apply Remote** to jump to remote progress or **Upload Local** to push current progress.
- With **Sync Behavior** set to **Smart sync**, CrossPoint auto-resolves simple cases: upload when no remote progress exists, confirm and leave both unchanged when local and remote progress are already synchronized, upload when local progress is further ahead, or apply remote when remote progress is further ahead.
### 3.7 Sleep Screen
The **Sleep Screen** setting controls what is displayed when the device goes to sleep:
| Mode | Behavior |
|------|----------|
| **Dark** (default) | The CrossPoint logo on a dark background. |
| **Light** | The CrossPoint logo on a white background. |
| **Custom** | A custom image from the SD card (see below). Falls back to **Dark** if no custom image is found. |
| **Cover** | The cover of the currently open book. Falls back to **Dark** if no book is open. |
| **Cover + Custom** | The cover of the currently open book. Falls back to **Custom** behavior if no book is open. |
| **None** | A blank screen. |
| Mode | Behavior |
| ------------------ | ---------------------------------------------------------------------------------------------------------------------------- |
| **Dark** (default) | The CrossPoint logo on a dark background. |
| **Light** | The CrossPoint logo on a white background. |
| **Custom** | A custom image from the SD card (see below). Falls back to **Dark** if no custom image is found. |
| **Cover** | The cover of the currently open book. Falls back to **Dark** if no book is open. |
| **Cover + Custom** | The cover of the currently open book, shown only while actively reading. Falls back to **Custom** behavior when not reading. |
| **None** | A blank screen. |
#### Cover settings
@@ -335,8 +519,29 @@ To use custom sleep images, set the sleep screen mode to **Custom** or **Cover +
> [!TIP]
> For best results:
>
> - Use uncompressed BMP files with 24-bit color depth
> - Use a resolution of 480x800 pixels to match the device's screen resolution.
> - X4: Use a resolution of 480x800 pixels to match the device's screen resolution.
> - X3: Use a resolution of 528x792 pixels to match the device's screen resolution.
> [!TIP]
> You can set an image as the sleep screen cover directly from the BMP image viewer in the **[Browse Files](#33-browse-files-screen)** screen.
---
### 3.8 Custom Fonts (SD Card)
CrossPoint supports loading additional fonts from the SD card, extending beyond the two built-in families (Noto Serif, Noto Sans). Custom fonts can include extended Unicode coverage, enabling CJK (Chinese, Japanese, Korean) and other scripts.
There are three ways to install fonts:
1. **Download from device (recommended):** Go to **Settings -> System -> Manage Fonts**, browse the available font families, and select one to download over Wi-Fi.
2. **Upload via web interface:** While in **File Transfer** mode, open the web UI in a browser and navigate to the **Fonts** tab to upload `.cpfont` files.
3. **Manual SD card copy:** Download font files from the [crosspoint-fonts repository](https://github.com/crosspoint-reader/crosspoint-fonts) and copy them to `/.fonts/` (preferred) or `/fonts/` on your SD card.
Once installed, custom fonts appear in **Settings → Reader → Font Family** alongside the built-in fonts.
See [docs/sd-card-fonts.md](./docs/sd-card-fonts.md) for full installation details and SD card folder structure.
---
@@ -345,6 +550,7 @@ To use custom sleep images, set the sleep screen mode to **Custom** or **Cover +
Once you have opened a book, the button layout changes to facilitate reading.
### Page Turning
| Action | Buttons |
| ----------------- | ------------------------------------ |
| **Previous Page** | Press **Left** _or_ **Volume Up** |
@@ -355,55 +561,143 @@ The role of the volume (side) buttons can be swapped in the **[Controls Settings
If the **Short Power Button Click** setting is set to "Page Turn", you can also turn to the next page by briefly pressing the Power button.
### Chapter Navigation
* **Next Chapter:** Press and **hold** the **Right** (or **Volume Down**) button briefly, then release.
* **Previous Chapter:** Press and **hold** the **Left** (or **Volume Up**) button briefly, then release.
This feature can be disabled in the **[Controls Settings](#363-controls)** to help avoid changing chapters by mistake.
### Auto Page Turn
Auto Page Turn automatically advances pages at a set interval, useful for hands-free reading. This feature can be enabled and configured from the **[Reader Menu](#5-reader-menu)** while reading an EPUB.
### Tilt Page Turn (X3 only)
On the **Xteink X3**, the gyroscope can be used to turn pages by tilting the device. This feature is available in the Controls settings.
### Footnote Navigation
When reading an EPUB that contains footnotes, you can navigate to the footnote text by selecting the footnote reference in the book. From the footnote, you can return to your original reading position.
If the device goes to sleep or you close the book while viewing a footnote, the book reopens to your original reading position, not the footnote.
### System Navigation
* **Return to Home:** Press the **Back** button to close the book and return to the **[Home](#31-home-screen)** screen.
* **Return to Browse Files:** Press and hold the **Back** button to close the book and return to the **[Browse Files](#33-browse-files-screen)** screen.
* **Chapter Menu:** Press **Confirm** to open the **[Table of Contents/Chapter Selection](#5-chapter-selection-screen)** screen.
* **Reader Menu:** Press **Confirm** to open the **[Reader Menu](#5-reader-menu)**, which includes chapter navigation, reading options, and more.
* **Long-press Confirm (configurable):** Holding **Confirm** runs the function chosen by the **Long-press Menu** setting in **[Controls Settings](#363-controls)** — "Bookmark" (default) drops a bookmark, "KOSync" launches KOReader Sync, "Dictionary" starts a word lookup, "Disabled" does nothing. A short press always opens the Reader Menu.
### Supported Languages
CrossPoint renders text using the following Unicode character blocks, enabling support for a wide range of languages:
* **Latin Script (Basic, Supplement, Extended-A):** Covers English, German, French, Spanish, Portuguese, Italian, Dutch, Swedish, Norwegian, Danish, Finnish, Polish, Czech, Hungarian, Romanian, Slovak, Slovenian, Turkish, and others.
* **Cyrillic Script (Standard and Extended):** Covers Russian, Ukrainian, Belarusian, Bulgarian, Serbian, Macedonian, Kazakh, Kyrgyz, Mongolian, and others.
* **Latin Script (Basic, Supplement, Extended-A/B):** Covers English, German, French, Spanish, Portuguese, Italian, Dutch, Swedish, Norwegian, Danish, Finnish, Polish, Czech, Hungarian, Romanian, Slovak, Slovenian, Turkish, Catalan, and others.
* **Cyrillic Script (Standard and Extended):** Covers Russian, Ukrainian, Belarusian, Bulgarian, Serbian, Macedonian, Kazakh, Kyrgyz, Mongolian, and others.
* **Vietnamese:** Supported via extended Latin glyph coverage in the built-in reader fonts.
What is not supported: Chinese, Japanese, Korean, Vietnamese, Hebrew, Arabic, Greek and Farsi.
What is not supported with built-in reader fonts: Chinese, Japanese, Korean, Arabic, Greek, Hebrew, and Farsi. However, **CJK, Hebrew, Greek, and other extended scripts can be enabled by installing custom SD card fonts** — see [Custom Fonts (SD Card)](#38-custom-fonts-sd-card).
---
## 5. Chapter Selection Screen
## 5. Reader Menu
Accessible by pressing **Confirm** while inside a book.
Press **Confirm** while reading to open the Reader Menu. From here you can access reading utilities and navigation options without leaving the book.
1. Use **Left** (or **Volume Up**), or **Right** (or **Volume Down**) to highlight the desired chapter.
2. Press **Confirm** to jump to that chapter.
3. *Alternatively, press **Back** to cancel and return to your current page.*
Available options include:
- **Select Chapter** Open the table of contents to jump to a specific chapter (see [Chapter Selection](#51-chapter-selection) below).
- **Footnotes** Navigate to the footnotes for the current section *(only shown in books that contain footnotes)*.
- **Look Up** Select a word on the current page and show its dictionary definition (see [docs/dictionary.md](docs/dictionary.md)). Requires a dictionary to be selected in **Settings → Reader → Dictionary**.
- **Reading Orientation** Cycle through screen orientations without leaving the reader.
- **Auto Turn (Pages Per Minute)** Cycle through automatic page turn speed options for hands-free reading.
- **Go to %** Jump to a specific position in the book by percentage.
- **Take screenshot** Save a screenshot of the current page to the `screenshots/` folder.
- **Show page as QR** Display a QR code encoding the current reading position.
- **Go Home** Close the book and return to the Home screen.
- **Sync Progress** Push or pull reading progress with a KOReader sync server (see [KOReader Sync Quick Setup](#367-koreader-sync-quick-setup)).
- **Delete Book Cache** Clear the cached layout data for the current book, forcing a re-index on next open.
Press **Back** at any time to close the menu and return to your current page.
### 5.1 Chapter Selection
Accessible by selecting **Chapters** from the Reader Menu.
1. Use **Left** (or **Volume Up**), or **Right** (or **Volume Down**) to highlight the desired chapter.
2. Press **Confirm** to jump to that chapter.
3. *Alternatively, press **Back** to cancel and return to your current page.*
---
### 5.2 Bookmarks
Bookmarks can be created to quickly save and restore your place in a book.
To create a bookmark, hold **Confirm** for about half a second while inside a book. A popup will appear letting you know a bookmark was created. The popup message will automatically disappear in a couple of seconds.
To open bookmarks, press **Confirm** while inside a book. Then navigate to the **Bookmarks** menu. Bookmarks can be opened by navigating to them and pressing **Confirm**, which will redirect you to that place in the book. You can delete bookmarks by holding **Confirm** for about 0.7 seconds, and then pressing **Confirm** again to confirm deletion, or **Back** to cancel.
Bookmarks are stored in the `.crosspoint/bookmarks` folder in the JSON format.
## 6. Current Limitations & Roadmap
Please note that this firmware is currently in active development. The following features are **not yet supported** but are planned for future updates:
* **Images:** Embedded images in e-books will not render.
* **Cover Images:** Large cover images embedded into EPUB require several seconds (~10s for ~2000 pixel tall image) to convert for sleep screen and home screen thumbnail. Consider optimizing the EPUB with e.g. https://github.com/bigbag/epub-to-xtc-converter to speed this up.
* **Unsupported Image Formats:** Most JPG and PNG images in EPUBs render correctly. GIFs and progressive JPEGs are not supported and will fall back to an `[Image]` placeholder.
*
* **Dictionary Lookup:** Inline word lookup is not yet implemented.
---
## 7. Troubleshooting Issues & Escaping Bootloop
If an issue or crash is encountered while using Crosspoint, feel free to raise an issue ticket and attach the serial monitor logs. The logs can be obtained by connecting the device to a computer and starting a serial monitor. Either [Serial Monitor](https://www.serialmonitor.org/) or the following command can be used:
If an issue or crash is encountered while using Crosspoint, feel free to raise an issue ticket and attach the logs.
**Crash reports on SD card:** After a crash, CrossPoint automatically saves a crash report to the SD card (no USB connection needed). Check the root of the SD card for a crash log file and include it with any bug report.
**Serial monitor logs:** For more detailed debugging, connect the device to a computer and run the custom debugging monitor script (requires Python 3 with `pyserial`, `colorama`, and `matplotlib`; install via `pip3 install pyserial colorama matplotlib`):
```
pio device monitor
python3 scripts/debugging_monitor.py
```
The script auto-detects the serial port. You can also specify one explicitly:
```
python3 scripts/debugging_monitor.py /dev/ttyACM0 # Linux
python3 scripts/debugging_monitor.py /dev/tty.usbmodem1 # macOS
python3 scripts/debugging_monitor.py COM7 # Windows
```
**Features:**
- Color-coded log output by category (errors, memory, display, EPUB parsing, etc.)
- Live memory usage graph (free RAM, total RAM, max contiguous allocation) updated every second
- Interactive command prompt — type a command and press Enter to send it to the device
- Screenshot capture — saves the current display to `screenshot.bmp` when triggered by the device
**Options:**
| Option | Description |
| -------------------- | --------------------------------------------------------- |
| `--baud RATE` | Baud rate (default: 115200) |
| `--filter KEYWORD` | Show only lines containing the keyword (case-insensitive) |
| `--suppress KEYWORD` | Hide lines containing the keyword (case-insensitive) |
**Examples:**
```
# Show only memory-related log lines
python3 scripts/debugging_monitor.py --filter MEM
# Hide noisy SD card log lines
python3 scripts/debugging_monitor.py --suppress "[SD]"
```
Press **Ctrl-C** or close the graph window to exit.
If the device is stuck in a bootloop, press and release the Reset button. Then, press and hold on to the configured Back button and the Power Button to boot to the Home Screen.
There can be issues with broken cache or config. In this case, delete the `.crosspoint` directory on your SD card (or consider deleting only `settings.bin`, `state.bin`, or `epub_*` cache directories in the `.crosspoint/` folder).
There can be issues with broken cache or config. In this case, delete the `.crosspoint` directory on your SD card (or consider deleting only `settings.json`, `state.json`, or `epub_*` cache directories in the `.crosspoint/` folder).
+8
View File
@@ -14,6 +14,7 @@ fi
set -euo pipefail
GIT_LS_FILES_FLAGS=""
# -g scopes formatting to tracked files currently modified in git status.
if [[ "${1:-}" == "-g" ]]; then
GIT_LS_FILES_FLAGS="--modified"
fi
@@ -21,6 +22,7 @@ fi
CLANG_FORMAT_VERSION_RAW="$(${CLANG_FORMAT_BIN} --version)"
CLANG_FORMAT_MAJOR="$(printf '%s\n' "${CLANG_FORMAT_VERSION_RAW}" | grep -oE '[0-9]+' | head -n1)"
# Guard against local binaries older than the repo formatting config.
if [[ -z "${CLANG_FORMAT_MAJOR}" || "${CLANG_FORMAT_MAJOR}" -lt 21 ]]; then
echo "Error: ${CLANG_FORMAT_BIN} is too old: ${CLANG_FORMAT_VERSION_RAW}"
echo "This repository's .clang-format requires clang-format 21 or newer."
@@ -37,9 +39,15 @@ fi
# --exclude-standard: ignores files in .gitignore
# Additionally exclude files in 'lib/EpdFont/builtinFonts/' as they are script-generated.
# Also exclude files in 'lib/Epub/Epub/hyphenation/generated/' as they are script-generated.
# Keep the no-match case non-fatal: grep returns 1 when no files match,
# which is expected when there are no modified C/C++ files.
set +o pipefail
git ls-files --exclude-standard ${GIT_LS_FILES_FLAGS} \
| grep -E '\.(c|cpp|h|hpp)$' \
| grep -v -E '^lib/EpdFont/builtinFonts/' \
| grep -v -E '^lib/Epub/Epub/hyphenation/generated/' \
| grep -v -E '^lib/uzlib/' \
| grep -v -E '^lib/miniz/third_party/' \
| xargs -r "${CLANG_FORMAT_BIN}" -style=file -i
# Restore strict pipeline failure handling for the rest of the script.
set -o pipefail
+3 -2
View File
@@ -4,7 +4,7 @@
.DESCRIPTION
Formats all C/C++ source and header files in the repository, excluding
generated, vendored, and build directories (open-x4-sdk, builtinFonts,
generated, vendored, and build directories (freeink-sdk, builtinFonts,
hyphenation tries, uzlib, .pio, *.generated.h).
The clang-format binary path is resolved once and cached in
@@ -92,11 +92,12 @@ function Resolve-ClangFormat {
$clangFormat = Resolve-ClangFormat
$exclude = @(
'open-x4-sdk'
'freeink-sdk'
'lib\EpdFont\builtinFonts'
'lib\Epub\Epub\hyphenation\generated'
'lib\uzlib'
'.pio'
'.venv'
)
function Test-Excluded($fullPath) {
+1
View File
@@ -7,5 +7,6 @@ It is written for software developers who may be new to embedded development.
- [Architecture Overview](./architecture.md)
- [Development Workflow](./development-workflow.md)
- [Testing and Debugging](./testing-debugging.md)
- [Touch and UI Development](./touch-and-ui.md)
If you are new, start with [Getting Started](./getting-started.md).
+40 -23
View File
@@ -8,17 +8,18 @@ At a high level, it is firmware that uses an activity-driven application archite
```mermaid
graph TD
A[Hardware: ESP32-C3 + SD + E-ink + Buttons] --> B[open-x4-sdk HAL]
B --> C[src/main.cpp runtime loop]
C --> D[Activities layer]
C --> E[State and settings]
D --> F[Reader flows]
D --> G[Home/Library/Settings flows]
D --> H[Network/Web server flows]
F --> I[lib/Epub parsing + layout + hyphenation]
I --> J[SD cache in .crosspoint]
D --> K[GfxRenderer]
K --> L[E-ink display buffer]
A[Hardware: ESP32-C3 + SD + E-ink + Buttons] --> B[freeink-sdk]
B --> C[lib/hal wrappers]
C --> D[src/main.cpp runtime loop]
D --> E[Activities layer]
D --> F[State and settings]
E --> G[Reader flows]
E --> H[Home/Library/Settings flows]
E --> I[Network/Web server flows]
G --> J[lib/Epub parsing + layout + hyphenation]
J --> K[SD cache in .crosspoint]
E --> L[GfxRenderer]
L --> M[E-ink display buffer]
```
## Runtime lifecycle
@@ -58,7 +59,7 @@ Top-level activity groups:
- `src/activities/home/`: home and library navigation
- `src/activities/reader/`: EPUB/XTC/TXT reading flows
- `src/activities/settings/`: settings menus and configuration
- `src/activities/network/`: WiFi selection, AP/STA mode, file transfer server
- `src/activities/network/`: Wi-Fi selection, AP/STA mode, file transfer server
- `src/activities/boot_sleep/`: boot and sleep transitions
## Reader and content pipeline
@@ -73,10 +74,11 @@ flowchart LR
C -->|EPUB| D[lib/Epub/Epub]
C -->|XTC| E[lib/Xtc reader]
C -->|TXT| F[lib/Txt reader]
D --> G[Parse OPF/TOC/CSS]
G --> H[Layout pages/sections]
H --> I[Write section and metadata caches]
I --> J[Render current page via GfxRenderer]
D --> G[Parse OPF/TOC and collect CSS refs]
G --> H[Build/load book.bin and css_rules.cache]
H --> I[Layout pages/sections]
I --> J[Write section cache]
J --> K[Render current page via GfxRenderer]
```
Why caching matters:
@@ -98,7 +100,7 @@ flowchart TD
D --> E[Locate container and OPF]
E --> F[Build or load BookMetadataCache]
F --> G[Load TOC and spine]
G --> H[Load or parse CSS rules]
G --> H[Load CSS cache or parse manifest/base-dir CSS]
H --> I[EpubReaderActivity]
I --> J{Section cache exists for current settings?}
@@ -121,7 +123,12 @@ flowchart TD
Notes:
- "section cache exists" depends on cache-busting parameters such as font and layout-related settings
- CSS files are collected from the OPF manifest and, when needed, discovered by
streaming ZIP paths under the OPF content base directory; the firmware avoids
preloading the full ZIP central directory for large books.
- "section cache exists" depends on cache-busting parameters such as font,
viewport size, paragraph alignment, hyphenation, embedded CSS, image rendering,
and Focus Reading settings
- rendering favors reusing precomputed layout data to keep page turns responsive on constrained hardware
- progress/session state is persisted so the reader can reopen at the last position after reboot/sleep
@@ -138,14 +145,18 @@ Typical persisted areas on SD:
/.crosspoint/
epub_<hash>/
book.bin
css_rules.cache
progress.bin
cover.bmp
sections/*.bin
settings.bin
state.bin
img_* cache files
settings.json
state.json
```
For binary cache formats, see `docs/file-formats.md`.
`sections/*.bin` contains rendered pages plus anchor, paragraph, and list-item
lookup tables used for TOC/footnote jumps and KOReader sync refinement. For
binary cache formats, see `docs/file-formats.md`.
## Networking architecture
@@ -153,14 +164,18 @@ Network file transfer is controlled by `src/activities/network/CrossPointWebServ
Modes:
- STA: join existing WiFi network
- STA: join existing Wi-Fi network
- AP: create hotspot
- Calibre Wireless: STA flow specialized for Calibre plugin uploads
Server behavior:
- HTTP server on port 80
- WebSocket upload server on port 81
- WebDAV handler on the HTTP server
- UDP discovery listener for upload clients
- file operations backed by SD storage
- browser APIs for file management, settings, fonts, OPDS servers, and saved Wi-Fi networks
- activity requests faster loop responsiveness while server is running
Endpoint reference: `docs/webserver-endpoints.md`.
@@ -170,6 +185,7 @@ Endpoint reference: `docs/webserver-endpoints.md`.
Some sources are generated and should not be edited manually.
- `scripts/build_html.py` generates `src/network/html/*.generated.h` from HTML files
- `scripts/gen_i18n.py` generates `lib/I18n/I18nKeys.h`, `I18nStrings.h`, and `I18nStrings.cpp`
- `scripts/generate_hyphenation_trie.py` generates hyphenation headers under `lib/Epub/Epub/hyphenation/generated/`
When editing related source assets, regenerate via normal build steps/scripts.
@@ -179,9 +195,10 @@ When editing related source assets, regenerate via normal build steps/scripts.
- `src/`: app orchestration, settings/state, and activity implementations
- `src/network/`: web server and OTA/update networking
- `src/components/`: theming and shared UI components
- `lib/hal/`: hardware abstraction wrappers around freeink-sdk
- `lib/Epub/`: EPUB parser, layout, CSS handling, and hyphenation
- `lib/`: supporting libraries (fonts, text, filesystem helpers, etc.)
- `open-x4-sdk/`: hardware SDK submodule (display, input, storage, battery)
- `freeink-sdk/`: hardware SDK submodule (display, input, storage, battery). Docs: https://freeink.org/docs
- `docs/`: user and technical documentation
## Embedded constraints that shape design
@@ -6,6 +6,7 @@ This page defines the expected local workflow before opening a pull request.
- Fork the repository to your own GitHub account
- Clone your fork locally and add the upstream repository if needed
- Enable repo hooks once per clone: `git config core.hooksPath .githooks && chmod +x .githooks/pre-commit`
- Branch from `master`
- Keep each PR focused on one fix or feature area
+7
View File
@@ -53,6 +53,13 @@ If you already cloned without submodules:
git submodule update --init --recursive
```
Enable the repository-managed Git hooks (required once per clone):
```sh
git config core.hooksPath .githooks
chmod +x .githooks/pre-commit
```
## Build
```sh
+221
View File
@@ -0,0 +1,221 @@
# Touch and UI Development
CrossPoint now runs on touch devices (Seeed Sticky, M5Paper, LilyGo T5) alongside the button-only Xteink X3/X4. Every screen must work with both input styles. There are two supported ways to get there:
1. **New screens: build them with FreeInkUI components.** Touch hit-testing, tap highlighting, long-press, and button focus navigation come with the component; you never hand-roll coordinate math.
2. **Existing screens and in-flight features: use the MappedInputManager touch bridge.** A small set of helpers adds tap/hold/swipe support to hand-rolled rendering without restructuring the activity.
If you are starting a new activity, use FreeInkUI. If you have a feature branch with a hand-rolled screen already working on buttons, use the bridge; do not rewrite mid-flight.
---
## Path 1: New screens with FreeInkUI
FreeInkUI (`freeink-sdk/libs/ui/FreeInkUI`, namespace `freeink::ui`) is an immediate-mode component library. The core idea:
- While a component renders, it registers its tappable areas ("hit rects") into the frame's interaction buffer via `Frame::hit(rect, action, value, inputMask, state)`.
- Each loop you build an `InputSnapshot` from the input manager and route it against that buffer. If a tap (or mapped button press) lands in a registered rect, the routed `ActionEvent` tells you which action fired and with what value.
- Components query `frame.stateFor(action, value)` while painting, so touch-down highlight and focus states render correctly without any per-activity code.
You get touch, hold highlighting, long-press, minimum touch-target sizing, and orientation-aware coordinates for free. The same action IDs fire from physical buttons, so one code path serves both input styles.
### Component inventory
All under `freeink-sdk/libs/ui/FreeInkUI/include/components/`:
| Category | Components |
|---|---|
| Controls | `button`, `checkbox`, `slider`, `progress-bar`, `header` |
| Lists | `list` (virtualized), `table`, `dropdown`, `radio-group`, `setting-row`, `toggle-row`, `stepper-row` |
| Keyboard | `keyboard` (QWERTY/AZERTY/QWERTZ/ES layouts), `key-grid` |
| Overlays | `popup`, `option-dialog`, `context-menu`, `message-panel`, `toast` |
| Bars | `status-bar`, `tab-bar`, `reader-chrome`, `battery-indicator`, `gesture-bar`, `tap-zones` |
| Media | `book-card`, `cover-grid`, `cover-carousel`, `metric-card` |
| Text | `text-field`, `text-area` |
### Integration skeleton
The in-tree reference is [`src/activities/util/KeyboardEntryActivity.cpp`](../../src/activities/util/KeyboardEntryActivity.cpp); it drives the FreeInkUI keyboard component inside a normal Activity. The shape:
```cpp
#include <FreeInkUIGfxRenderer.h>
namespace fui = freeink::ui;
// Render step: draw the component and (re)register its hit rects.
fui::GfxRendererTarget target(renderer); // adapts GfxRenderer to FreeInkUI's DrawTarget
target.setFont(fui::GfxRendererTarget::FONT_BODY, UI_12_FONT_ID);
const fui::DeviceContext device = target.deviceContext(); // orientation, safe area, touch sizing
fui::Frame<48> frame(target, device, fui::InputSnapshot{}, interactions); // 48 = max hit rects
fui::KeyboardProps props;
props.layout = &currentLayout();
props.keyAction = ACTION_KEY; // one action id; the key is the event value
props.inputMask = fui::InputTouch | fui::InputLongPress;
fui::keyboard(frame, kbRect, props); // draws AND registers hit rects
```
```cpp
// Input step (each loop): feed touch state, act on routed events.
int tx, ty, tapX, tapY, hx, hy;
const bool pressedDown = mappedInput.wasScreenTouchDown(tx, ty);
const bool tapped = mappedInput.wasScreenTapped(tapX, tapY);
const bool inContact = mappedInput.isScreenTouchHeld(hx, hy);
const auto result = touchRouter.update(interactions, pressedDown, tx, ty,
tapped, tapX, tapY, inContact, millis());
if (result.event) {
activateValue(result.event.value, result.event.longPress);
requestUpdate();
} else if (result.activeChanged) {
requestUpdate(); // repaint touch-down highlight
}
```
Notes:
- `Frame<N>` is templated on the max number of hit rects; the buffer is stack/static, no heap.
- `fui::TouchHoldRouter` (see `components/keyboard/keyboard.h`) synthesizes long-press while the finger is still down and swallows the eventual release. Use it whenever a component distinguishes tap from long-press.
- For simpler screens without long-press, `FreeInkApp` / `Screen<N>` (`FreeInkApp.h`) is the ergonomic layer: it owns the interaction buffer, offers a top-to-bottom builder (`takeTop`, footer helpers, etc.), and its `render(input)` call routes the snapshot and dispatches registered action callbacks in one step. `snapshotFrom(input, device)` in `FreeInkUIInputManager.h` builds the `InputSnapshot` with orientation-aware touch mapping.
- Everything stays allocation-free and works on the button-only devices unchanged: physical buttons route through the same interaction table via each hit rect's `inputMask`.
---
## Path 2: Adding touch to existing hand-rolled screens
For activities that draw their own rows, menus, and buttons, `MappedInputManager` ([src/MappedInputManager.h](../../src/MappedInputManager.h)) exposes bridge helpers. They all return **logical screen coordinates** (orientation already applied via `GfxRenderer::tapToLogical`); never touch raw normalized panel coordinates or the SDK `InputManager` directly.
| Helper | Use for |
|---|---|
| `wasScreenTapped(x, y)` | A completed tap (press + release), with logical coords |
| `wasScreenTouchDown(x, y)` | Touch-down (held > 90 ms, not yet released): draw selection highlight |
| `isScreenTouchHeld(x, y)` | Live contact position while the finger is down (drag tracking) |
| `wasTapInRect(x, y, w, h)` | One-off hit test on a rectangle (a single button, a banner) |
| `wasListItemTapped(index, count, selected, listTop, listHeight, hasSubtitle)` | Taps on a standard UITheme list; does the row/paging math for you |
| `wasListItemTouchedDown(...)` | Same geometry, touch-down phase (highlight before activate) |
| `rowTouch(row, top, rowStep, rowCount, xStart, xEnd, rowHeight)` | Any custom band of equal-height rows; returns `RowTouch::None/Down/Tap` |
| `colTouch(col, left, colStep, colCount, yStart, yEnd, colWidth)` | Horizontal button bands (dialogs, prompts) |
| `wasSwipe()` | Raw swipe direction if you need one beyond the global gestures |
| `hasTouch()` | True when the device has a touch panel (rarely needed; helpers simply never fire without one) |
### Pattern A: standard themed list
One call per loop; UITheme owns the row geometry ([EpubReaderBookmarksActivity.cpp:123](../../src/activities/reader/EpubReaderBookmarksActivity.cpp)):
```cpp
int tapped = -1;
if (mappedInput.wasListItemTapped(tapped, bookmarks.size(), selectorIndex, listY, listHeight, true)) {
selectorIndex = tapped;
openBookmark();
return;
}
```
### Pattern B: custom rows with hold highlight
For non-theme row layouts, use `rowTouch` and distinguish `Down` (highlight) from `Tap` (activate), as in [EpubReaderFootnotesActivity.cpp](../../src/activities/reader/EpubReaderFootnotesActivity.cpp):
```cpp
int row = -1;
const auto touch = mappedInput.rowTouch(row, listTop, lineHeight, visibleCount,
contentX, contentX + contentWidth);
if (touch != MappedInputManager::RowTouch::None) {
const int touched = scrollOffset + row;
if (touch == MappedInputManager::RowTouch::Down) {
if (selectedIndex != touched) { selectedIndex = touched; requestUpdate(); }
} else { // RowTouch::Tap
selectedIndex = touched;
activateSelection();
}
return;
}
```
The `Down` state exists because e-ink repaints are slow: highlight on touch-down gives immediate feedback, activation happens on release.
### Global gestures: do not reimplement these
Three gestures are handled once, for every screen. Activities must not add their own edge-swipe handling:
| Gesture | Trigger | Where it is handled |
|---|---|---|
| Back | Right-swipe starting in the left 25% of the screen | Folded into `Button::Back`, so the existing `wasPressed(Button::Back)` in your activity already fires |
| Home | Up-swipe starting in the bottom 14% | `ActivityManager::loop()`; pops to Home (activities can override via `handleHomeGesture()`) |
| Menu | Down-swipe starting in the top 14% | Activities that have a menu check `wasMenuGesture()` themselves (the reader does this) |
Because the back gesture arrives as `Button::Back`, most button-era activities gain back-swipe support with zero changes. That is the bar to aim for: bridge helpers should make touch an additive layer over the button flow, not a second input state machine.
### Bridge rules
- Handle touch in `loop()` next to the existing button handling, one helper call per interaction zone, and `return` after consuming an event (mirrors the button pattern).
- Never call the SDK `InputManager` or read GPIO directly; the HAL rule from the main guide applies to touch too.
- Coordinates from the helpers are logical and orientation-correct on all four rotations; test at least Portrait and one Landscape mode before PR.
- Nothing to clean up in `onExit()`; the helpers are stateless from the activity's point of view.
---
## Building and testing on non-Xteink devices
Each MCU family is its own binary: X3/X4 are ESP32-C3, Sticky and LilyGo T5 are ESP32-S3, M5Paper v1.1 is a classic ESP32. The Sticky env ships in `platformio.ini` (`pio run -e sticky`). Envs for other devices go in **`platformio.local.ini`**, a gitignored file that PlatformIO merges over `platformio.ini` (see `extra_configs`). Create it next to `platformio.ini`; personal envs, ports, and debug flags live there and never get committed.
Both envs below extend the repo's `[base]`, so they build against the `freeink-sdk` submodule with all the normal deps and scripts.
### M5Paper v1.1 (classic ESP32, IT8951 panel)
```ini
[env:m5paper_v11]
extends = base
board = esp32dev
board_build.mcu = esp32
board_build.flash_mode = qio
; CP2104 UART bridge: 921600 drops out on macOS after the stub baud switch
upload_speed = 460800
build_unflags =
${base.build_unflags}
; classic ESP32 has UART serial, not USB CDC; Logging.h keys off these
-DARDUINO_USB_MODE=1
-DARDUINO_USB_CDC_ON_BOOT=1
build_flags =
${base.build_flags}
-DFREEINK_DEVICE_M5PAPER=1
; the 63KB 540x960 framebuffer lives in PSRAM (FREEINK_FB_PSRAM auto-on)
-DBOARD_HAS_PSRAM
-DCROSSPOINT_VERSION=\"${crosspoint.version}-m5paper\"
-DENABLE_SERIAL_LOG
-DLOG_LEVEL=2
; touch-first device: hide front-button hint labels
-DCROSSPOINT_SHOW_BUTTON_HINTS=0
; archive-scan-order workaround: without these a full relink drops Wire's i2c symbols
-Wl,-u,i2cInit
-Wl,-u,i2cSlaveInit
```
### LilyGo T5 S3 (ESP32-S3, controller-less panel via LovyanGFX)
```ini
[env:lilygo_t5s3]
extends = base
board = esp32-s3-devkitc1-n16r8
board_build.mcu = esp32s3
build_flags =
${base.build_flags}
-DFREEINK_DEVICE_LILYGO=1
; board injects the parallel-bus pins + PMIC power hooks (BoardT5S3)
-DFREEINK_LGFX_EPD_CONFIG=lilygoT5S3LgfxConfig
-DCROSSPOINT_VERSION=\"${crosspoint.version}-lilygo\"
-DENABLE_SERIAL_LOG
-DLOG_LEVEL=2
-DCROSSPOINT_SHOW_BUTTON_HINTS=0
lib_deps =
${base.lib_deps}
; LgfxEpdConfig for the T5 S3 (pins, PCA9535/TPS65185 power sequence)
BoardT5S3=symlink://freeink-sdk/libs/hardware/BoardT5S3
; LovyanGFX Panel_EPD drives the controller-less ED047TC1 panel
m5stack/M5GFX @ 0.2.20
```
Then `pio run -e m5paper_v11 -t upload` (or `-e lilygo_t5s3`). Gotchas worth knowing:
- **Flash mode matters.** The M5Paper is `qio`; the X4-family standalone envs need `dio`. A wrong flash-mode header boots into a `partition 0 invalid magic number 0xffff` loop even though esptool verified the write.
- **One `FREEINK_DEVICE_*` flag per env** selects the board profile (pins, panel, touch controller) from the SDK's `BoardConfig`. See `freeink-sdk/platformio.sample.ini` for reference envs of every supported device.
- **Serial logs:** `[base]` does not enable logging; without `-DENABLE_SERIAL_LOG` a non-default env prints nothing.
- No touch hardware on your desk? The X4 build still exercises the same code paths through buttons; touch-specific behavior (tap zones, gestures) needs a real device.
+51
View File
@@ -0,0 +1,51 @@
# Dictionary
Look up words while reading an EPUB using an offline StarDict dictionary stored on the SD card.
## Supported Format
The reader supports **StarDict** dictionaries. When searching for dictionaries online, look for "StarDict format" or files with `.dict`, `.idx`, and `.ifo` extensions.
A dictionary folder must contain:
- `.idx` — word index (required, **must be uncompressed** — a `.idx.gz` will not work; decompress it on your computer with `gzip -d` first)
- `.dict` or `.dict.dz` — definition data (`.dict.dz` is supported as-is; entries are decompressed on the fly during lookup)
- `.ifo` — metadata (optional)
Not supported: `.syn` synonym files (ignored), dictionaries with 64-bit index offsets (`idxoffsetbits=64` in the `.ifo` — rare, and rejected with an error), and HTML-formatted definitions render as raw markup rather than styled text.
## Setting Up a Dictionary
1. Copy your dictionary folder(s) to `/dictionaries/` on the SD card — one dictionary per folder, e.g. `/dictionaries/webster/webster.idx` + `webster.dict.dz`. A hidden `/.dictionaries/` folder (dot-prefixed) works the same way, for keeping it out of the file browser.
2. Open **Settings → Reader → Dictionary** on the device.
3. Select a dictionary from the list, or **None** to disable lookups.
The Dictionary setting only appears when at least one usable dictionary folder exists. Folders containing more than one dictionary (multiple `.idx` stems) are skipped as ambiguous.
## Looking Up a Word
Two ways to start a lookup while reading:
- Open the reader menu (**Confirm**) and choose **Look Up**.
- Or set **Settings → Controls → Long-press Menu** to "Dictionary", then hold **Confirm** (~0.4s) on the reading page.
One word on the page becomes highlighted:
1. Use **Left/Right** to move between words in reading order, and the side **Up/Down** buttons to jump between lines.
2. Press **Confirm** to look up the highlighted word.
3. Press **Back** to return to the reader.
On the very first lookup with a dictionary (and again if the dictionary file changes), the reader shows *"Indexing dictionary…"* while it builds a small `.qidx` sidecar file next to the `.idx`. This takes a few seconds for large dictionaries and makes all subsequent lookups fast. The sidecar can be deleted safely at any time — it will simply be rebuilt.
### How Lookup Works
1. **Direct match** — the word is found as-is (case-insensitive) in the dictionary index. Surrounding punctuation is ignored.
2. **Stemming** — on a miss, common English word forms are retried automatically: possessives and plurals (`dogs``dog`, `stories``story`) and verb endings (`walked``walk`, `running``run`, `making``make`).
3. **Not found** — a short popup appears and you return to word selection.
## The Definition Screen
When a word is found, the definition screen shows the matched headword at the top and the definition text below, with a page counter for long definitions.
- **Left/Right** or side **Up/Down** — previous / next page
- **Back** — return to word selection
+195 -90
View File
@@ -1,22 +1,26 @@
# File Formats
These formats describe the SD-card cache files under `/.crosspoint/epub_<hash>/`.
All POD fields are written in the ESP32 little-endian representation used by
`Serialization.h`; strings are length-prefixed UTF-8.
## `book.bin`
### Version 3
### Version 7
ImHex Pattern:
`book.bin` stores EPUB metadata plus lookup tables for spine and TOC entries.
The current firmware writes this version from `BookMetadataCache`.
ImHex pattern:
```c++
import std.mem;
import std.string;
import std.core;
// === Configuration ===
#define EXPECTED_VERSION 3
#define EXPECTED_VERSION 7
#define MAX_STRING_LENGTH 65535
// === String Structure ===
struct String {
u32 length [[hidden, comment("String byte length")]];
if (length > MAX_STRING_LENGTH) {
@@ -29,74 +33,56 @@ fn format_string(String s) {
return s.data;
};
// === Metadata Structure ===
struct Metadata {
String title [[comment("Book title")]];
String author [[comment("Book author")]];
String language [[comment("Book language code")]];
String coverItemHref [[comment("Path to cover image")]];
String textReferenceHref [[comment("Path to guided first text reference")]];
} [[comment("Book metadata information")]];
// === Spine Entry Structure ===
};
struct SpineEntry {
String href [[comment("Resource path")]];
u32 cumulativeSize [[comment("Cumulative size in bytes"), color("FF6B6B")]];
s16 tocIndex [[comment("Index into TOC (-1 if none)"), color("4ECDC4")]];
} [[comment("Spine entry defining reading order")]];
// === TOC Entry Structure ===
u32 cumulativeSize [[comment("Cumulative uncompressed spine size through this entry")]];
s16 tocIndex [[comment("Index into TOC, or inherited/previous TOC index when no direct entry exists")]];
};
struct TocEntry {
String title [[comment("Chapter/section title")]];
String href [[comment("Resource path")]];
String anchor [[comment("Fragment identifier")]];
u8 level [[comment("Nesting level (0-255)"), color("95E1D3")]];
s16 spineIndex [[comment("Index into spine (-1 if none)"), color("F38181")]];
} [[comment("Table of contents entry")]];
// === Book Bin Structure ===
u8 level [[comment("Nesting level")]];
s16 spineIndex [[comment("Index into spine (-1 if none)")]];
};
struct BookBin {
// Header
u8 version [[comment("Format version"), color("FFD93D")]];
// Version validation
u8 version;
if (version != EXPECTED_VERSION) {
std::error(std::format("Unsupported version: {} (expected {})", version, EXPECTED_VERSION));
}
u32 lutOffset [[comment("Offset to lookup tables"), color("6BCB77")]];
u16 spineCount [[comment("Number of spine entries"), color("4D96FF")]];
u16 tocCount [[comment("Number of TOC entries"), color("FF6B9D")]];
// Metadata section
Metadata metadata [[comment("Book metadata")]];
// Validate LUT offset alignment
u32 lutOffset [[comment("Offset to lookup tables")]];
u16 spineCount;
u16 tocCount;
Metadata metadata;
u32 currentOffset = $;
if (currentOffset != lutOffset) {
std::warning(std::format("LUT offset mismatch: expected 0x{:X}, got 0x{:X}", lutOffset, currentOffset));
}
// Lookup Tables
u32 spineLut[spineCount] [[comment("Spine entry offsets"), color("4D96FF")]];
u32 tocLut[tocCount] [[comment("TOC entry offsets"), color("FF6B9D")]];
// Data Entries
SpineEntry spines[spineCount] [[comment("Spine entries (reading order)")]];
TocEntry toc[tocCount] [[comment("Table of contents entries")]];
};
// === File Parsing ===
u32 spineLut[spineCount] [[comment("Spine entry offsets")]];
u32 tocLut[tocCount] [[comment("TOC entry offsets")]];
SpineEntry spines[spineCount];
TocEntry toc[tocCount];
};
BookBin book @ 0x00;
// Validate we've consumed the entire file
u32 fileSize = std::mem::size();
u32 parsedSize = $;
if (parsedSize != fileSize) {
std::warning(std::format("Unparsed data detected: {} bytes remaining at offset 0x{:X}", fileSize - parsedSize, parsedSize));
}
@@ -104,20 +90,45 @@ if (parsedSize != fileSize) {
## `section.bin`
### Version 8
### Version 30
ImHex Pattern:
Each file in `sections/*.bin` stores one laid-out spine section. The header is
also the cache-busting key: if any layout-affecting setting differs from the
current reader settings, the section is discarded and rebuilt.
Version 30 is binary-identical to version 29. The version was bumped because
Arabic contextual shaping changed text measurement (`getTextAdvanceX` now
measures the shaped visual text), so word positions cached by v29 no longer
match what `drawText` renders.
Version 28 introduced serialized word style bits for underline, strikethrough,
superscript, and subscript. The format also includes:
- cache-busting fields for paragraph alignment, hyphenation, embedded CSS,
image rendering mode, and Focus Reading
- page offset LUT
- anchor-to-page map for fragment and footnote navigation
- paragraph and list-item LUTs used by KOReader sync page refinement
- optional per-word Focus Reading split metadata
- per-page footnote entries
- serialized word style bits for underline, strikethrough, superscript, and
subscript
- flat TextBlock word storage (v29): per-word arrays plus one shared
NUL-terminated text blob, replacing v28's length-prefixed word strings. The
on-disk order mirrors the in-RAM arena so the firmware reads a whole block
payload with a single allocation and a single SD read
ImHex pattern:
```c++
import std.mem;
import std.string;
import std.core;
// === Configuration ===
#define EXPECTED_VERSION 8
#define EXPECTED_VERSION 30
#define MAX_STRING_LENGTH 65535
// === String Structure ===
#define FOOTNOTE_NUMBER_LEN 32
#define FOOTNOTE_HREF_LEN 96
struct String {
u32 length [[hidden, comment("String byte length")]];
@@ -131,90 +142,184 @@ fn format_string(String s) {
return s.data;
};
// === Page Structure ===
enum StorageType : u8 {
PageLine = 1
enum PageElementTag : u8 {
TAG_PageLine = 1,
TAG_PageImage = 2,
TAG_PageHorizontalRule = 3
};
enum WordStyle : u8 {
REGULAR = 0,
BOLD = 1,
ITALIC = 2,
BOLD_ITALIC = 3
BOLD_ITALIC = 3,
UNDERLINE = 4,
STRIKETHROUGH = 8,
SUP = 16,
SUB = 32
};
enum BlockStyle : u8 {
enum TextAlign : u8 {
JUSTIFIED = 0,
LEFT_ALIGN = 1,
CENTER_ALIGN = 2,
RIGHT_ALIGN = 3,
NONE = 4
};
struct BlockStyle {
TextAlign alignment;
bool textAlignDefined;
s16 marginTop;
s16 marginBottom;
s16 marginLeft;
s16 marginRight;
s16 paddingTop;
s16 paddingBottom;
s16 paddingLeft;
s16 paddingRight;
s16 textIndent;
bool textIndentDefined;
bool isRtl;
bool directionDefined;
};
struct TextBlock {
u16 wordCount;
u8 hasFocus;
u16 textBytes [[comment("Total size of text[], including one NUL per word")]];
if (wordCount > 0) {
u16 textOff[wordCount] [[comment("Byte offset of word i's text within text[]")]];
s16 wordXPos[wordCount];
if (hasFocus != 0) {
u16 wordFocusSuffixX[wordCount] [[comment("Suffix x offset from word start")]];
}
WordStyle wordStyle[wordCount];
if (hasFocus != 0) {
u8 wordFocusBoundary[wordCount] [[comment("UTF-8 byte boundary between bold prefix and suffix")]];
}
char text[textBytes] [[comment("All words back to back, each NUL-terminated")]];
}
BlockStyle blockStyle;
};
struct ImageBlock {
String imagePath;
s16 width;
s16 height;
};
struct PageLine {
s16 xPos;
s16 yPos;
u16 wordCount;
String words[wordCount];
u16 wordXPos[wordCount];
WordStyle wordStyle[wordCount];
BlockStyle blockStyle;
s16 xPos;
s16 yPos;
TextBlock block;
};
struct PageImage {
s16 xPos;
s16 yPos;
ImageBlock image;
};
struct PageHorizontalRule {
s16 xPos;
s16 yPos;
u16 width;
u8 thickness;
};
struct PageElement {
u8 pageElementType;
if (pageElementType == 1) {
PageElementTag pageElementType;
if (pageElementType == TAG_PageLine) {
PageLine pageLine [[inline]];
} else if (pageElementType == TAG_PageImage) {
PageImage pageImage [[inline]];
} else if (pageElementType == TAG_PageHorizontalRule) {
PageHorizontalRule horizontalRule [[inline]];
} else {
std::error(std::format("Unknown page element type: {}", pageElementType));
}
};
struct FootnoteEntry {
char number[FOOTNOTE_NUMBER_LEN];
char href[FOOTNOTE_HREF_LEN];
};
struct Page {
u16 elementCount;
PageElement elements[elementCount] [[inline]];
u16 footnoteCount;
FootnoteEntry footnotes[footnoteCount];
};
// === Section Bin Structure ===
struct AnchorEntry {
String anchor;
u16 page;
};
struct AnchorMap {
u16 count;
AnchorEntry entries[count];
};
struct ParagraphLut {
u16 count;
u16 paragraphIndex[count];
};
struct SectionBin {
// Header
u8 version [[comment("Format version"), color("FFD93D")]];
// Version validation
u8 version;
if (version != EXPECTED_VERSION) {
std::error(std::format("Unsupported version: {} (expected {})", version, EXPECTED_VERSION));
}
// Cache busting parameters
s32 fontId;
float lineCompression;
bool extraParagraphSpacing;
u8 paragraphAlignment;
u16 viewportWidth;
u16 vieportHeight;
u16 viewportHeight;
bool hyphenationEnabled;
bool embeddedStyle;
u8 imageRendering;
bool focusReadingEnabled;
u16 pageCount;
u32 lutOffset;
Page page[pageCount];
// Validate LUT offset alignment
u32 pageLutOffset;
u32 anchorMapOffset;
u32 paragraphLutOffset;
u32 listItemLutOffset;
Page pages[pageCount];
u32 currentOffset = $;
if (currentOffset != lutOffset) {
std::warning(std::format("LUT offset mismatch: expected 0x{:X}, got 0x{:X}", lutOffset, currentOffset));
if (currentOffset != pageLutOffset) {
std::warning(std::format("Page LUT offset mismatch: expected 0x{:X}, got 0x{:X}", pageLutOffset, currentOffset));
}
u32 pageLut[pageCount] [[comment("Page data offsets")]];
if (anchorMapOffset != 0) {
AnchorMap anchorMap @ anchorMapOffset;
}
if (paragraphLutOffset != 0) {
ParagraphLut paragraphLut @ paragraphLutOffset;
}
if (listItemLutOffset != 0 && paragraphLutOffset != 0) {
u16 listItemIndex[paragraphLut.count] @ listItemLutOffset;
}
// Lookup Tables
u32 lut[pageCount];
};
// === File Parsing ===
SectionBin section @ 0x00;
SectionBin book @ 0x00;
// Validate we've consumed the entire file
u32 fileSize = std::mem::size();
u32 parsedSize = $;
if (parsedSize != fileSize) {
std::warning(std::format("Unparsed data detected: {} bytes remaining at offset 0x{:X}", fileSize - parsedSize, parsedSize));
}
+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 invalidates affected EPUB section caches for the current layout, the same as changing font settings. Sections are rebuilt on demand, then page turns proceed as normal. No changes are made to your EPUB files.
## Examples
<img src="./images/focus-reading/focus-reading-notoserif.jpg" height="500" alt="Focus Reading with Noto Serif font" />
*Focus Reading with Noto Serif font*
<img src="./images/focus-reading/focus-reading-merriweather.jpg" height="500" alt="Focus Reading with Merriweather font" />
*Focus Reading with Merriweather font*
<img src="./images/focus-reading/focus-reading-atkinson.jpg" height="500" alt="Focus Reading with Atkinson Hyperlegible Next font" />
*Focus Reading with Atkinson Hyperlegible Next font*
## Notes
- Focus Reading only applies to regular body text. Already-bold text (headings, emphasis) is left unchanged.
- The setting is per-device, not per-book — it applies to all books while enabled.
+1 -1
View File
@@ -49,5 +49,5 @@ A convenient script `update_hyphenation.sh` is used to update all languages.
To use it, run:
```sh
./scripts/update_hypenation.sh
./scripts/update_hyphenation.sh
```
+42 -23
View File
@@ -5,17 +5,29 @@ This guide explains the multi-language support system in CrossPoint Reader.
## Supported Languages
- English
- French
- German
- Portuguese
- Spanish
- Swedish
- Czech
- Russian
- Ukrainian
- Polish
- Danish
- Turkish
- Español
- Français
- Deutsch
- Čeština
- Português (Brasil)
- Русский
- Svenska
- Română
- Català
- Українська
- Беларуская
- Italiano
- Polski
- Suomi
- Dansk
- Nederlands
- Türkçe
- Қазақша
- Magyar
- Lietuvių
- Slovenščina
- Valencià
- עברית
---
@@ -108,7 +120,9 @@ This automatically:
#### 3. Use in code
```cpp
#include <CrossPointSettings.h>
#include <I18n.h>
#include <Logging.h>
// Using the tr() macro (recommended)
renderer.drawText(font, x, y, tr(STR_MY_NEW_STRING));
@@ -175,7 +189,7 @@ The YAML files use UTF-8 encoding. Special characters are automatically converte
// tr(id) - Get translated string without StrId:: prefix
const char* text = tr(STR_SETTINGS_TITLE);
renderer.drawText(font, x, y, tr(STR_BROWSE_FILES));
Serial.printf("Status: %s\n", tr(STR_CONNECTED));
LOG_INF("I18N", "Status: %s", tr(STR_CONNECTED));
// I18N - Shorthand for I18n::getInstance()
I18N.setLanguage(Language::ES);
@@ -191,34 +205,39 @@ const char* text = tr(STR_SETTINGS_TITLE); // Macro (recommended)
const char* text = I18N.get(StrId::STR_SETTINGS_TITLE); // Direct call
const char* text = I18N[StrId::STR_SETTINGS_TITLE]; // Operator overload
// Set language
// Set runtime language
I18N.setLanguage(Language::ES);
// Get current language
Language lang = I18N.getLanguage();
// Save language setting to file
I18N.saveSettings();
// Load language setting from file
I18N.loadSettings();
// Get character set for font subsetting (static method)
const char* chars = I18n::getCharacterSet(Language::FR);
// Persist a user language choice
SETTINGS.language = static_cast<uint8_t>(Language::ES);
SETTINGS.saveToFile();
```
---
## File Storage
Language settings are stored in:
The selected language is stored with the rest of the device settings in:
```text
/.crosspoint/settings.json
```
The JSON field is `language`, stored as a stable language code string such as
`"EN"`, `"DE"`, or `"HE"` rather than a raw enum value.
Older firmware versions used:
```text
/.crosspoint/language.bin
```
This file contains:
- Version byte
- Current language selection (1 byte)
On load, current firmware migrates that legacy file into `settings.json` and
renames it to `language.bin.bak`.
---
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

Binary file not shown.

After

Width:  |  Height:  |  Size: 234 KiB

+126
View File
@@ -0,0 +1,126 @@
# SD Card Fonts
CrossPoint supports loading additional fonts from the SD card, including fonts
with extended Unicode coverage (CJK, Cyrillic, Greek, etc.).
## Installing Fonts
There are three ways to install fonts:
### Option 1: Download from device (recommended)
1. Connect your CrossPoint reader to Wi-Fi
2. Go to **Settings > System > Manage Fonts**
3. Browse available font families and tap to download
4. Downloaded fonts appear immediately in **Settings > Reader > Font Family**
### Option 2: Upload via web browser
1. Start **File Transfer** and connect through **Join Network** or **Create Hotspot**
2. Open the web interface URL shown on the reader
3. Navigate to the **Fonts** tab
4. Upload `.cpfont` files using the upload form
### Option 3: Manual SD card copy
1. Download font files from the
[crosspoint-fonts repository](https://github.com/crosspoint-reader/crosspoint-fonts)
2. Copy font family folders to one of two locations on your SD card:
- `/.fonts/` — hidden directory (preferred; keeps the SD root tidy
when mounted on a desktop)
- `/fonts/` — visible directory (use this if your OS hides dot-files
and you'd rather see the folder in your file manager)
Both roots are always scanned at boot and the results are merged: a
family installed in `/fonts/` shows up even when `/.fonts/` also
exists, and vice versa. The two roots only collide if the same family
name appears in both — in that case the copy in `/.fonts/` wins and
the duplicate in `/fonts/` is ignored.
SD Card Root/
├── .fonts/ ← Hidden root (preferred)
│ └── Literata/
│ ├── Literata_12.cpfont
│ ├── Literata_14.cpfont
│ ├── Literata_16.cpfont
│ └── Literata_18.cpfont
└── fonts/ ← Visible root (equally valid)
└── Merriweather/
├── Merriweather_12.cpfont
└── ...
3. Insert the SD card and power on your CrossPoint reader
## Available Pre-Built Fonts
The current list of pre-built fonts is maintained in the
[crosspoint-fonts repository](https://github.com/crosspoint-reader/crosspoint-fonts).
## Converting Custom Fonts
To convert your own TrueType/OpenType fonts:
### Prerequisites
pip install freetype-py fonttools
### Single font (one style)
python3 lib/EpdFont/scripts/fontconvert_sdcard.py \
MyFont-Regular.ttf \
--intervals latin-ext \
--sizes 12,14,16,18 \
--style regular \
--name MyFont \
--output-dir ./MyFont/
### Multi-style font
python3 lib/EpdFont/scripts/fontconvert_sdcard.py \
--regular MyFont-Regular.ttf \
--bold MyFont-Bold.ttf \
--italic MyFont-Italic.ttf \
--bolditalic MyFont-BoldItalic.ttf \
--intervals latin-ext \
--sizes 12,14,16,18 \
--name MyFont \
--output-dir ./MyFont/
### Available Unicode interval presets
| Preset | Coverage |
|--------|----------|
| `ascii` | U+0020U+007E (Basic Latin) |
| `latin1` | U+0080U+00FF (Latin-1 Supplement) |
| `latin-ext` | European languages (Latin + Extended-A/B + punctuation + ligatures) |
| `greek` | Greek + Extended Greek |
| `cyrillic` | Cyrillic + Supplement |
| `hebrew` | Hebrew + Alphabetic Presentation Forms |
| `georgian` | Georgian + Georgian Supplement |
| `armenian` | Armenian |
| `ethiopic` | Ethiopic + Extended |
| `vietnamese` | Vietnamese subset (ơ/ư and combining marks) |
| `punctuation` | General punctuation (U+2000U+206F) |
| `cjk` | CJK Unified Ideographs + Hiragana + Katakana + Fullwidth |
| `hangul` | Korean Hangul syllables + Jamo + Compatibility Jamo |
| `cherokee` | Cherokee (historic + supplement block) |
| `tifinagh` | Tifinagh |
| `symbols` | Math, currency, arrows, box-drawing, misc symbols, dingbats |
| `reading` | Literary fiction coverage: Latin, Greek, Cyrillic, math/symbol blocks, supplemental punctuation, and CJK quote marks |
| `builtin` | Matches the firmware's built-in font conversion intervals |
Combine presets with commas: `--intervals latin-ext,greek,cyrillic`
You can also specify arbitrary Unicode ranges directly:
`--intervals latin-ext,(0x2100-0x214F)`
To list all presets with codepoint counts:
python3 lib/EpdFont/scripts/fontconvert_sdcard.py --list-presets
### Additional options
`--force-autohint` — force FreeType's auto-hinter instead of the font's native hinting (useful when a font's built-in hints produce poor results at small sizes).
Install custom fonts via the web interface or manual SD card copy.
+8 -2
View File
@@ -1,7 +1,9 @@
# Translators
Below is a list of users and languages CrossPoint may support in the future.
Note because a language is below does not mean there is official support for the language at this time.
Below is a list of translator credits for languages with known contributors.
Official UI language support is determined by the YAML files in
`lib/I18n/translations/`; see [i18n.md](./i18n.md) for the current supported
language list.
## Contributing
@@ -26,6 +28,7 @@ If you'd like to add your name to this list, please open a PR adding yourself an
## Italian
- [andreaturchet](https://github.com/andreaturchet)
- [fragolinux](https://github.com/fragolinux)
- [alan0ford](https://github.com/alan0ford)
## Russian
- [madebyKir](https://github.com/madebyKir)
@@ -36,6 +39,7 @@ If you'd like to add your name to this list, please open a PR adding yourself an
- [Skrzakk](https://github.com/Skrzakk)
- [pablohc](https://github.com/pablohc)
- [DaniPhii](https://github.com/DaniPhii)
- [lpla](https://github.com/lpla)
## Swedish
- [dawiik](https://github.com/dawiik)
@@ -46,12 +50,14 @@ If you'd like to add your name to this list, please open a PR adding yourself an
## Catalan
- [angeldenom](https://github.com/angeldenom)
- [lpla](https://github.com/lpla)
## Finnish
- [plahteenlahti](https://github.com/plahteenlahti)
## Ukrainian
- [mirus-ua](https://github.com/mirus-ua)
- [KymAndriy](https://github.com/KymAndriy)
## Belarusian
- [Dexif](https://github.com/dexif)
+13 -10
View File
@@ -1,6 +1,6 @@
# Troubleshooting
This document show most common issues and possible solutions while using the device features.
This document shows common issues and possible solutions while using the device features.
- [Troubleshooting](#troubleshooting)
- [Cannot See the Device on the Network](#cannot-see-the-device-on-the-network)
@@ -14,25 +14,27 @@ This document show most common issues and possible solutions while using the dev
**Solutions:**
1. Verify both devices are on the **same WiFi network**
- Check your computer/phone WiFi settings
- Confirm the CrossPoint Reader shows "Connected" status
1. Verify both devices are on the correct network
- Check your computer/phone Wi-Fi settings
- In **Join Network** mode, your computer/phone and CrossPoint Reader must be on the same Wi-Fi network
- In **Create Hotspot** mode, your computer/phone must be connected to the `CrossPoint-Reader` hotspot
2. Double-check the IP address
- Make sure you typed it correctly
- Include `http://` at the beginning
- Try the displayed IP address if `http://crosspoint.local/` does not resolve
3. Try disabling VPN if you're using one
4. Some networks have "client isolation" enabled - check with your network administrator
4. Some networks have "client isolation" enabled - use Create Hotspot mode or check with your network administrator
### Connection Drops or Times Out
**Problem:** WiFi connection is unstable
**Problem:** Wi-Fi connection is unstable
**Solutions:**
1. Move closer to the WiFi router
1. Move closer to the Wi-Fi router, or use Create Hotspot mode for a direct connection
2. Check signal strength on the device (should be at least `||` or better)
3. Avoid interference from other devices
4. Try a different WiFi network if available
4. Try a different Wi-Fi network if available
### Upload Fails
@@ -40,10 +42,11 @@ This document show most common issues and possible solutions while using the dev
**Solutions:**
1. Ensure the file is a valid `.epub` file
2. Check that the SD card has enough free space
1. Check that the SD card has enough free space
2. Check that the filename is valid for the SD card filesystem
3. Try uploading a smaller file first to test
4. Refresh the browser page and try again
5. If WebSocket upload fails repeatedly, refresh the page and retry with the HTTP fallback path
### Saved Password Not Working
+400 -231
View File
@@ -1,72 +1,36 @@
# Webserver Endpoints
This document describes all HTTP and WebSocket endpoints available on the CrossPoint Reader webserver.
This document describes the HTTP, WebSocket, WebDAV, and discovery endpoints
available while CrossPoint Reader is in File Transfer or Calibre Wireless mode.
- [Webserver Endpoints](#webserver-endpoints)
- [Overview](#overview)
- [HTTP Endpoints](#http-endpoints)
- [GET `/` - Home Page](#get----home-page)
- [GET `/files` - File Browser Page](#get-files---file-browser-page)
- [GET `/api/status` - Device Status](#get-apistatus---device-status)
- [GET `/api/files` - List Files](#get-apifiles---list-files)
- [POST `/upload` - Upload File](#post-upload---upload-file)
- [POST `/mkdir` - Create Folder](#post-mkdir---create-folder)
- [POST `/delete` - Delete File or Folder](#post-delete---delete-file-or-folder)
- [WebSocket Endpoint](#websocket-endpoint)
- [Port 81 - Fast Binary Upload](#port-81---fast-binary-upload)
- [Network Modes](#network-modes)
- [Station Mode (STA)](#station-mode-sta)
- [Access Point Mode (AP)](#access-point-mode-ap)
- [Notes](#notes)
- HTTP server: port 80
- WebSocket upload server: port 81
- UDP discovery listener: port 8134
- WebDAV: port 80, handled by the same HTTP server
Examples use `crosspoint.local`. If mDNS does not resolve on your network, use
the IP address shown on the device screen.
## Overview
## HTTP Pages
The CrossPoint Reader exposes a webserver for file management and device monitoring:
| Method | Path | Purpose |
|--------|------|---------|
| `GET` | `/` | Home/status page |
| `GET` | `/files` | File manager page |
| `GET` | `/settings` | Web settings page |
| `GET` | `/fonts` | SD-card font manager page |
| `GET` | `/js/jszip.min.js` | JavaScript asset used by the file manager |
- **HTTP Server**: Port 80
- **WebSocket Server**: Port 81 (for fast binary uploads)
## Device Status
---
### `GET /api/status`
## HTTP Endpoints
### GET `/` - Home Page
Serves the home page HTML interface.
**Request:**
```bash
curl http://crosspoint.local/
```
**Response:** HTML page (200 OK)
---
### GET `/files` - File Browser Page
Serves the file browser HTML interface.
**Request:**
```bash
curl http://crosspoint.local/files
```
**Response:** HTML page (200 OK)
---
### GET `/api/status` - Device Status
Returns JSON with device status information.
**Request:**
```bash
curl http://crosspoint.local/api/status
```
**Response (200 OK):**
Response:
```json
{
"version": "1.0.0",
@@ -74,258 +38,463 @@ curl http://crosspoint.local/api/status
"mode": "STA",
"rssi": -45,
"freeHeap": 123456,
"uptime": 3600
"uptime": 3600,
"device": "X4"
}
```
| Field | Type | Description |
| ---------- | ------ | --------------------------------------------------------- |
| `version` | string | CrossPoint firmware version |
| `ip` | string | Device IP address |
| `mode` | string | `"STA"` (connected to WiFi) or `"AP"` (access point mode) |
| `rssi` | number | WiFi signal strength in dBm (0 in AP mode) |
| `freeHeap` | number | Free heap memory in bytes |
| `uptime` | number | Seconds since device boot |
| Field | Type | Description |
|-------|------|-------------|
| `version` | string | Firmware version |
| `ip` | string | Device IP address |
| `mode` | string | `"STA"` for joined Wi-Fi or `"AP"` for hotspot mode |
| `rssi` | number | Wi-Fi RSSI in dBm; `0` in AP mode |
| `freeHeap` | number | Free heap in bytes |
| `uptime` | number | Seconds since boot |
| `device` | string | `"X3"` or `"X4"` hardware detection |
---
## File Management
### GET `/api/files` - List Files
### `GET /api/files`
Returns a JSON array of files and folders in the specified directory.
Lists files and folders under a directory.
**Request:**
```bash
# List root directory
curl http://crosspoint.local/api/files
# List specific directory
curl "http://crosspoint.local/api/files?path=/Books"
```
**Query Parameters:**
Query parameters:
| Parameter | Required | Default | Description |
| --------- | -------- | ------- | ---------------------- |
| `path` | No | `/` | Directory path to list |
| Parameter | Required | Default | Description |
|-----------|----------|---------|-------------|
| `path` | No | `/` | Directory to list |
Response:
**Response (200 OK):**
```json
[
{"name": "MyBook.epub", "size": 1234567, "isDirectory": false, "isEpub": true},
{"name": "Notes", "size": 0, "isDirectory": true, "isEpub": false},
{"name": "document.pdf", "size": 54321, "isDirectory": false, "isEpub": false}
{"name":"MyBook.epub","size":1234567,"isDirectory":false,"isEpub":true},
{"name":"Notes","size":0,"isDirectory":true,"isEpub":false}
]
```
| Field | Type | Description |
| ------------- | ------- | ---------------------------------------- |
| `name` | string | File or folder name |
| `size` | number | Size in bytes (0 for directories) |
| `isDirectory` | boolean | `true` if the item is a folder |
| `isEpub` | boolean | `true` if the file has `.epub` extension |
Hidden dotfiles are omitted unless the device setting `showHiddenFiles` is
enabled. `System Volume Information` and `XTCache` are always hidden/protected.
**Notes:**
- Hidden files (starting with `.`) are automatically filtered out
- System folders (`System Volume Information`, `XTCache`) are hidden
### `GET /download`
---
Downloads a file from the SD card.
### POST `/upload` - Upload File
Uploads a file to the SD card via multipart form data.
**Request:**
```bash
# Upload to root directory
curl -X POST -F "file=@mybook.epub" http://crosspoint.local/upload
curl -OJ "http://crosspoint.local/download?path=/Books/MyBook.epub"
```
# Upload to specific directory
Query parameters:
| Parameter | Required | Description |
|-----------|----------|-------------|
| `path` | Yes | File path to download |
Protected dotfiles, `System Volume Information`, and `XTCache` cannot be
downloaded. EPUB files are served as `application/epub+zip`; other files use
`application/octet-stream`.
### `POST /upload`
Uploads a file with HTTP multipart form data.
```bash
curl -X POST -F "file=@mybook.epub" "http://crosspoint.local/upload?path=/Books"
```
**Query Parameters:**
Query parameters:
| Parameter | Required | Default | Description |
| --------- | -------- | ------- | ------------------------------- |
| `path` | No | `/` | Target directory for the upload |
| Parameter | Required | Default | Description |
|-----------|----------|---------|-------------|
| `path` | No | `/` | Destination directory |
**Response (200 OK):**
```
Successful response:
```text
File uploaded successfully: mybook.epub
```
**Error Responses:**
Notes:
| Status | Body | Cause |
| ------ | ----------------------------------------------- | --------------------------- |
| 400 | `Failed to create file on SD card` | Cannot create file |
| 400 | `Failed to write to SD card - disk may be full` | Write error during upload |
| 400 | `Failed to write final data to SD card` | Error flushing final buffer |
| 400 | `Upload aborted` | Client aborted the upload |
| 400 | `Unknown error during upload` | Unspecified error |
- Existing files with the same name are overwritten.
- EPUB cache data for the uploaded path is cleared after a successful upload.
- HTTP upload uses a 4 KB write buffer before flushing to the SD card.
**Notes:**
- Existing files with the same name will be overwritten
- Uses a 4KB buffer for efficient SD card writes
### `POST /mkdir`
---
Creates a folder.
### POST `/mkdir` - Create Folder
Creates a new folder on the SD card.
**Request:**
```bash
curl -X POST -d "name=NewFolder&path=/" http://crosspoint.local/mkdir
```
**Form Parameters:**
Form parameters:
| Parameter | Required | Default | Description |
| --------- | -------- | ------- | ---------------------------- |
| `name` | Yes | - | Name of the folder to create |
| `path` | No | `/` | Parent directory path |
| Parameter | Required | Default | Description |
|-----------|----------|---------|-------------|
| `name` | Yes | - | New folder name |
| `path` | No | `/` | Parent folder |
**Response (200 OK):**
```
Folder created: NewFolder
```
### `POST /rename`
**Error Responses:**
Renames a file.
| Status | Body | Cause |
| ------ | ----------------------------- | ----------------------------- |
| 400 | `Missing folder name` | `name` parameter not provided |
| 400 | `Folder name cannot be empty` | Empty folder name |
| 400 | `Folder already exists` | Folder with same name exists |
| 500 | `Failed to create folder` | SD card error |
---
### POST `/delete` - Delete File or Folder
Deletes a file or folder from the SD card.
**Request:**
```bash
# Delete a file
curl -X POST -d "path=/Books/mybook.epub&type=file" http://crosspoint.local/delete
# Delete an empty folder
curl -X POST -d "path=/OldFolder&type=folder" http://crosspoint.local/delete
curl -X POST -d "path=/Books/old.epub&name=new.epub" http://crosspoint.local/rename
```
**Form Parameters:**
Form parameters:
| Parameter | Required | Default | Description |
| --------- | -------- | ------- | -------------------------------- |
| `path` | Yes | - | Path to the item to delete |
| `type` | No | `file` | Type of item: `file` or `folder` |
| Parameter | Required | Description |
|-----------|----------|-------------|
| `path` | Yes | Existing file path |
| `name` | Yes | New file name, not a path |
**Response (200 OK):**
```
Deleted successfully
Only files can be renamed through this endpoint. The old EPUB cache path is
cleared before the rename.
### `POST /move`
Moves a file into an existing folder.
```bash
curl -X POST -d "path=/Books/mybook.epub&dest=/Read" http://crosspoint.local/move
```
**Error Responses:**
Form parameters:
| Status | Body | Cause |
| ------ | --------------------------------------------- | ----------------------------- |
| 400 | `Missing path` | `path` parameter not provided |
| 400 | `Cannot delete root directory` | Attempted to delete `/` |
| 400 | `Folder is not empty. Delete contents first.` | Non-empty folder |
| 403 | `Cannot delete system files` | Hidden file (starts with `.`) |
| 403 | `Cannot delete protected items` | Protected system folder |
| 404 | `Item not found` | Path does not exist |
| 500 | `Failed to delete item` | SD card error |
| Parameter | Required | Description |
|-----------|----------|-------------|
| `path` | Yes | Existing file path |
| `dest` | Yes | Existing destination folder |
**Protected Items:**
- Files/folders starting with `.`
- `System Volume Information`
- `XTCache`
Only files can be moved through this endpoint. The old EPUB cache path is
cleared before the move.
---
### `POST /delete`
## WebSocket Endpoint
Deletes one or more files or empty folders.
### Port 81 - Fast Binary Upload
A WebSocket endpoint for high-speed binary file uploads. More efficient than HTTP multipart for large files.
**Connection:**
```bash
curl -X POST -d "path=/Books/mybook.epub" http://crosspoint.local/delete
curl -X POST -d 'paths=["/Books/old.epub","/OldFolder"]' http://crosspoint.local/delete
```
Form parameters:
| Parameter | Required | Description |
|-----------|----------|-------------|
| `path` | Yes, unless `paths` is provided | Single path to delete |
| `paths` | Yes, unless `path` is provided | JSON array of paths to delete |
Protected items cannot be deleted. Non-empty folders are rejected. EPUB cache
data for deleted files is cleared.
## Settings API
### `GET /api/settings`
Returns a streamed JSON array of editable settings. Each item contains common
fields plus type-specific fields.
```bash
curl http://crosspoint.local/api/settings
```
Example item:
```json
{
"key": "fontSize",
"name": "Font Size",
"category": "Reader",
"type": "enum",
"value": 1,
"options": ["Small", "Medium", "Large"]
}
```
Types:
| Type | Extra fields |
|------|--------------|
| `toggle` | `value` (`0` or `1`) |
| `enum` | `value`, `options` |
| `value` | `value`, `min`, `max`, `step` |
| `string` | `value` |
The font-family setting includes SD-card font families when they are installed.
### `POST /api/settings`
Applies a partial settings update from a JSON object.
```bash
curl -X POST \
-H "Content-Type: application/json" \
-d '{"fontSize":2,"showHiddenFiles":1}' \
http://crosspoint.local/api/settings
```
Successful response:
```text
Applied 2 setting(s)
```
## Font Management API
### `GET /api/fonts`
Lists installed SD-card font families.
```bash
curl http://crosspoint.local/api/fonts
```
Response:
```json
{
"maxFamilies": 128,
"families": [
{
"name": "Literata",
"sizes": [12, 14, 16, 18],
"files": [
{"name": "Literata_12.cpfont", "size": 123456}
]
}
]
}
```
### `POST /api/fonts/upload`
Uploads one `.cpfont` file into a family folder.
```bash
curl -X POST \
-F "family=Literata" \
-F "file=@Literata_12.cpfont" \
http://crosspoint.local/api/fonts/upload
```
The handler validates the family name, `.cpfont` filename, and `CPFONT` magic
bytes before accepting the file.
Successful response:
```json
{"ok":true}
```
### `POST /api/fonts/delete`
Deletes an installed font family.
```bash
curl -X POST \
-H "Content-Type: application/json" \
-d '{"family":"Literata"}' \
http://crosspoint.local/api/fonts/delete
```
Successful response:
```json
{"ok":true}
```
## OPDS Server API
### `GET /api/opds`
Lists saved OPDS servers. Passwords are never returned.
```bash
curl http://crosspoint.local/api/opds
```
Response:
```json
[
{
"index": 0,
"name": "My Catalog",
"url": "http://calibre.local:8080/opds",
"username": "reader",
"hasPassword": true
}
]
```
### `POST /api/opds`
Adds or updates an OPDS server. Include `index` to update an existing entry.
If `password` is omitted during an update, the existing password is preserved.
```bash
curl -X POST \
-H "Content-Type: application/json" \
-d '{"name":"My Catalog","url":"http://calibre.local:8080/opds","username":"reader","password":"secret"}' \
http://crosspoint.local/api/opds
```
### `POST /api/opds/delete`
Deletes an OPDS server by index.
```bash
curl -X POST \
-H "Content-Type: application/json" \
-d '{"index":0}' \
http://crosspoint.local/api/opds/delete
```
## Wi-Fi Credential API
### `GET /api/wifi`
Lists saved Wi-Fi networks. Passwords are never returned.
```bash
curl http://crosspoint.local/api/wifi
```
Response:
```json
[
{
"index": 0,
"ssid": "HomeWiFi",
"hasPassword": true,
"isLastConnected": true
}
]
```
### `POST /api/wifi`
Adds or updates a saved Wi-Fi network. Include `index` to update an existing
entry. If `password` is omitted during an update, the existing password is
preserved.
```bash
curl -X POST \
-H "Content-Type: application/json" \
-d '{"ssid":"HomeWiFi","password":"secret"}' \
http://crosspoint.local/api/wifi
```
### `POST /api/wifi/delete`
Deletes a saved Wi-Fi network by index.
```bash
curl -X POST \
-H "Content-Type: application/json" \
-d '{"index":0}' \
http://crosspoint.local/api/wifi/delete
```
## WebSocket Upload
### Port 81
The WebSocket path is used for fast binary uploads from the file manager and
Calibre plugin workflows.
Connection:
```text
ws://crosspoint.local:81/
```
**Protocol:**
Protocol:
1. **Client** sends TEXT message: `START:<filename>:<size>:<path>`
2. **Server** responds with TEXT: `READY`
3. **Client** sends BINARY messages with file data chunks
4. **Server** sends TEXT progress updates: `PROGRESS:<received>:<total>`
5. **Server** sends TEXT when complete: `DONE` or `ERROR:<message>`
1. Client sends text: `START:<filename>:<size>:<path>`
2. Server replies `READY`
3. Client sends binary chunks
4. Server sends `PROGRESS:<received>:<total>` every 64 KB or at completion
5. Server sends `DONE` when complete or `ERROR:<message>` on failure
**Example Session:**
Example session:
```
Client -> "START:mybook.epub:1234567:/Books"
Server -> "READY"
Client -> [binary chunk 1]
Client -> [binary chunk 2]
Server -> "PROGRESS:65536:1234567"
Client -> [binary chunk 3]
```text
Client -> START:mybook.epub:1234567:/Books
Server -> READY
Client -> [binary chunk]
Server -> PROGRESS:65536:1234567
...
Server -> "PROGRESS:1234567:1234567"
Server -> "DONE"
Server -> DONE
```
**Error Messages:**
Error messages include:
| Message | Cause |
| --------------------------------- | ---------------------------------- |
| `ERROR:Failed to create file` | Cannot create file on SD card |
| `ERROR:Invalid START format` | Malformed START message |
| `ERROR:No upload in progress` | Binary data received without START |
| `ERROR:Write failed - disk full?` | SD card write error |
| Message | Cause |
|---------|-------|
| `ERROR:Upload already in progress` | A second upload was started before the first completed |
| `ERROR:Invalid START format` | Malformed START message or invalid size token |
| `ERROR:Failed to create file` | Destination file could not be opened |
| `ERROR:No upload in progress` | Binary data arrived without a matching START |
| `ERROR:Upload overflow` | Client sent more bytes than declared |
| `ERROR:Write failed - disk full?` | SD write failed |
**Example with `websocat`:**
```bash
# Interactive session
websocat ws://crosspoint.local:81
Incomplete WebSocket uploads are deleted on disconnect or error.
# Then type:
START:mybook.epub:1234567:/Books
# Wait for READY, then send binary data
## WebDAV
The same HTTP server registers a WebDAV-compatible handler for file manager clients.
Supported methods:
```text
OPTIONS, GET, HEAD, PUT, DELETE, PROPFIND, MKCOL, MOVE, COPY, LOCK, UNLOCK
```
**Notes:**
- Progress updates are sent every 64KB or at completion
- Disconnection during upload will delete the incomplete file
- Existing files with the same name will be overwritten
Notes:
---
- `PUT` writes to a temporary `.davtmp` file first, then renames it into place.
- Protected paths are rejected.
- `LOCK` and `UNLOCK` are accepted for client compatibility only. The server
does not implement full WebDAV Class 2 locking semantics such as persistent
locks or lock discovery.
## UDP Discovery
The server listens on UDP port `8134`. When it receives the text payload
`hello`, it replies to the sender with:
```text
crosspoint (on <hostname>);81
```
The final field is the WebSocket upload port.
## Network Modes
The device can operate in two network modes:
### Station Mode (STA)
- Device connects to an existing WiFi network
- IP address assigned by router/DHCP
- `mode` field in `/api/status` returns `"STA"`
- `rssi` field shows signal strength
- Device joins an existing 2.4 GHz Wi-Fi network.
- `crosspoint.local` is advertised with mDNS when available.
- `/api/status` returns `"mode": "STA"` and RSSI in dBm.
### Access Point Mode (AP)
- Device creates its own WiFi hotspot
- Default IP is typically `192.168.4.1`
- `mode` field in `/api/status` returns `"AP"`
- `rssi` field returns `0`
---
- Device creates an open hotspot named `CrossPoint-Reader`.
- The device shows a Wi-Fi QR code and URL QR code.
- The fallback IP is typically `192.168.4.1`.
- `/api/status` returns `"mode": "AP"` and `"rssi": 0`.
## Notes
### Calibre Wireless
- These examples use `crosspoint.local`. If your network does not support mDNS or the address does not resolve, replace it with the specific **IP Address** displayed on your device screen (e.g., `http://192.168.1.102/`).
- All paths on the SD card start with `/`
- Trailing slashes are automatically stripped (except for root `/`)
- The webserver uses chunked transfer encoding for file listings
Calibre Wireless starts the same web server in STA mode and displays setup
instructions plus WebSocket upload progress on the device screen.
+103 -185
View File
@@ -1,235 +1,153 @@
# Web Server Guide
This guide explains how to connect your CrossPoint Reader to WiFi and use the built-in web server to upload files from your computer or phone.
This guide explains how to use CrossPoint Reader's built-in web server for file
transfer, device settings, Wi-Fi/OPDS management, and SD-card font management.
## Overview
CrossPoint Reader includes a built-in web server that allows you to:
The web server is available while the device is in **File Transfer** or
**Calibre Wireless** mode. It can:
- Upload files wirelessly from any device on the same WiFi network
- Browse and manage files on your device's SD card
- Create folders to organize your library
- Delete files and folders
- Upload, download, rename, move, and delete files on the SD card
- Create folders
- Edit many device settings from a browser
- Manage saved Wi-Fi networks and OPDS servers
- Upload and delete `.cpfont` SD-card font families
- Accept WebDAV clients and Calibre wireless uploads
## Prerequisites
The server does not require authentication. Use it only on trusted private
networks or in hotspot mode when you control who is connected.
- Your CrossPoint Reader device
- A WiFi network
- A computer, phone, or tablet connected to the **same WiFi network**
## Starting File Transfer
---
1. From the Home screen, select **File Transfer**.
2. Choose one of the available modes:
## Step 1: Accessing the WiFi Screen
| Mode | Use when |
|------|----------|
| **Join Network** | You want the reader to join an existing Wi-Fi network. |
| **Calibre Wireless** | You want to receive books from the CrossPoint Calibre plugin workflow. |
| **Create Hotspot** | You want the reader to create its own open Wi-Fi network. |
1. From the main menu or file browser, navigate to the **Settings** screen
2. Select the **WiFi** option
3. The device will automatically start scanning for available networks
## Join Network Mode
---
1. Select **Join Network**.
2. If you have saved Wi-Fi credentials, CrossPoint first tries the last
connected network, then other visible saved networks in signal-strength
order. Press **Back** to cancel or **Confirm** to stop auto-connect and show
the network list.
3. If the network list is shown, pick a 2.4 GHz Wi-Fi network from the scan
results.
4. Enter the password if prompted.
5. Save credentials if you want the reader to reconnect automatically next time.
## Step 2: Connecting to WiFi
After connection, the reader shows:
### Viewing Available Networks
- The connected SSID
- A QR code for the web URL
- The direct IP URL, for example `http://192.168.1.102/`
- The mDNS fallback URL, usually `http://crosspoint.local/`
Once the scan completes, you'll see a list of available WiFi networks with the following indicators:
Use either URL from a phone, tablet, or computer on the same network.
- **Signal strength bars** (`||||`, `|||`, `||`, `|`) - Shows connection quality
- **`*` symbol** - Indicates the network is password-protected (encrypted)
- **`+` symbol** - Indicates you have previously saved credentials for this network
## Create Hotspot Mode
<img src="./images/wifi/wifi_networks.jpeg" height="500">
1. Select **Create Hotspot**.
2. Connect your phone or computer to the open Wi-Fi network:
### Selecting a Network
```text
CrossPoint-Reader
```
1. Use the **Left/Right** (or **Volume Up/Down**) buttons to navigate through the network list
2. Press **Confirm** to select the highlighted network
3. Open the URL shown on the reader. `http://crosspoint.local/` is preferred
when supported; the fallback IP is typically `http://192.168.4.1/`.
### Entering Password (for encrypted networks)
The reader displays one QR code for joining the hotspot and another QR code for
opening the web interface.
If the network requires a password:
## Calibre Wireless Mode
1. An on-screen keyboard will appear
2. Use the navigation buttons to select characters
3. Press **Confirm** to enter each character
4. When complete, select the **Done** option on the keyboard
Calibre Wireless starts the same web server in station mode, then displays setup
instructions and upload progress on the reader. Use this mode with the
CrossPoint Calibre plugin or other clients that speak the documented WebSocket
upload protocol.
<img src="./images/wifi/wifi_password.jpeg" height="500">
For Calibre OPDS browsing, add `/opds` to the catalog URL when configuring an
OPDS server.
**Note:** If you've previously connected to this network, the saved password will be used automatically.
## Web Interface
### Connection Process
The browser UI has four primary pages.
The device will display "Connecting..." while establishing the connection. This typically takes 5-10 seconds.
### Home
### Saving Credentials
If this is a new network, you'll be prompted to save the password:
- Select **Yes** to save credentials for automatic connection next time (NOTE: These are stored in plaintext on the device's SD card. Do not use this for sensitive networks.)
- Select **No** to connect without saving
---
## Step 3: Connection Success
Once connected, the screen will display:
- **Network name** (SSID)
- **IP Address** (e.g., `192.168.1.102`)
- **Web server URL** (e.g., `http://192.168.1.102/`)
<img src="./images/wifi/wifi_connected.jpeg" height="500">
**Important:** Make note of the IP address - you'll need this to access the web interface from your computer or phone.
---
## Step 4: Accessing the Web Interface
### From a Computer
1. Ensure your computer is connected to the **same WiFi network** as your CrossPoint Reader
2. Open any web browser (Chrome is recommended)
3. Type the IP address shown on your device into the browser's address bar
- Example: `http://192.168.1.102/`
4. Press Enter
### From a Phone or Tablet
1. Ensure your phone/tablet is connected to the **same WiFi network** as your CrossPoint Reader
2. Open your mobile browser (Safari, Chrome, etc.)
3. Type the IP address into the address bar
- Example: `http://192.168.1.102/`
4. Tap Go
---
## Step 5: Using the Web Interface
### Home Page
The home page displays:
- Device status and version information
- WiFi connection status
- Current IP address
- Available memory
Navigation links:
- **Home** - Returns to the status page
- **File Manager** - Access file management features
<img src="./images/wifi/webserver_homepage.png" width="600">
The Home page shows firmware status, network mode, IP address, device type,
uptime, and free heap.
### File Manager
Click **File Manager** to access file management features.
The File Manager page can:
#### Browsing Files
- Browse SD-card folders
- Upload files, using WebSocket upload when available and HTTP upload as a fallback
- Create folders
- Download files
- Rename files
- Move files into existing folders
- Delete one or more selected files or empty folders
- The file manager displays all files and folders on your SD card
- **Folders** are highlighted in yellow and indicated with a 📁 icon
- **EPUB Files** are highlighted in green and indicated with a 📗 icon
- **All Other Files** are not highlighted and indicated with a 📄 icon
- Click on a folder name to navigate into it
- Use the breadcrumb navigation at the top to go back to parent folders
Existing files with the same name are overwritten by uploads. When EPUB files
are overwritten, moved, renamed, or deleted through the web server, the matching
book cache is cleared so stale metadata is not reused.
<img src="./images/wifi/webserver_files.png" width="600">
### Settings
#### Uploading Files
The Settings page exposes many firmware settings in the browser. It also has
cards for:
1. Click the **📤 Upload** button in the top-right corner
2. Click **Choose File** and select a file from your device
3. Click **Upload**
4. A progress bar will show the upload status
5. The page will automatically refresh when the upload is complete
- Saved Wi-Fi networks
- OPDS servers
<img src="./images/wifi/webserver_upload.png" width="600">
Passwords are accepted when adding or editing entries, but saved passwords are
not returned by the API.
#### Creating Folders
### Fonts
1. Click the **📁 New Folder** button in the top-right corner
2. Enter a folder name (must not contain characters \" * : < > ? / \\ | and must not be . or ..)
3. Click **Create Folder**
The Fonts page lists installed SD-card font families and lets you upload
`.cpfont` files. Upload files from one font family at a time. The server validates
the font family name, filename, and `.cpfont` magic bytes before accepting the
upload.
This is useful for organizing your library by genre, author, series or file type.
Installed fonts appear in **Settings > Reader > Font Family** after the font
registry refreshes.
#### Deleting Files and Folders
## Command Line Use
1. Click the **🗑️** (trash) icon next to any file or folder
2. Confirm the deletion in the popup dialog
3. Click **Delete** to permanently remove the item
Power users can use `curl`, WebDAV clients, or WebSocket clients while the web
server is running.
**Warning:** Deletion is permanent and cannot be undone!
**Note:** Folders must be empty before they can be deleted.
#### Moving Files
1. Click the **📂** (folder) icon next to any file
2. Enter a folder name or select one from the dropdown
3. Click **Move** to relocate the file
**Note:** Typing in a nonexistent folder name will result in the following error: "Failed to move: Destination not found"
#### Renaming Files
1. Click the **✏️** (pencil) icon next to any file
2. Enter a file name (must not contain characters \" * : < > ? / \\ | and must not be . or ..)
3. Click **Rename** to permanently rename the file
---
## Command Line File Management
For power users, you can manage files directly from your terminal using `curl` while the device is in File Upload mode. Detailed documentation can be found [here](./webserver-endpoints.md).
Endpoint details are documented in [webserver-endpoints.md](./webserver-endpoints.md).
## Security Notes
- The web server runs on port 80 (standard HTTP)
- **No authentication is required** - anyone on the same network can access the interface
- The web server is only accessible while the WiFi screen shows "Connected"
- The web server automatically stops when you exit the WiFi screen
- For security, only use on trusted private networks
- The HTTP server runs on port 80.
- The WebSocket upload server runs on port 81.
- There is no authentication.
- Anyone on the same network can access the web interface while it is running.
- The server stops when you exit File Transfer or Calibre Wireless mode.
- Hotspot mode creates an open network for connectivity fallback; disconnect when done.
---
## Tips
## Technical Details
- **Supported WiFi:** 2.4GHz networks (802.11 b/g/n)
- **Web Server Port:** 80 (HTTP)
- **Maximum Upload Size:** Limited by available SD card space
- **Browser Compatibility:** All modern browsers (Chrome, Firefox, Safari, Edge)
---
## Tips and Best Practices
1. **Organize with folders** - Create folders before uploading to keep your library organized
2. **Check signal strength** - Stronger signals (`|||` or `||||`) provide faster, more reliable uploads
3. **Upload multiple files** - You can select and upload multiple files at once; the manager will queue them and refresh when the batch is finished
4. **Use descriptive names** - Name your folders clearly (e.g., "SciFi", "Mystery", "Non-Fiction")
5. **Keep credentials saved** - Save your WiFi password for quick reconnection in the future
6. **Exit when done** - Press **Back** to exit the WiFi screen and save battery
---
## Exiting WiFi Mode
When you're finished uploading files:
1. Press the **Back** button on your CrossPoint Reader
2. The web server will automatically stop
3. WiFi will disconnect to conserve battery
4. You'll return to the previous screen
Your uploaded files will be immediately available in the file browser!
---
1. Use **Create Hotspot** when no trusted network is available.
2. Prefer `crosspoint.local` when available, but keep the displayed IP address as a fallback.
3. Move closer to the router if upload progress stalls in Join Network mode.
4. Upload custom fonts through the Fonts page or copy them to `/.fonts/` or `/fonts/` on the SD card.
5. Exit File Transfer mode when finished to conserve battery.
## Related Documentation
- [User Guide](../USER_GUIDE.md) - General device operation
- [Troubleshooting](./troubleshooting.md) - Troubleshooting
- [README](../README.md) - Project overview and features
- [User Guide](../USER_GUIDE.md)
- [Webserver Endpoints](./webserver-endpoints.md)
- [SD Card Fonts](./sd-card-fonts.md)
- [Troubleshooting](./troubleshooting.md)
Submodule
+1
Submodule freeink-sdk added at 566fce3d4f
+46 -29
View File
@@ -15,11 +15,11 @@ void EpdFont::getTextBounds(const char* string, const int startX, const int star
return;
}
int32_t cursorXFP = fp4::fromPixel(startX); // 12.4 fixed-point accumulator
int lastBaseX = startX;
int lastBaseAdvanceFP = 0; // 12.4 fixed-point
int lastBaseLeft = 0;
int lastBaseWidth = 0;
int lastBaseTop = 0;
constexpr int MIN_COMBINING_GAP_PX = 1;
int32_t prevAdvanceFP = 0; // 12.4 fixed-point: prev glyph's advance + next kern for snap
uint32_t cp;
uint32_t prevCp = 0;
while ((cp = utf8NextCodepoint(reinterpret_cast<const uint8_t**>(&string)))) {
@@ -31,24 +31,30 @@ void EpdFont::getTextBounds(const char* string, const int startX, const int star
const EpdGlyph* glyph = getGlyph(cp);
if (!glyph) {
prevCp = 0;
// Keep cursor movement stable when a base glyph is missing, but don't attach subsequent
// combining marks to stale base metrics.
if (!isCombining) {
lastBaseX += fp4::toPixel(prevAdvanceFP); // flush pending advance before resetting
prevCp = 0;
prevAdvanceFP = 0;
lastBaseLeft = 0;
lastBaseWidth = 0;
lastBaseTop = 0;
}
continue;
}
int raiseBy = 0;
if (isCombining) {
const int currentGap = glyph->top - glyph->height - lastBaseTop;
if (currentGap < MIN_COMBINING_GAP_PX) {
raiseBy = MIN_COMBINING_GAP_PX - currentGap;
}
}
const combiningMark::Anchor anchor = combiningMark::anchorFor(cp);
const int raiseBy = isCombining ? combiningMark::raiseAboveBase(anchor, glyph->top, glyph->height, lastBaseTop) : 0;
if (!isCombining && prevCp != 0) {
cursorXFP += getKerning(prevCp, cp); // 4.4 fixed-point kern
const auto kernFP = getKerning(prevCp, cp); // 4.4 fixed-point kern
lastBaseX += fp4::toPixel(prevAdvanceFP + kernFP);
}
const int cursorXPixels = fp4::toPixel(cursorXFP); // snap 12.4 fixed-point to nearest pixel
const int glyphBaseX = isCombining ? (lastBaseX + fp4::toPixel(lastBaseAdvanceFP / 2)) : cursorXPixels;
const int glyphBaseX = isCombining ? combiningMark::anchorOver(anchor, lastBaseX, lastBaseLeft, lastBaseWidth,
glyph->left, glyph->width)
: lastBaseX;
const int glyphBaseY = startY - raiseBy;
*minX = std::min(*minX, glyphBaseX + glyph->left);
@@ -57,10 +63,10 @@ void EpdFont::getTextBounds(const char* string, const int startX, const int star
*maxY = std::max(*maxY, glyphBaseY + glyph->top);
if (!isCombining) {
lastBaseX = cursorXPixels;
lastBaseAdvanceFP = glyph->advanceX; // 12.4 fixed-point
lastBaseLeft = glyph->left;
lastBaseWidth = glyph->width;
lastBaseTop = glyph->top;
cursorXFP += glyph->advanceX; // 12.4 fixed-point advance
prevAdvanceFP = glyph->advanceX; // 12.4 fixed-point
prevCp = cp;
}
}
@@ -96,6 +102,9 @@ static uint8_t lookupKernClass(const EpdKernClassEntry* entries, const uint16_t
}
int8_t EpdFont::getKerning(const uint32_t leftCp, const uint32_t rightCp) const {
if (utf8IsCjkBreakable(leftCp) || utf8IsCjkBreakable(rightCp)) {
return 0;
}
if (!data->kernMatrix) {
return 0;
}
@@ -148,24 +157,32 @@ uint32_t EpdFont::applyLigatures(uint32_t cp, const char*& text) const {
const EpdGlyph* EpdFont::getGlyph(const uint32_t cp) const {
const int count = data->intervalCount;
if (count == 0) return nullptr;
if (count == 0 && !data->glyphMissHandler) return nullptr;
const EpdUnicodeInterval* intervals = data->intervals;
const auto* end = intervals + count;
if (count > 0) {
const EpdUnicodeInterval* intervals = data->intervals;
const auto* end = intervals + count;
// upper_bound: range lookup. Finds the first interval with first > cp, so the
// interval just before it is the last one with first <= cp. That's the only
// candidate that could contain cp. Then we verify cp <= candidate.last.
const auto it = std::upper_bound(
intervals, end, cp, [](uint32_t value, const EpdUnicodeInterval& interval) { return value < interval.first; });
// upper_bound: range lookup. Finds the first interval with first > cp, so the
// interval just before it is the last one with first <= cp. That's the only
// candidate that could contain cp. Then we verify cp <= candidate.last.
const auto it = std::upper_bound(
intervals, end, cp, [](uint32_t value, const EpdUnicodeInterval& interval) { return value < interval.first; });
if (it != intervals) {
const auto& interval = *(it - 1);
if (cp <= interval.last) {
return &data->glyph[interval.offset + (cp - interval.first)];
if (it != intervals) {
const auto& interval = *(it - 1);
if (cp <= interval.last) {
return &data->glyph[interval.offset + (cp - interval.first)];
}
}
}
// Codepoint not in interval table — try on-demand loading (SD card fonts).
if (data->glyphMissHandler) {
const EpdGlyph* loaded = data->glyphMissHandler(data->glyphMissCtx, cp);
if (loaded) return loaded;
}
if (cp != REPLACEMENT_GLYPH) {
return getGlyph(REPLACEMENT_GLYPH);
}
+116 -6
View File
@@ -7,10 +7,12 @@
/// Font metrics use "fixed-point 4" (4 fractional bits, i.e. 1/16-pixel
/// resolution). Both the 12.4 glyph advances (uint16_t) and the 4.4 kern
/// values (int8_t) share the same 4 fractional bits, so they can be freely
/// added into a single int32_t accumulator during text layout. The
/// accumulator is snapped to the nearest whole pixel only at render time,
/// which avoids the per-character rounding errors that plagued integer-only
/// layout.
/// added before snapping to whole pixels.
///
/// Rendering and measurement use "differential rounding": each glyph step
/// (previous advance + current kern) is combined in fixed-point and snapped
/// to a pixel as one unit. This guarantees identical character pairs always
/// produce the same pixel spacing, regardless of position on the line.
///
/// The helpers below eliminate the raw bit-shifts that would otherwise be
/// scattered across every layout / measurement call site.
@@ -28,6 +30,98 @@ constexpr int toPixel(int32_t fp) { return static_cast<int>((fp + HALF) >> FRAC_
constexpr float toFloat(int32_t fp) { return fp / static_cast<float>(1 << FRAC_BITS); }
} // namespace fp4
/// Helpers for positioning Unicode combining marks (U+0300 ff.) over a
/// preceding base glyph without GPOS anchor tables.
namespace combiningMark {
constexpr int MIN_GAP_PX = 1;
/// Placement of a mark relative to its base glyph. The default heuristic —
/// centered over the base, raised clear of its top — suits Latin diacritics
/// and Arabic harakat, but misplaces the Hebrew niqqud whose identity depends
/// on position: dagesh sits inside the letter body, the shin/sin dots
/// distinguish the letter by sitting over its right/left arm, and holam hangs
/// over the left corner. "Native" anchors keep the glyph's font-designed
/// height (which may overlap the base) instead of raising it.
enum class Anchor : uint8_t {
CenterRaised, ///< centered over the base, lifted above its top (default)
CenterNative, ///< centered over the base at font-native height
RightNative, ///< right edges aligned, font-native height
LeftNative, ///< left edges aligned, font-native height
};
constexpr Anchor anchorFor(const uint32_t cp) {
switch (cp) {
case 0x05BC: // dagesh / mapiq / shuruk dot: inside the letter body
case 0x05BA: // holam haser for vav: straight above the vav stem
return Anchor::CenterNative;
case 0x05C1: // shin dot: over the letter's right arm
return Anchor::RightNative;
case 0x05B9: // holam: above the letter's left corner
case 0x05C2: // sin dot: over the letter's left arm
return Anchor::LeftNative;
default:
return Anchor::CenterRaised;
}
}
/// Horizontal offset from the base bitmap's left edge to the mark bitmap's
/// left edge for a given anchor.
constexpr int anchorShift(const Anchor anchor, const int baseWidth, const int markWidth) {
switch (anchor) {
case Anchor::LeftNative:
return 0;
case Anchor::RightNative:
return baseWidth - markWidth;
default:
return baseWidth / 2 - markWidth / 2;
}
}
/// Compute the cursor-X at which to render a combining mark so its bitmap
/// lands at its anchor position over the base glyph's bitmap.
constexpr int anchorOver(const Anchor anchor, const int baseCursorPos, const int baseLeft, const int baseWidth,
const int markLeft, const int markWidth) {
return baseCursorPos + baseLeft + anchorShift(anchor, baseWidth, markWidth) - markLeft;
}
/// Rotated-90CW variant of anchorOver. In the rotated coordinate system
/// renderCharImpl uses (cursorY - left) instead of (cursorX + left), so
/// every left/width term inverts sign.
constexpr int anchorOverRotated90CW(const Anchor anchor, const int baseCursorPos, const int baseLeft,
const int baseWidth, const int markLeft, const int markWidth) {
return baseCursorPos - baseLeft - anchorShift(anchor, baseWidth, markWidth) + markLeft;
}
/// For combining marks that sit entirely above the baseline, compute how many
/// pixels to raise the mark so there is at least MIN_GAP_PX between its bottom
/// edge and the top of the base glyph. Returns 0 for marks that extend to or
/// below the baseline (e.g. cedilla, dot-below, ogonek) and for anchors that
/// keep the font-native height (dagesh must stay inside the letter, the
/// shin/sin dots touch its arms).
constexpr int raiseAboveBase(const Anchor anchor, const int markTop, const int markHeight, const int baseTop) {
if (anchor != Anchor::CenterRaised) return 0;
if (markTop - markHeight <= 0) return 0;
const int gap = markTop - markHeight - baseTop;
return (gap < MIN_GAP_PX) ? (MIN_GAP_PX - gap) : 0;
}
} // namespace combiningMark
/// GCC/Clang (the ESP32 firmware toolchain) pack structs with __attribute__((packed)).
/// MSVC (host unit tests) has no equivalent attribute and instead needs a #pragma pack
/// region achieving the same 1-byte alignment. These macros keep the on-disk font layout
/// identical across both toolchains.
#if defined(_MSC_VER)
#define EPD_PACKED_BEGIN __pragma(pack(push, 1))
#define EPD_PACKED_END __pragma(pack(pop))
#define EPD_PACKED_ATTR
#else
#define EPD_PACKED_BEGIN
#define EPD_PACKED_END
#define EPD_PACKED_ATTR __attribute__((packed))
#endif
/// Fixed-point conventions used by EpdGlyph and EpdFontData:
/// advanceX: 12.4 unsigned fixed-point in uint16_t (use fp4::toPixel)
/// kernMatrix: 4.4 signed fixed-point in int8_t (use fp4::toPixel)
@@ -62,17 +156,21 @@ typedef struct {
/// Maps a codepoint to a kerning class ID, sorted by codepoint for binary search.
/// Class IDs are 1-based; codepoints not in the table have implicit class 0 (no kerning).
EPD_PACKED_BEGIN
typedef struct {
uint16_t codepoint; ///< Unicode codepoint
uint8_t classId; ///< 1-based kerning class ID
} __attribute__((packed)) EpdKernClassEntry;
} EPD_PACKED_ATTR EpdKernClassEntry;
EPD_PACKED_END
/// Ligature substitution for a specific glyph pair, sorted by `pair` for binary search.
/// `pair` encodes (leftCodepoint << 16 | rightCodepoint) for single-key lookup.
EPD_PACKED_BEGIN
typedef struct {
uint32_t pair; ///< Packed codepoint pair (left << 16 | right)
uint32_t ligatureCp; ///< Codepoint of the replacement ligature glyph
} __attribute__((packed)) EpdLigaturePair;
} EPD_PACKED_ATTR EpdLigaturePair;
EPD_PACKED_END
/// Data stored for FONT AS A WHOLE
typedef struct {
@@ -96,4 +194,16 @@ typedef struct {
uint8_t kernRightClassCount; ///< Number of distinct right classes (matrix cols)
const EpdLigaturePair* ligaturePairs; ///< Sorted ligature pair table (nullptr if none)
uint32_t ligaturePairCount; ///< Number of entries in ligaturePairs
/// On-demand glyph loading for fonts that don't keep all glyphs in RAM (e.g. SD card fonts).
/// Called by getGlyph() when a codepoint is not found in the interval table.
/// Returns a valid EpdGlyph* with correct metadata, or nullptr to fall back to the
/// replacement glyph. The returned pointer is valid until the next glyphMissHandler
/// call that causes a ring-buffer eviction — callers must consume it (measure or draw)
/// before requesting another missed glyph.
const EpdGlyph* (*glyphMissHandler)(void* ctx, uint32_t codepoint);
/// Context pointer for glyphMissHandler (typically SdCardFont*). Also used by
/// GfxRenderer::getGlyphBitmap() to retrieve overflow bitmaps via SdCardFont.
void* glyphMissCtx;
} EpdFontData;
+1 -1
View File
@@ -1,7 +1,7 @@
#include "EpdFontFamily.h"
const EpdFont* EpdFontFamily::getFont(const Style style) const {
// Extract font style bits (ignore UNDERLINE bit for font selection)
// Extract font style bits; render-time overlay bits do not affect font selection.
const bool hasBold = (style & BOLD) != 0;
const bool hasItalic = (style & ITALIC) != 0;
+18 -1
View File
@@ -3,7 +3,21 @@
class EpdFontFamily {
public:
enum Style : uint8_t { REGULAR = 0, BOLD = 1, ITALIC = 2, BOLD_ITALIC = 3, UNDERLINE = 4 };
// Bitmask of text style flags carried per-word through layout and serialized in page cache.
// Bits 0-1 select the font variant (BOLD/ITALIC); bits 2-5 are decoration/positioning overlays
// applied at render time without changing the underlying font. getFont() ignores all bits
// above bit 1 so decorations compose freely with bold/italic (e.g. BOLD | UNDERLINE | SUP).
enum Style : uint8_t {
REGULAR = 0,
BOLD = 1,
ITALIC = 2,
BOLD_ITALIC = 3,
UNDERLINE = 4, // drawn as a line below baseline by TextBlock::render()
STRIKETHROUGH = 8, // drawn as a line through midline by TextBlock::render()
SUP = 16, // superscript: glyph scaled 50%, raised ~40% of ascender
SUB = 32, // subscript: glyph scaled 50%, lowered ~25% of ascender
};
static constexpr uint8_t TEXT_DECORATION_MASK = static_cast<uint8_t>(UNDERLINE | STRIKETHROUGH);
explicit EpdFontFamily(const EpdFont* regular, const EpdFont* bold = nullptr, const EpdFont* italic = nullptr,
const EpdFont* boldItalic = nullptr)
@@ -14,6 +28,9 @@ class EpdFontFamily {
const EpdGlyph* getGlyph(uint32_t cp, Style style = REGULAR) const;
int8_t getKerning(uint32_t leftCp, uint32_t rightCp, Style style = REGULAR) const;
uint32_t applyLigatures(uint32_t cp, const char*& text, Style style = REGULAR) const;
static constexpr bool hasTextDecoration(const Style style) {
return (static_cast<uint8_t>(style) & TEXT_DECORATION_MASK) != 0;
}
private:
const EpdFont* regular;
+63 -20
View File
@@ -33,12 +33,24 @@ void FontDecompressor::freePageBuffer() {
}
void FontDecompressor::freeHotGroup() {
hotGroup.clear();
hotGroup.shrink_to_fit();
free(hotGroup);
hotGroup = nullptr;
hotGroupCapacity = 0;
hotGroupFont = nullptr;
hotGroupIndex = UINT16_MAX;
hotGlyphBuf.clear();
hotGlyphBuf.shrink_to_fit();
free(hotGlyphBuf);
hotGlyphBuf = nullptr;
hotGlyphBufCapacity = 0;
}
bool FontDecompressor::ensureCapacity(uint8_t*& buf, uint32_t& capacity, uint32_t needed) {
if (capacity >= needed) return true;
// Grow-only, free-then-malloc: every caller fully rewrites the buffer after a grow, so the
// old contents are dead -- freeing first gives the allocator its best shot on a tight heap.
free(buf);
buf = static_cast<uint8_t*>(malloc(needed)); // owned by FontDecompressor, freed in freeHotGroup()
capacity = buf ? needed : 0;
return buf != nullptr;
}
uint16_t FontDecompressor::getGroupIndex(const EpdFontData* fontData, uint32_t glyphIndex) {
@@ -170,24 +182,20 @@ const uint8_t* FontDecompressor::getBitmap(const EpdFontData* fontData, const Ep
}
// Check if hot group already has this group decompressed — if not, decompress it
if (!(!hotGroup.empty() && hotGroupFont == fontData && hotGroupIndex == groupIndex)) {
if (!(hotGroup != nullptr && hotGroupFont == fontData && hotGroupIndex == groupIndex)) {
stats.cacheMisses++;
const EpdFontGroup& group = fontData->groups[groupIndex];
hotGroup.resize(group.uncompressedSize);
if (hotGroup.empty()) {
// ensureCapacity may free the buffer, so the cached-group identity dies with it either way.
hotGroupFont = nullptr;
hotGroupIndex = UINT16_MAX;
if (!ensureCapacity(hotGroup, hotGroupCapacity, group.uncompressedSize)) {
LOG_ERR("FDC", "Failed to allocate %u bytes for hot group %u", group.uncompressedSize, groupIndex);
hotGroupFont = nullptr;
hotGroupIndex = UINT16_MAX;
stats.getBitmapTimeUs += micros() - tStart;
return nullptr;
}
if (!decompressGroup(fontData, groupIndex, hotGroup.data(), group.uncompressedSize)) {
hotGroup.clear();
hotGroup.shrink_to_fit();
hotGroupFont = nullptr;
hotGroupIndex = UINT16_MAX;
if (!decompressGroup(fontData, groupIndex, hotGroup, group.uncompressedSize)) {
stats.getBitmapTimeUs += micros() - tStart;
return nullptr;
}
@@ -200,18 +208,16 @@ const uint8_t* FontDecompressor::getBitmap(const EpdFontData* fontData, const Ep
}
// Compact just the requested glyph from byte-aligned data into scratch buffer
if (glyph->dataLength > hotGlyphBuf.size()) {
hotGlyphBuf.resize(glyph->dataLength);
}
if (hotGlyphBuf.empty()) {
if (!ensureCapacity(hotGlyphBuf, hotGlyphBufCapacity, glyph->dataLength)) {
LOG_ERR("FDC", "Failed to allocate %u bytes for glyph scratch", (unsigned)glyph->dataLength);
stats.getBitmapTimeUs += micros() - tStart;
return nullptr;
}
uint32_t alignedOff = getAlignedOffset(fontData, groupIndex, glyphIndex);
compactSingleGlyph(&hotGroup[alignedOff], hotGlyphBuf.data(), glyph->width, glyph->height);
compactSingleGlyph(&hotGroup[alignedOff], hotGlyphBuf, glyph->width, glyph->height);
stats.getBitmapTimeUs += micros() - tStart;
return hotGlyphBuf.data();
return hotGlyphBuf;
}
// --- Prewarm: pre-decompress glyph bitmaps for a page of text ---
@@ -284,6 +290,43 @@ int FontDecompressor::prewarmCache(const EpdFontData* fontData, const char* utf8
}
}
// Add ligature output glyphs: if both input codepoints of a ligature pair are
// in the needed set, the output glyph will be queried during rendering.
if (fontData->ligaturePairs && fontData->ligaturePairCount > 0) {
for (uint32_t li = 0; li < fontData->ligaturePairCount && glyphCount < MAX_PAGE_GLYPHS; li++) {
uint32_t leftCp = fontData->ligaturePairs[li].pair >> 16;
uint32_t rightCp = fontData->ligaturePairs[li].pair & 0xFFFF;
int32_t leftIdx = findGlyphIndex(fontData, leftCp);
int32_t rightIdx = findGlyphIndex(fontData, rightCp);
if (leftIdx < 0 || rightIdx < 0) continue;
// Check if both inputs are in neededGlyphs
bool hasLeft = false, hasRight = false;
for (uint16_t i = 0; i < glyphCount; i++) {
if (neededGlyphs[i] == static_cast<uint32_t>(leftIdx)) hasLeft = true;
if (neededGlyphs[i] == static_cast<uint32_t>(rightIdx)) hasRight = true;
if (hasLeft && hasRight) break;
}
if (!hasLeft || !hasRight) continue;
int32_t outIdx = findGlyphIndex(fontData, fontData->ligaturePairs[li].ligatureCp);
if (outIdx < 0) continue;
// Deduplicate
bool found = false;
for (uint16_t i = 0; i < glyphCount; i++) {
if (neededGlyphs[i] == static_cast<uint32_t>(outIdx)) {
found = true;
break;
}
}
if (!found) {
neededGlyphs[glyphCount++] = static_cast<uint32_t>(outIdx);
}
}
}
if (glyphCount == 0) return 0;
// Step 2: Compute total buffer size and collect unique groups
+12 -5
View File
@@ -2,8 +2,6 @@
#include <InflateReader.h>
#include <vector>
#include "EpdFontData.h"
class FontDecompressor {
@@ -67,13 +65,22 @@ class FontDecompressor {
// Hot group: last decompressed group (byte-aligned) for non-prewarmed fallback path.
// Kept in byte-aligned format; individual glyphs are compacted on demand into hotGlyphBuf.
// Nothrow high-water malloc buffers, NOT std::vector: getBitmap() runs on the render path,
// and under -fno-exceptions a vector resize that hits OOM abort()s the firmware instead of
// failing (field crash: hotGroup.resize() -> std::bad_alloc -> abort with ~11 KB free).
// ensureCapacity() returns false on OOM so the caller can skip the glyph gracefully.
const EpdFontData* hotGroupFont = nullptr;
uint16_t hotGroupIndex = UINT16_MAX;
std::vector<uint8_t> hotGroup;
uint8_t* hotGroup = nullptr; // owned; freed in freeHotGroup()/dtor
uint32_t hotGroupCapacity = 0;
// Scratch buffer for compacting a single glyph from the hot group.
// Valid until the next getBitmap() call.
std::vector<uint8_t> hotGlyphBuf;
// Valid until the next getBitmap() call. Same ownership/OOM contract as hotGroup.
uint8_t* hotGlyphBuf = nullptr;
uint32_t hotGlyphBufCapacity = 0;
// Grow (never shrink) an owned buffer to at least `needed` bytes; false on OOM, buffer freed.
static bool ensureCapacity(uint8_t*& buf, uint32_t& capacity, uint32_t needed);
void freePageBuffer();
void freeHotGroup();
File diff suppressed because it is too large Load Diff
+268
View File
@@ -0,0 +1,268 @@
#pragma once
#include <cstdint>
#include <string>
#include <vector>
#include "EpdFont.h"
#include "EpdFontData.h"
// On-disk binary format version for .cpfont files. Defined as a preprocessor
// macro (rather than a constexpr) so it can be stringified into the SD-fonts
// release URL — see FONT_MANIFEST_URL in FontDownloadActivity.h. No integer
// suffix because stringification would include it (e.g. `4U` → `"4U"`).
//
// The canonical version for the build tooling lives in
// lib/EpdFont/scripts/cpfont_version.py. This firmware-side copy must be
// bumped manually when the firmware is updated to support a new format.
// Reader enforcement: SdCardFont::load().
#define CPFONT_VERSION 4
class SdCardFont {
public:
static constexpr uint16_t MAX_PAGE_GLYPHS = 512;
static constexpr uint8_t MAX_STYLES = 4;
SdCardFont() = default;
~SdCardFont();
// Owns raw buffers freed in dtor — no shallow-copy semantics. Make any
// accidental pass-by-value or move a compile-time error.
SdCardFont(const SdCardFont&) = delete;
SdCardFont& operator=(const SdCardFont&) = delete;
SdCardFont(SdCardFont&&) = delete;
SdCardFont& operator=(SdCardFont&&) = delete;
// Load .cpfont file: reads header + intervals into RAM, records file layout offsets.
// Supports v4 (multi-style) format.
// Returns true on success.
bool load(const char* path);
// Pre-read glyphs needed for the given UTF-8 text from SD card.
// styleMask: bitmask of styles to prewarm (bit 0=regular, 1=bold, 2=italic, 3=bolditalic).
// Default 0x0F = all present styles.
// When metadataOnly=true, only glyph metrics are loaded (no bitmap data).
// Returns number of glyphs that couldn't be loaded (0 on full success).
int prewarm(const char* utf8Text, uint8_t styleMask = 0x0F, bool metadataOnly = false);
// Build a compact advance-only table for layout measurement.
// Extracts ALL unique codepoints from words (no MAX_PAGE_GLYPHS cap),
// batch-reads advanceX from SD, stores in a sorted per-style table.
// extraText: optional additional codepoints to warm in the same SD pass
// (e.g. shaped Arabic presentation forms the measurement path will look up).
// Returns number of codepoints not found in font coverage.
int buildAdvanceTable(const char* utf8Text, uint8_t styleMask = 0x0F, const char* extraText = nullptr);
int buildAdvanceTable(const std::vector<std::string>& words, bool includeHyphen, uint8_t styleMask = 0x0F,
const char* extraText = nullptr);
// Look up advanceX for a codepoint from the advance table.
// Returns the 12.4 fixed-point advance, or 0 if not found.
uint16_t getAdvance(uint32_t codepoint, uint8_t style) const;
// Returns true if advance table is populated for at least one style.
bool hasAdvanceTable() const;
// Free mini data for all styles and restore stub EpdFontData.
// Preserves the persistent advance cache so repeated layout passes can reuse
// previously fetched metrics.
void clearCache();
// Drop the persistent advance cache. Call when unloading the SD font or
// when font/size/family/glyph-table state changes.
void clearPersistentCache();
// Returns pointer to the managed EpdFont for a given style.
// Returns nullptr if the style is not present.
EpdFont* getEpdFont(uint8_t style = 0);
// Returns true if the given style is present in this font file.
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.
uint8_t styleCount() const { return styleCount_; }
// Returns true if the glyph pointer points into the overflow buffer.
bool isOverflowGlyph(const EpdGlyph* glyph) const;
// Returns the bitmap for an on-demand-loaded (overflow) glyph.
const uint8_t* getOverflowBitmap(const EpdGlyph* glyph) const;
// Extract SdCardFont* from an opaque glyphMissCtx pointer.
// Used by GfxRenderer::getGlyphBitmap() to recover the SdCardFont from EpdFontData::glyphMissCtx.
static SdCardFont* fromMissCtx(void* ctx);
struct Stats {
uint32_t prewarmTotalMs = 0;
uint32_t sdReadTimeMs = 0;
uint32_t seekCount = 0;
uint32_t uniqueGlyphs = 0;
uint32_t bitmapBytes = 0;
};
void logStats(const char* label = "SDCF");
void resetStats();
const Stats& getStats() const { return stats_; }
// Content hash of the file header + style TOC entries (computed during load).
// Used to generate deterministic font IDs for section cache invalidation.
uint32_t contentHash() const { return contentHash_; }
private:
// Per-style metadata (parsed from file header/TOC)
struct CpFontHeader {
uint32_t intervalCount = 0;
uint32_t glyphCount = 0;
uint8_t advanceY = 0;
int16_t ascender = 0;
int16_t descender = 0;
bool is2Bit = false;
uint16_t kernLeftEntryCount = 0;
uint16_t kernRightEntryCount = 0;
uint8_t kernLeftClassCount = 0;
uint8_t kernRightClassCount = 0;
uint8_t ligaturePairCount = 0;
};
// All per-style data: file offsets, intervals, kern/lig, prewarm cache, EpdFont
struct PerStyle {
CpFontHeader header{};
// File layout offsets for this style's data sections
uint32_t intervalsFileOffset = 0;
uint32_t glyphsFileOffset = 0;
uint32_t kernLeftFileOffset = 0;
uint32_t kernRightFileOffset = 0;
uint32_t kernMatrixFileOffset = 0;
uint32_t ligatureFileOffset = 0;
uint32_t bitmapFileOffset = 0;
// Full intervals loaded from file (kept in RAM for codepoint lookup)
EpdUnicodeInterval* fullIntervals = nullptr;
EPD_PACKED_BEGIN
struct BmpInterval16 {
uint16_t first;
uint16_t last;
uint16_t offset;
} EPD_PACKED_ATTR;
EPD_PACKED_END
static_assert(sizeof(BmpInterval16) == 6, "BmpInterval16 must remain compact");
BmpInterval16* bmpIntervals = nullptr;
bool intervalsAreBmp16 = false;
// Persistent kern-class + ligature tables (lazy-loaded on first prewarm).
// The full kern MATRIX is NOT resident — on Literata-class fonts a single
// style's matrix is ~36-42KB contiguous, and 4 styles' worth won't fit
// alongside bitmaps + framebuffer on a 380KB device. Only kernLeftClasses
// and kernRightClasses (small codepoint→classId tables, ~3KB each) stay
// resident; the matrix is reconstructed per-page as miniKernMatrix.
EpdKernClassEntry* kernLeftClasses = nullptr;
EpdKernClassEntry* kernRightClasses = nullptr;
EpdLigaturePair* ligaturePairs = nullptr;
bool kernLigLoaded = false;
// Stub EpdFontData returned when not prewarmed
EpdFontData stubData{};
// Mini EpdFontData built during prewarm
EpdFontData miniData{};
EpdUnicodeInterval* miniIntervals = nullptr;
EpdGlyph* miniGlyphs = nullptr;
uint8_t* miniBitmap = nullptr;
uint32_t miniIntervalCount = 0;
uint32_t miniGlyphCount = 0;
// Per-page mini kern matrix (built by buildMiniKernMatrix on each full
// prewarm). miniKernLeftClasses/miniKernRightClasses map ONLY the codepoints
// used on the current page to renumbered class IDs (1..miniKern*ClassCount).
// miniKernMatrix is a small miniKernLeftClassCount × miniKernRightClassCount
// flat matrix. Typical Latin page: ~25×25 matrix = ~625 bytes per style vs
// ~36KB for the full Literata matrix — ~50× reduction.
EpdKernClassEntry* miniKernLeftClasses = nullptr;
EpdKernClassEntry* miniKernRightClasses = nullptr;
uint16_t miniKernLeftEntryCount = 0;
uint16_t miniKernRightEntryCount = 0;
uint8_t miniKernLeftClassCount = 0;
uint8_t miniKernRightClassCount = 0;
int8_t* miniKernMatrix = nullptr;
// The EpdFont whose data pointer we manage
EpdFont epdFont{&stubData};
bool present = false;
};
PerStyle styles_[MAX_STYLES] = {};
uint8_t styleCount_ = 0;
char filePath_[128] = {};
// Overflow context: glyphMissHandler needs to know which style it's serving
struct OverflowContext {
SdCardFont* self;
uint8_t styleIdx;
};
OverflowContext overflowCtx_[MAX_STYLES] = {};
// Shared on-demand overflow buffer (ring buffer of glyphs loaded via glyphMissHandler)
static constexpr uint32_t OVERFLOW_CAPACITY = 8;
struct OverflowEntry {
EpdGlyph glyph;
uint8_t* bitmap = nullptr;
uint32_t codepoint = 0;
uint8_t styleIdx = 0;
};
OverflowEntry overflow_[OVERFLOW_CAPACITY] = {};
uint32_t overflowCount_ = 0;
uint32_t overflowNext_ = 0;
// Compact advance-only table for layout measurement (per-style).
// Built by buildAdvanceTable(), queried by getAdvance().
struct AdvanceEntry {
uint32_t codepoint;
uint16_t advanceX; // 12.4 fixed-point
};
// Per-style advance table. Sorted by codepoint for binary lookup.
// Bounded to ADVANCE_CACHE_LIMIT entries; persists across layout passes
// (across calls to clearCache()) so repeated indexing of the same font
// amortizes SD reads. Cleared only on font unload or clearPersistentCache().
static constexpr uint32_t ADVANCE_CACHE_LIMIT = 768;
AdvanceEntry* advanceTable_[MAX_STYLES] = {};
uint32_t advanceTableSize_[MAX_STYLES] = {};
bool advanceTableLookup(uint8_t styleIdx, uint32_t codepoint, uint16_t* outAdvance) const;
// Merge sortedNew (sorted by codepoint, no overlap with existing) into the
// advance table for styleIdx, preserving sort order; cap-truncates the tail.
void mergeIntoAdvanceTable(uint8_t styleIdx, const AdvanceEntry* sortedNew, uint32_t newCount);
Stats stats_;
uint32_t contentHash_ = 0;
bool loaded_ = false;
// Per-style helpers
void freeStyleMiniData(PerStyle& s);
void freeStyleAll(PerStyle& s);
void freeStyleKernLigatureData(PerStyle& s);
void freeStyleMiniKern(PerStyle& s);
bool loadStyleKernLigatureData(PerStyle& s);
bool buildMiniKernMatrix(PerStyle& s, const uint32_t* codepoints, uint32_t cpCount);
void applyKernLigaturePointers(PerStyle& s, EpdFontData& data) const;
void applyGlyphMissCallback(uint8_t styleIdx);
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,
const char* extraText = nullptr);
int prewarmStyle(uint8_t styleIdx, const uint32_t* codepoints, uint32_t cpCount, bool metadataOnly);
// Global helpers
void freeAll();
void clearOverflow();
static void computeStyleFileOffsets(PerStyle& s, uint32_t baseOffset);
// Static callback for EpdFontData::glyphMissHandler (per-style via OverflowContext)
static const EpdGlyph* onGlyphMiss(void* ctx, uint32_t codepoint);
};
+94
View File
@@ -0,0 +1,94 @@
#include "SdCardFontManager.h"
#include <EpdFontFamily.h>
#include <GfxRenderer.h>
#include <Logging.h>
#include <SdCardFont.h>
#include <SdCardFontRegistry.h>
SdCardFontManager::~SdCardFontManager() {
for (auto& lf : loaded_) {
delete lf.font;
}
}
// FNV-1a continuation: seeds with contentHash, then hashes family name + point size.
// Produces a deterministic ID that is stable across load/unload cycles and reboots,
// and changes when font content changes (different header/TOC = different contentHash).
int SdCardFontManager::computeFontId(uint32_t contentHash, const char* familyName, uint8_t pointSize) {
static constexpr uint32_t FNV_PRIME = 16777619u;
uint32_t hash = contentHash;
while (*familyName) {
hash ^= static_cast<uint8_t>(*familyName++);
hash *= FNV_PRIME;
}
hash ^= pointSize;
hash *= FNV_PRIME;
int id = static_cast<int>(hash);
return id != 0 ? id : 1; // 0 is reserved as "not found" sentinel
}
bool SdCardFontManager::loadFamily(const SdCardFontFamilyInfo& family, GfxRenderer& renderer, uint8_t fontSizeEnum) {
// Unload any previously loaded family first
if (!loadedFamilyName_.empty()) {
unloadAll(renderer);
}
// Select the physical point size closest to the built-in reader sizes. Some
// CJK font packs only ship larger sizes, so ordinal selection can make
// MEDIUM load 18pt+ and produce oversized pages on small devices.
const SdCardFontFileInfo* selected = family.findClosestReaderSize(fontSizeEnum);
if (!selected) {
LOG_ERR("SDMGR", "Family %s has no files to load", family.name.c_str());
return false;
}
auto* font = new (std::nothrow) SdCardFont();
if (!font) {
LOG_ERR("SDMGR", "Failed to allocate SdCardFont for %s", selected->path.c_str());
return false;
}
if (!font->load(selected->path.c_str())) {
LOG_ERR("SDMGR", "Failed to load %s", selected->path.c_str());
delete font;
return false;
}
int fontId = computeFontId(font->contentHash(), family.name.c_str(), selected->pointSize);
// Guard against collision with built-in font IDs (astronomically unlikely
// with FNV-1a hashes, but provides a safety net)
if (renderer.getFontMap().count(fontId) != 0) {
LOG_ERR("SDMGR", "Font ID %d collides with existing font, skipping %s", fontId, selected->path.c_str());
delete font;
return false;
}
renderer.registerSdCardFont(fontId, font);
loaded_.push_back({font, fontId, selected->pointSize});
LOG_DBG("SDMGR", "Loaded %s size=%u id=%d styles=%u (sizeEnum=%u)", selected->path.c_str(), selected->pointSize,
fontId, font->styleCount(), fontSizeEnum);
EpdFontFamily fontFamily(font->getEpdFont(0), font->getEpdFont(1), font->getEpdFont(2), font->getEpdFont(3));
renderer.insertFont(fontId, fontFamily);
loadedFamilyName_ = family.name;
loadedPointSize_ = selected->pointSize;
return true;
}
void SdCardFontManager::unloadAll(GfxRenderer& renderer) {
renderer.clearSdCardFonts();
for (auto& lf : loaded_) {
renderer.removeFont(lf.fontId);
delete lf.font;
}
loaded_.clear();
loadedFamilyName_.clear();
loadedPointSize_ = 0;
}
int SdCardFontManager::getFontId(const std::string& familyName) const {
if (familyName != loadedFamilyName_ || loaded_.empty()) return 0;
return loaded_.front().fontId;
}
+50
View File
@@ -0,0 +1,50 @@
#pragma once
#include <cstdint>
#include <string>
#include <vector>
class GfxRenderer;
class SdCardFont;
struct SdCardFontFamilyInfo;
class SdCardFontManager {
public:
SdCardFontManager() = default;
~SdCardFontManager();
SdCardFontManager(const SdCardFontManager&) = delete;
SdCardFontManager& operator=(const SdCardFontManager&) = delete;
// Load the font file whose physical point size is closest to the reader
// fontSizeEnum (SMALL=12, MEDIUM=14, LARGE=16, EXTRA_LARGE=18). Only one
// .cpfont file is loaded; other sizes remain on disk. This keeps resident
// interval + kern/ligature tables to one size's worth of memory.
// Returns true on success.
bool loadFamily(const SdCardFontFamilyInfo& family, GfxRenderer& renderer, uint8_t fontSizeEnum);
// Unload everything, unregister from renderer.
void unloadAll(GfxRenderer& renderer);
// Look up the font ID for the loaded family. Returns 0 if nothing loaded
// or familyName doesn't match.
int getFontId(const std::string& familyName) const;
// Get name of currently loaded family (empty if none).
const std::string& currentFamilyName() const { return loadedFamilyName_; };
// Point size that was actually loaded.
// 0 if nothing loaded.
uint8_t currentPointSize() const { return loadedPointSize_; };
private:
struct LoadedFont {
SdCardFont* font; // heap-allocated, owned
int fontId;
uint8_t size;
};
static int computeFontId(uint32_t contentHash, const char* familyName, uint8_t pointSize);
std::string loadedFamilyName_;
uint8_t loadedPointSize_ = 0;
std::vector<LoadedFont> loaded_;
};
+284
View File
@@ -0,0 +1,284 @@
#include "SdCardFontRegistry.h"
#include <HalStorage.h>
#include <Logging.h>
#include <algorithm>
#include <cstring>
// --- SdCardFontFamilyInfo helpers ---
const SdCardFontFileInfo* SdCardFontFamilyInfo::findFile(uint8_t size, uint8_t style) const {
for (const auto& f : files) {
if (f.pointSize == size && f.style == style) return &f;
}
return nullptr;
}
const SdCardFontFileInfo* SdCardFontFamilyInfo::findClosestReaderSize(const uint8_t fontSizeEnum,
const uint8_t style) const {
if (files.empty()) return nullptr;
// Collect sizes matching the requested style, sorted ascending.
std::vector<uint8_t> sizes;
for (const auto& f : files) {
if (f.style != style) continue;
sizes.push_back(f.pointSize);
}
if (sizes.empty()) return nullptr;
std::sort(sizes.begin(), sizes.end());
// When the family provides at least 4 sizes, use ordinal (index-based)
// selection so custom-built font sets (e.g. 10/12/14/16) map SMALL to
// the smallest file, not to a hardcoded 12pt target.
if (sizes.size() >= 4) {
uint8_t idx = fontSizeEnum;
if (idx >= sizes.size()) idx = sizes.size() - 1;
return findFile(sizes[idx], style);
}
// Fewer sizes than enum slots (e.g. CJK packs with only 2-3 sizes):
// fall back to closest-match against the built-in reader targets.
uint8_t target = 14;
switch (fontSizeEnum) {
case 0:
target = 12;
break;
case 2:
target = 16;
break;
case 3:
target = 18;
break;
case 1:
default:
target = 14;
break;
}
const SdCardFontFileInfo* best = nullptr;
uint8_t bestDelta = 255;
for (const auto& f : files) {
if (f.style != style) continue;
const uint8_t delta = f.pointSize > target ? f.pointSize - target : target - f.pointSize;
if (!best || delta < bestDelta || (delta == bestDelta && f.pointSize < best->pointSize)) {
best = &f;
bestDelta = delta;
}
}
return best;
}
bool SdCardFontFamilyInfo::hasSize(uint8_t size) const {
for (const auto& f : files) {
if (f.pointSize == size) return true;
}
return false;
}
std::vector<uint8_t> SdCardFontFamilyInfo::availableSizes() const {
std::vector<uint8_t> sizes;
for (const auto& f : files) {
bool found = false;
for (uint8_t s : sizes) {
if (s == f.pointSize) {
found = true;
break;
}
}
if (!found) sizes.push_back(f.pointSize);
}
std::sort(sizes.begin(), sizes.end());
return sizes;
}
// --- SdCardFontRegistry ---
bool SdCardFontRegistry::parseFilename(const char* filename, uint8_t& size, uint8_t& style) {
// V4 naming: <name>_<size>.cpfont (e.g. Bookerly-SD_14.cpfont)
// Use an ends-with check rather than strstr() so that in-progress downloads
// like "Foo_14.cpfont.tmp" or backups like "Foo_14.cpfont~" aren't accepted.
static constexpr char kExt[] = ".cpfont";
static constexpr size_t kExtLen = sizeof(kExt) - 1;
const size_t nameLen = strlen(filename);
if (nameLen <= kExtLen) return false;
if (strcmp(filename + nameLen - kExtLen, kExt) != 0) return false;
const char* ext = filename + nameLen - kExtLen;
size_t baseLen = ext - filename;
if (baseLen == 0 || baseLen > 127) return false;
char base[128];
memcpy(base, filename, baseLen);
base[baseLen] = '\0';
char* lastUnderscore = strrchr(base, '_');
if (!lastUnderscore || lastUnderscore == base) return false;
const char* sizeStr = lastUnderscore + 1;
char* endPtr;
long sizeVal = strtol(sizeStr, &endPtr, 10);
if (endPtr == sizeStr || *endPtr != '\0' || sizeVal < 1 || sizeVal > 255) return false;
size = static_cast<uint8_t>(sizeVal);
// V4 .cpfont files bundle every style (regular/bold/italic/bold-italic) into
// one file, so style is always 0 at the registry level. The per-style
// bitstream is selected later by SdCardFont::getEpdFont(style). The `style`
// field in SdCardFontFileInfo is reserved for future formats that split
// styles across files; scanDirectory() defends against accidental
// (pointSize, style) collisions in that scenario.
style = 0;
return true;
}
void SdCardFontRegistry::scanDirectory(const char* dirPath, SdCardFontFamilyInfo& family) {
HalFile dir = Storage.open(dirPath);
if (!dir || !dir.isDirectory()) return;
char nameBuffer[128];
while (true) {
HalFile entry = dir.openNextFile();
if (!entry) break;
if (entry.isDirectory()) {
entry.close();
continue;
}
entry.getName(nameBuffer, sizeof(nameBuffer));
entry.close();
// Skip macOS resource fork files (._*) and other hidden files
if (nameBuffer[0] == '.' || nameBuffer[0] == '_') continue;
uint8_t size, style;
if (!parseFilename(nameBuffer, size, style)) continue;
// Reject duplicate (pointSize, style) entries in the same family. With
// v4's bundle-everything design parseFilename always returns style=0, so
// two files at the same size in the same family would silently shadow
// each other in findFile(). Skip the duplicate and warn.
bool duplicate = false;
for (const auto& existing : family.files) {
if (existing.pointSize == size && existing.style == style) {
duplicate = true;
break;
}
}
if (duplicate) {
LOG_ERR("SDREG", "Duplicate font %s in %s — skipping", nameBuffer, dirPath);
continue;
}
SdCardFontFileInfo info;
info.path = std::string(dirPath) + "/" + nameBuffer;
info.pointSize = size;
info.style = style;
family.files.push_back(std::move(info));
}
}
// Scan a single root (e.g. "/.fonts") and append its families to `out`.
// Skips families whose names already exist in `out` (de-duplicates between
// the hidden and visible roots — first scan wins).
void SdCardFontRegistry::scanRoot(const char* rootPath, std::vector<SdCardFontFamilyInfo>& out) {
HalFile root = Storage.open(rootPath);
if (!root) {
LOG_DBG("SDREG", "Fonts directory not found: %s", rootPath);
return;
}
if (!root.isDirectory()) {
LOG_ERR("SDREG", "Fonts path is not a directory: %s", rootPath);
return;
}
char nameBuffer[128];
while (true) {
HalFile entry = root.openNextFile();
if (!entry) break;
if (entry.isDirectory()) {
entry.getName(nameBuffer, sizeof(nameBuffer));
entry.close();
// Skip hidden/system directories inside the root (macOS ._*, .Trashes, etc.)
if (nameBuffer[0] == '.' || nameBuffer[0] == '_') continue;
// De-dup by family name across roots.
bool exists = false;
for (const auto& fam : out) {
if (fam.name == nameBuffer) {
exists = true;
break;
}
}
if (exists) continue;
SdCardFontFamilyInfo family;
family.name = nameBuffer;
std::string subDirPath = std::string(rootPath) + "/" + nameBuffer;
SdCardFontRegistry::scanDirectory(subDirPath.c_str(), family);
if (!family.files.empty()) {
out.push_back(std::move(family));
LOG_DBG("SDREG", "Found family: %s (%d files) in %s", out.back().name.c_str(),
static_cast<int>(out.back().files.size()), rootPath);
}
} else {
entry.close();
}
}
}
bool SdCardFontRegistry::discover() {
families_.clear();
families_.reserve(MAX_SD_FAMILIES);
// Hidden root is scanned first so it wins on name collisions, matching the
// sleep-folder pattern (/.sleep preferred over /sleep).
scanRoot(FONTS_DIR_HIDDEN, families_);
scanRoot(FONTS_DIR_VISIBLE, families_);
// Sort families alphabetically
std::sort(families_.begin(), families_.end(),
[](const SdCardFontFamilyInfo& a, const SdCardFontFamilyInfo& b) { return a.name < b.name; });
// Cap at MAX_SD_FAMILIES
if (static_cast<int>(families_.size()) > MAX_SD_FAMILIES) {
families_.resize(MAX_SD_FAMILIES);
}
LOG_DBG("SDREG", "Discovery complete: %d families", static_cast<int>(families_.size()));
return !families_.empty();
}
const char* SdCardFontRegistry::findFamilyRoot(const char* familyName) {
if (!familyName || !*familyName) return nullptr;
char path[160];
snprintf(path, sizeof(path), "%s/%s", FONTS_DIR_HIDDEN, familyName);
if (Storage.exists(path)) return FONTS_DIR_HIDDEN;
snprintf(path, sizeof(path), "%s/%s", FONTS_DIR_VISIBLE, familyName);
if (Storage.exists(path)) return FONTS_DIR_VISIBLE;
return nullptr;
}
const char* SdCardFontRegistry::defaultWriteRoot() {
// If exactly one of the roots already exists, keep using it. Otherwise
// (neither exists, or both exist) prefer the hidden root for new installs.
bool hiddenExists = Storage.exists(FONTS_DIR_HIDDEN);
bool visibleExists = Storage.exists(FONTS_DIR_VISIBLE);
if (hiddenExists) return FONTS_DIR_HIDDEN;
if (visibleExists) return FONTS_DIR_VISIBLE;
return FONTS_DIR_HIDDEN;
}
const SdCardFontFamilyInfo* SdCardFontRegistry::findFamily(const std::string& name) const {
for (const auto& f : families_) {
if (f.name == name) return &f;
}
return nullptr;
}
int SdCardFontRegistry::getFamilyIndex(const std::string& name) const {
for (int i = 0; i < static_cast<int>(families_.size()); i++) {
if (families_[i].name == name) return i;
}
return -1;
}
+59
View File
@@ -0,0 +1,59 @@
#pragma once
#include <cstdint>
#include <string>
#include <vector>
struct SdCardFontFileInfo {
std::string path; // v4 on-disk naming: "/<root>/<Family>/<Family>_<size>.cpfont"
// where <root> is "/.fonts" (preferred, hidden) or "/fonts" (visible).
// e.g. "/.fonts/NotoSansCJK/NotoSansCJK_14.cpfont"
uint8_t pointSize; // parsed from filename: 14
uint8_t style; // always 0 in v4 (all 4 styles bundled in one file);
// kept for potential future formats
};
struct SdCardFontFamilyInfo {
std::string name; // directory name, e.g. "NotoSansCJK"
std::vector<SdCardFontFileInfo> files;
const SdCardFontFileInfo* findFile(uint8_t size, uint8_t style = 0) const;
const SdCardFontFileInfo* findClosestReaderSize(uint8_t fontSizeEnum, uint8_t style = 0) const;
bool hasSize(uint8_t size) const;
std::vector<uint8_t> availableSizes() const;
};
class SdCardFontRegistry {
public:
static constexpr int MAX_SD_FAMILIES = 128;
// Two top-level roots are scanned at discovery time. Hidden is preferred
// when creating new installs; both are read from if present.
static constexpr const char* FONTS_DIR_HIDDEN = "/.fonts";
static constexpr const char* FONTS_DIR_VISIBLE = "/fonts";
// Returns the existing root for `familyName` (the one that contains
// /<root>/<familyName>/), or nullptr if the family is not installed in
// either root. Used by writers to keep re-installs in their existing dir.
static const char* findFamilyRoot(const char* familyName);
// Returns the root path that should be used when creating a brand-new
// family on disk (no prior install): the existing root if exactly one of
// the two roots exists, otherwise the hidden root.
static const char* defaultWriteRoot();
// Scan SD card, populate families_. Returns true if any families found.
bool discover();
const std::vector<SdCardFontFamilyInfo>& getFamilies() const { return families_; }
const SdCardFontFamilyInfo* findFamily(const std::string& name) const;
int getFamilyIndex(const std::string& name) const;
int getFamilyCount() const { return static_cast<int>(families_.size()); }
private:
std::vector<SdCardFontFamilyInfo> families_; // sorted alphabetically
static bool parseFilename(const char* filename, uint8_t& size, uint8_t& style);
static void scanDirectory(const char* dirPath, SdCardFontFamilyInfo& family);
// Scan one root (e.g. "/.fonts"), append families to `out`, dedup by name.
static void scanRoot(const char* rootPath, std::vector<SdCardFontFamilyInfo>& out);
};
+16 -32
View File
@@ -1,21 +1,21 @@
#pragma once
#include <builtinFonts/bookerly_12_bold.h>
#include <builtinFonts/bookerly_12_bolditalic.h>
#include <builtinFonts/bookerly_12_italic.h>
#include <builtinFonts/bookerly_12_regular.h>
#include <builtinFonts/bookerly_14_bold.h>
#include <builtinFonts/bookerly_14_bolditalic.h>
#include <builtinFonts/bookerly_14_italic.h>
#include <builtinFonts/bookerly_14_regular.h>
#include <builtinFonts/bookerly_16_bold.h>
#include <builtinFonts/bookerly_16_bolditalic.h>
#include <builtinFonts/bookerly_16_italic.h>
#include <builtinFonts/bookerly_16_regular.h>
#include <builtinFonts/bookerly_18_bold.h>
#include <builtinFonts/bookerly_18_bolditalic.h>
#include <builtinFonts/bookerly_18_italic.h>
#include <builtinFonts/bookerly_18_regular.h>
#include <builtinFonts/notoserif_12_bold.h>
#include <builtinFonts/notoserif_12_bolditalic.h>
#include <builtinFonts/notoserif_12_italic.h>
#include <builtinFonts/notoserif_12_regular.h>
#include <builtinFonts/notoserif_14_bold.h>
#include <builtinFonts/notoserif_14_bolditalic.h>
#include <builtinFonts/notoserif_14_italic.h>
#include <builtinFonts/notoserif_14_regular.h>
#include <builtinFonts/notoserif_16_bold.h>
#include <builtinFonts/notoserif_16_bolditalic.h>
#include <builtinFonts/notoserif_16_italic.h>
#include <builtinFonts/notoserif_16_regular.h>
#include <builtinFonts/notoserif_18_bold.h>
#include <builtinFonts/notoserif_18_bolditalic.h>
#include <builtinFonts/notoserif_18_italic.h>
#include <builtinFonts/notoserif_18_regular.h>
#include <builtinFonts/notosans_8_regular.h>
#include <builtinFonts/notosans_12_bold.h>
#include <builtinFonts/notosans_12_bolditalic.h>
@@ -33,22 +33,6 @@
#include <builtinFonts/notosans_18_bolditalic.h>
#include <builtinFonts/notosans_18_italic.h>
#include <builtinFonts/notosans_18_regular.h>
#include <builtinFonts/opendyslexic_10_bold.h>
#include <builtinFonts/opendyslexic_10_bolditalic.h>
#include <builtinFonts/opendyslexic_10_italic.h>
#include <builtinFonts/opendyslexic_10_regular.h>
#include <builtinFonts/opendyslexic_12_bold.h>
#include <builtinFonts/opendyslexic_12_bolditalic.h>
#include <builtinFonts/opendyslexic_12_italic.h>
#include <builtinFonts/opendyslexic_12_regular.h>
#include <builtinFonts/opendyslexic_14_bold.h>
#include <builtinFonts/opendyslexic_14_bolditalic.h>
#include <builtinFonts/opendyslexic_14_italic.h>
#include <builtinFonts/opendyslexic_14_regular.h>
#include <builtinFonts/opendyslexic_8_bold.h>
#include <builtinFonts/opendyslexic_8_bolditalic.h>
#include <builtinFonts/opendyslexic_8_italic.h>
#include <builtinFonts/opendyslexic_8_regular.h>
#include <builtinFonts/ubuntu_10_bold.h>
#include <builtinFonts/ubuntu_10_regular.h>
#include <builtinFonts/ubuntu_12_bold.h>
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+308 -308
View File
@@ -3,239 +3,239 @@
* name: notosans_16_regular
* size: 16
* mode: 2-bit compressed: true
* Command used: fontconvert.py notosans_16_regular 16 ../builtinFonts/source/NotoSans/NotoSans-Regular.ttf --2bit --compress
* Command used: fontconvert.py notosans_16_regular 16 ../builtinFonts/source/NotoSans/NotoSans-Regular.ttf --2bit --compress --pnum
*/
#pragma once
#include "EpdFontData.h"
static const uint8_t notosans_16_regularBitmaps[35113] = {
0xBD, 0x5A, 0x3D, 0x8C, 0xE4, 0xC8, 0x75, 0x2E, 0x92, 0xBB, 0x4B, 0x40, 0x84, 0xC1, 0x0B, 0xB4,
0xB2, 0xA0, 0x0D, 0x98, 0x5C, 0x24, 0xC1, 0xC7, 0x54, 0xC1, 0x0E, 0x8B, 0x82, 0xA1, 0x7C, 0x83,
0x9D, 0x5B, 0xC0, 0x1B, 0x9C, 0x53, 0x1D, 0x20, 0x4C, 0xA2, 0xC0, 0xC1, 0xCC, 0x90, 0xF6, 0x05,
0x0A, 0x14, 0x68, 0x01, 0xE1, 0x0C, 0x03, 0x97, 0x28, 0x38, 0x19, 0x72, 0x6F, 0xB0, 0x89, 0x03,
0x09, 0xE8, 0x69, 0x12, 0x36, 0xE0, 0xC4, 0xC1, 0xD1, 0x06, 0x04, 0x87, 0xA2, 0xE0, 0xDB, 0x40,
0xE7, 0xE0, 0x08, 0x9B, 0x07, 0xB3, 0x57, 0x45, 0x96, 0xBF, 0xEF, 0x15, 0xD9, 0xDD, 0xB3, 0x3B,
0xFB, 0xAB, 0x85, 0x39, 0xAC, 0xC7, 0x6E, 0x76, 0xFD, 0xBC, 0x7A, 0xFF, 0xEF, 0xD5, 0x68, 0xA5,
0xAF, 0xF8, 0xCB, 0xD4, 0x7B, 0xF3, 0xDF, 0x9F, 0xA9, 0x6B, 0x8A, 0x97, 0x56, 0x55, 0x5E, 0x95,
0x05, 0xBE, 0xAD, 0xA3, 0x66, 0xBE, 0xC3, 0x1A, 0xF7, 0x29, 0xEE, 0x80, 0xF7, 0x77, 0x83, 0x52,
0xA9, 0x7F, 0xF4, 0x5A, 0x74, 0xEE, 0x03, 0xC2, 0x2E, 0x68, 0x94, 0xF2, 0xBA, 0xA0, 0x56, 0xCA,
0xEF, 0x42, 0xC2, 0x36, 0x02, 0x0C, 0x9A, 0xA8, 0x54, 0x5F, 0x7B, 0xF8, 0xE4, 0xD1, 0x17, 0x79,
0x6C, 0x71, 0x95, 0xD7, 0xEF, 0xFF, 0xE6, 0xE4, 0x38, 0x57, 0x51, 0x99, 0x62, 0x50, 0x9C, 0x67,
0x02, 0x8F, 0x00, 0x13, 0x75, 0x1B, 0x30, 0x55, 0xA7, 0x58, 0x1E, 0x1D, 0xA7, 0x19, 0xAA, 0x23,
0xD5, 0xE3, 0xFD, 0x6D, 0xD5, 0x01, 0x9E, 0x7A, 0x84, 0x6B, 0x9F, 0x70, 0xF0, 0xDD, 0xE2, 0xCD,
0xB2, 0xB8, 0xF2, 0xD0, 0xB0, 0x9A, 0xB4, 0x5B, 0x0F, 0x54, 0x68, 0x6D, 0x93, 0x58, 0xDB, 0x15,
0x9F, 0xFC, 0x6D, 0x77, 0x81, 0x77, 0x66, 0x6E, 0x23, 0x5A, 0xF1, 0x93, 0x52, 0xA5, 0xB6, 0x44,
0x9F, 0x41, 0xA9, 0x02, 0x1F, 0x22, 0xDB, 0xA9, 0xE8, 0xC7, 0x03, 0xC6, 0x1A, 0xB4, 0x35, 0x9F,
0x79, 0xF4, 0xD7, 0x66, 0xBA, 0xFF, 0xA8, 0x07, 0x1A, 0x4D, 0x41, 0x4C, 0x6E, 0x95, 0xFB, 0xF9,
0xD9, 0xDC, 0xE5, 0x4F, 0xDC, 0x81, 0x8A, 0x6D, 0x0D, 0xE4, 0x55, 0xFA, 0xE7, 0x2D, 0x30, 0x56,
0x19, 0x10, 0x5D, 0x13, 0xF7, 0x8E, 0xD8, 0x9F, 0x02, 0xCD, 0x4E, 0x1E, 0x24, 0x0C, 0x1E, 0x71,
0x29, 0x8F, 0x44, 0x36, 0xD6, 0x67, 0x37, 0x1E, 0xA8, 0x23, 0xAF, 0x5B, 0x6B, 0xA3, 0xDE, 0xF3,
0xBB, 0x7E, 0x53, 0xA9, 0xE4, 0x93, 0x7F, 0xEF, 0xFA, 0x38, 0x8F, 0xEC, 0x47, 0xFF, 0xD6, 0xC5,
0xA5, 0xF7, 0xEB, 0xF8, 0x9F, 0x3A, 0x12, 0x33, 0xF1, 0x5B, 0x4E, 0x90, 0x05, 0xF2, 0x38, 0x75,
0x8F, 0x41, 0x5E, 0xFA, 0x9D, 0xCF, 0x2E, 0x41, 0xE3, 0x61, 0x80, 0xC2, 0xA7, 0x21, 0xC9, 0x89,
0x56, 0x31, 0x91, 0x9B, 0x2A, 0x19, 0x04, 0x57, 0x2F, 0x5F, 0xB0, 0x4E, 0x2C, 0x3F, 0x56, 0x16,
0x34, 0xF4, 0xA6, 0xFB, 0xC0, 0x31, 0xE8, 0x3D, 0xF4, 0x01, 0x9A, 0x66, 0x0F, 0xF9, 0xC6, 0x1B,
0x02, 0xD2, 0x7F, 0x4C, 0x48, 0x6D, 0x5D, 0x71, 0xDB, 0xC9, 0xC8, 0x19, 0x0A, 0xBE, 0xF6, 0xED,
0xA8, 0x82, 0x36, 0xEA, 0x8B, 0x32, 0x6C, 0x92, 0x3A, 0x69, 0xE3, 0x5A, 0xE7, 0xE1, 0x00, 0x88,
0x59, 0x75, 0xAE, 0x49, 0x5B, 0x59, 0xDE, 0x10, 0x46, 0x46, 0xA5, 0xB5, 0xD2, 0x93, 0x8A, 0xB7,
0xF7, 0x2C, 0x46, 0x58, 0x3B, 0xC4, 0x2D, 0x70, 0x28, 0x03, 0xA3, 0xBC, 0x3B, 0x64, 0x2C, 0xAF,
0xD3, 0xD3, 0xD3, 0xEF, 0x42, 0x24, 0xD7, 0x58, 0xB8, 0x0B, 0x9B, 0xB8, 0x4E, 0xCA, 0x34, 0xA7,
0x94, 0x9E, 0xE3, 0xEF, 0xE2, 0xE0, 0x8F, 0xDF, 0xF9, 0x3E, 0x2D, 0xE3, 0x3A, 0x6E, 0xC2, 0xD6,
0x27, 0xD2, 0x67, 0xEA, 0xDA, 0x1A, 0xEF, 0x92, 0x3C, 0xAE, 0xC3, 0x36, 0xE8, 0x7C, 0x6E, 0xCA,
0xE0, 0x6F, 0x19, 0xB0, 0xFC, 0x5D, 0xA8, 0x91, 0xEF, 0x7D, 0xAE, 0xD1, 0x45, 0x4D, 0x5C, 0x6A,
0x0C, 0x7D, 0xC7, 0x11, 0x28, 0x6C, 0x5D, 0xA3, 0x4C, 0xA1, 0xBD, 0x1B, 0xD4, 0xD7, 0x8B, 0x9F,
0xFD, 0xCB, 0x54, 0x41, 0x0E, 0xBE, 0x55, 0x4D, 0xBF, 0x50, 0x31, 0x36, 0xAE, 0xC1, 0xDC, 0x0B,
0x6C, 0xD2, 0xA4, 0x65, 0xD0, 0xC7, 0x8D, 0xD0, 0xFE, 0x1D, 0x8F, 0x42, 0xAF, 0x5E, 0x02, 0x0A,
0x4C, 0x93, 0x0B, 0xB8, 0x79, 0xEF, 0xAB, 0xE3, 0x97, 0x8D, 0x50, 0xC0, 0x27, 0x26, 0x11, 0x6A,
0x12, 0x21, 0x83, 0x40, 0x9D, 0x12, 0x4D, 0x72, 0x2E, 0xC7, 0x7D, 0x7C, 0x7C, 0x49, 0x59, 0x15,
0x54, 0x58, 0x61, 0x77, 0x8A, 0xB2, 0x47, 0x16, 0x7A, 0xBD, 0x30, 0x53, 0x51, 0x43, 0x23, 0x6C,
0x27, 0xA6, 0x10, 0x95, 0xD4, 0x32, 0x91, 0x0A, 0xEC, 0x59, 0xBA, 0x29, 0xF0, 0xD9, 0x9F, 0xBB,
0x86, 0x2F, 0xE8, 0xEA, 0xAE, 0x14, 0x22, 0xE5, 0x59, 0x88, 0x7B, 0x30, 0x7E, 0xD0, 0xA9, 0xB8,
0xF5, 0x07, 0x76, 0x06, 0x6F, 0x73, 0xE1, 0x88, 0x30, 0xC5, 0x81, 0x94, 0xB8, 0xA4, 0xF9, 0x73,
0x41, 0x46, 0xB0, 0x1F, 0x21, 0x13, 0x40, 0x7A, 0x2F, 0xB0, 0x65, 0xCF, 0xA8, 0x60, 0x7B, 0xAB,
0xE7, 0x42, 0x40, 0x26, 0xA5, 0xB4, 0x8A, 0x88, 0x80, 0x2A, 0x49, 0xA7, 0xAA, 0x2E, 0xB0, 0x5D,
0xFC, 0x9F, 0x9D, 0xFE, 0xA8, 0xFB, 0x13, 0x20, 0xFD, 0x06, 0xB7, 0x5C, 0x54, 0x8E, 0x88, 0x0B,
0xE8, 0xED, 0x49, 0x0F, 0xA1, 0xF1, 0x0D, 0x89, 0x48, 0x82, 0x00, 0x87, 0x4B, 0x80, 0xEF, 0xA8,
0x0D, 0x6E, 0x68, 0x44, 0xD6, 0x40, 0x94, 0x31, 0x03, 0xDF, 0xF3, 0xC7, 0x90, 0xEF, 0x93, 0xE7,
0xBD, 0xFA, 0x62, 0xB5, 0x3A, 0x60, 0xBC, 0x53, 0xC5, 0x6A, 0xA2, 0x16, 0x42, 0x01, 0xF5, 0x63,
0x2C, 0xFE, 0x9E, 0xF2, 0x65, 0xDD, 0xF1, 0x8A, 0x75, 0x09, 0x22, 0x4C, 0xF6, 0x61, 0x85, 0xC9,
0x2C, 0x7B, 0x70, 0x18, 0xEC, 0x91, 0x8C, 0x98, 0xD4, 0xBC, 0xA8, 0xB0, 0xE9, 0x00, 0x14, 0xEA,
0x8E, 0xF2, 0x46, 0xB5, 0xC9, 0xD1, 0xAF, 0xE2, 0x88, 0xC4, 0x4E, 0x58, 0xFB, 0xA6, 0xAC, 0x2F,
0x93, 0x06, 0x04, 0x31, 0x41, 0x4A, 0x70, 0x44, 0x60, 0x0C, 0x0D, 0xB8, 0xA1, 0x20, 0x18, 0x4A,
0x81, 0x21, 0x1B, 0x0D, 0xF9, 0x6F, 0x28, 0x27, 0xC6, 0x19, 0x87, 0xB0, 0x01, 0x00, 0xAB, 0x44,
0xAF, 0x8D, 0xEC, 0xAD, 0x17, 0xF0, 0xED, 0xD5, 0x6A, 0xFA, 0xF9, 0x8C, 0xF8, 0xB3, 0x00, 0xFB,
0x6D, 0x52, 0xA0, 0x92, 0xF2, 0x39, 0xE3, 0xB9, 0xB4, 0x6C, 0x69, 0x1F, 0x3E, 0x50, 0x85, 0x33,
0xFF, 0xCD, 0x35, 0x32, 0x5B, 0x05, 0xC3, 0x8C, 0xEF, 0x53, 0xCD, 0x1F, 0x72, 0x10, 0x79, 0xFB,
0xBD, 0xA2, 0x21, 0x55, 0x53, 0x9A, 0xF2, 0x9B, 0x8B, 0x90, 0x7A, 0xFC, 0x96, 0x10, 0x54, 0x8F,
0xEF, 0x2A, 0x7F, 0xDC, 0xB1, 0x8E, 0x8E, 0x82, 0xB2, 0x0D, 0x76, 0x2B, 0x27, 0xDD, 0x4A, 0xDF,
0x00, 0x5D, 0x74, 0x0A, 0x0F, 0xA0, 0x37, 0xF7, 0xB1, 0xA3, 0x16, 0x34, 0xD5, 0xE5, 0x2C, 0xC2,
0x94, 0xCF, 0xE4, 0x00, 0x40, 0x48, 0x53, 0x58, 0x17, 0xFC, 0x06, 0x29, 0xD8, 0x40, 0x07, 0x42,
0x43, 0x7E, 0x74, 0x4A, 0xFC, 0x89, 0x02, 0xCD, 0x1D, 0xA7, 0xAB, 0x43, 0x76, 0x0B, 0x3F, 0xCE,
0x9F, 0x15, 0x24, 0x2A, 0x1B, 0x84, 0x7D, 0x41, 0x29, 0xDD, 0xF5, 0xDD, 0x31, 0xDF, 0x7F, 0xAA,
0x6F, 0xBD, 0xC8, 0x94, 0xCE, 0x17, 0xB6, 0x8F, 0x3B, 0x02, 0xCB, 0x75, 0x82, 0xCD, 0xF8, 0x64,
0x77, 0xB4, 0xBD, 0x2F, 0x9A, 0x39, 0x72, 0xAB, 0xE7, 0x4E, 0xBB, 0x04, 0x24, 0xA2, 0x62, 0x2D,
0xD0, 0x08, 0x46, 0xCA, 0x95, 0x25, 0xE2, 0x7A, 0x12, 0xB9, 0xAA, 0x55, 0x38, 0x24, 0xD0, 0x85,
0x1A, 0x92, 0x43, 0xF3, 0x4D, 0x96, 0x1C, 0xAA, 0xB1, 0xD3, 0x56, 0xF8, 0x15, 0x95, 0xB4, 0x37,
0x46, 0xFA, 0xD8, 0x9E, 0xC6, 0xA8, 0x9C, 0xD5, 0xD3, 0x61, 0x30, 0xD2, 0x31, 0x60, 0xAA, 0x68,
0x7B, 0xD2, 0x61, 0x2A, 0xEC, 0x01, 0x23, 0x44, 0x54, 0x32, 0xCE, 0xA7, 0x65, 0x3E, 0x07, 0xF8,
0x55, 0xFC, 0x44, 0x9E, 0x96, 0x9E, 0xCD, 0xE3, 0xFE, 0x96, 0x55, 0x81, 0xB5, 0xE0, 0x4B, 0x35,
0xEC, 0x45, 0x39, 0xBB, 0x4C, 0x93, 0xDE, 0x29, 0xC4, 0x35, 0x0F, 0x0A, 0x11, 0x59, 0x51, 0xE2,
0xCF, 0xF1, 0xEA, 0x2E, 0xDA, 0x35, 0x67, 0x0E, 0x73, 0xAD, 0xAE, 0xBA, 0xF6, 0xA6, 0xD2, 0x03,
0x5E, 0x69, 0x93, 0x36, 0x42, 0xD0, 0x67, 0xAE, 0x84, 0x06, 0xB7, 0xA4, 0x00, 0x9C, 0xC1, 0x94,
0x0E, 0x6A, 0xB7, 0x35, 0x15, 0x0A, 0xD9, 0xE9, 0x25, 0x2C, 0x3F, 0xD0, 0x55, 0x4C, 0x87, 0x1F,
0x74, 0x33, 0xFF, 0xAE, 0xD2, 0x41, 0xC4, 0x90, 0xDF, 0x13, 0xD1, 0xD1, 0x8A, 0x38, 0x46, 0x56,
0x2D, 0x1D, 0xC2, 0xFC, 0x00, 0x31, 0x08, 0x8A, 0x03, 0x57, 0x5D, 0x50, 0xAA, 0xD5, 0xD3, 0x5D,
0x7E, 0xB4, 0x9B, 0x27, 0x75, 0x8E, 0xF8, 0xAA, 0x8F, 0xDE, 0x6E, 0xB5, 0x88, 0x46, 0xBB, 0x6A,
0x9C, 0xF3, 0xF6, 0xED, 0x03, 0x2C, 0x39, 0x2C, 0x13, 0xFC, 0xE8, 0xD2, 0x62, 0x37, 0xC0, 0xD1,
0x82, 0x4B, 0xFD, 0xA2, 0x2A, 0xDF, 0x15, 0x81, 0x0B, 0x1B, 0xE7, 0x41, 0x0E, 0x5D, 0x03, 0x18,
0x0A, 0x09, 0x51, 0x61, 0x3F, 0x7F, 0xA5, 0xF9, 0x60, 0x80, 0x36, 0xB7, 0xAF, 0x1F, 0x4C, 0x48,
0xCF, 0x61, 0xE7, 0x36, 0x2D, 0xBF, 0x79, 0x8F, 0x3E, 0x15, 0xD4, 0x44, 0xF6, 0x02, 0xFB, 0xF1,
0x09, 0xD5, 0xB5, 0x45, 0xD4, 0xE0, 0x84, 0x1A, 0xC2, 0x30, 0x88, 0xB6, 0x42, 0xF4, 0xAF, 0x43,
0xFE, 0xC3, 0x9A, 0x91, 0x06, 0xE2, 0xA6, 0x18, 0xEC, 0x1E, 0x12, 0x2C, 0x8F, 0x08, 0x21, 0x05,
0x6F, 0x32, 0x75, 0x9A, 0x81, 0x41, 0x78, 0x1C, 0xC1, 0xD9, 0xC9, 0xA3, 0xA3, 0x97, 0x74, 0x8F,
0x35, 0x5F, 0x9E, 0xA9, 0xE1, 0x08, 0x5D, 0x46, 0xD5, 0x65, 0xEA, 0xC2, 0xB7, 0x41, 0x9B, 0x2A,
0xFD, 0xE5, 0x67, 0x5F, 0xD6, 0x89, 0x8A, 0xA7, 0xC0, 0xD2, 0x8A, 0xDD, 0xC1, 0x1F, 0x37, 0x49,
0x55, 0xEB, 0x1D, 0xD2, 0x93, 0x8B, 0x05, 0xD2, 0xED, 0xF7, 0x1F, 0x95, 0x64, 0xB6, 0xD3, 0xE3,
0xEF, 0xD8, 0x4F, 0x77, 0x32, 0x49, 0x13, 0x41, 0xAE, 0x8A, 0x00, 0x09, 0x9B, 0xBD, 0xFF, 0xA5,
0x8A, 0x06, 0xFF, 0x4D, 0x6D, 0x0D, 0x7E, 0x27, 0x42, 0xDA, 0x88, 0x85, 0xAD, 0x29, 0xBA, 0x49,
0x79, 0x2E, 0x06, 0xC7, 0xD9, 0x3B, 0xAA, 0xFA, 0x85, 0x90, 0xCE, 0x10, 0xC2, 0xCD, 0x41, 0x63,
0xA1, 0x34, 0x30, 0x24, 0xC1, 0x93, 0xD5, 0xC3, 0x9E, 0xC4, 0xF6, 0x86, 0xB9, 0x03, 0x15, 0x7E,
0x23, 0x43, 0xB4, 0x1B, 0x4E, 0x5D, 0xC4, 0x84, 0x6E, 0xF2, 0xCA, 0xFE, 0x9E, 0x9A, 0x8B, 0xF1,
0x00, 0x0C, 0x76, 0x10, 0x91, 0xE2, 0x97, 0x8A, 0x40, 0x5F, 0x06, 0x1B, 0x00, 0x58, 0x52, 0xBB,
0xAA, 0xA4, 0x73, 0x4E, 0xD0, 0xF3, 0x9D, 0x8C, 0xD0, 0x39, 0x8D, 0x3D, 0x67, 0x4D, 0xEA, 0x05,
0xA4, 0x04, 0x9A, 0xEF, 0xAC, 0x9A, 0x56, 0x8F, 0x46, 0x37, 0xC2, 0x8A, 0x27, 0x71, 0xE1, 0x2F,
0x3F, 0xC2, 0x9E, 0x77, 0x9E, 0xFD, 0xE2, 0x51, 0x13, 0x8C, 0xCA, 0x2B, 0xE3, 0x6E, 0x71, 0x8F,
0x7B, 0x23, 0xB5, 0xD9, 0x59, 0xAA, 0xF1, 0xB2, 0x53, 0xD8, 0xBF, 0xDB, 0xEC, 0x1D, 0x99, 0xD8,
0x41, 0x31, 0x75, 0xA4, 0x68, 0x34, 0xC8, 0x3A, 0x1F, 0xDF, 0xAF, 0xE9, 0xC7, 0xA8, 0xF2, 0x14,
0x1A, 0xF8, 0xB1, 0x4B, 0x3B, 0x67, 0x40, 0x07, 0xD1, 0x27, 0xCE, 0xA4, 0x7A, 0x44, 0x1F, 0x16,
0xF4, 0xE2, 0x26, 0x18, 0x09, 0x99, 0xC5, 0xB9, 0x39, 0x30, 0xEE, 0xC0, 0xEE, 0x9D, 0x74, 0x91,
0xCE, 0x32, 0x2C, 0xEE, 0xE6, 0xED, 0xC3, 0x2C, 0x4D, 0xAB, 0x6A, 0x5C, 0x16, 0xDA, 0xD2, 0xDE,
0x59, 0xBA, 0x9C, 0x96, 0x99, 0xC1, 0x15, 0x7E, 0x69, 0xD7, 0xEC, 0x6A, 0x55, 0xA2, 0x4F, 0xCD,
0xF6, 0xA2, 0x7E, 0x6C, 0xD3, 0x6A, 0xF5, 0x60, 0x9E, 0xB3, 0x7D, 0xD5, 0xF9, 0x5F, 0x77, 0x0D,
0x73, 0xC9, 0x51, 0x78, 0x15, 0xCC, 0x40, 0x42, 0xC2, 0xD9, 0x27, 0x0F, 0x4D, 0x00, 0x89, 0x7F,
0x97, 0xA1, 0xED, 0xEC, 0x5E, 0xCA, 0xE7, 0x73, 0x6E, 0xF5, 0x0F, 0x24, 0x8A, 0x15, 0xC0, 0x77,
0x05, 0xBB, 0x68, 0x76, 0xD6, 0xE4, 0x9C, 0x26, 0xEB, 0x35, 0x22, 0x7B, 0xA5, 0xE9, 0x1B, 0xB5,
0x6F, 0x1F, 0xBF, 0x6F, 0xC5, 0x29, 0x52, 0x8F, 0x60, 0x04, 0xAF, 0xDF, 0x61, 0x7C, 0xEA, 0x09,
0x6F, 0x5E, 0x19, 0x60, 0x9F, 0x0F, 0x99, 0x8A, 0x59, 0x07, 0x5E, 0x6B, 0xEC, 0x1E, 0xC0, 0x53,
0x55, 0xB6, 0x0E, 0xB7, 0xEA, 0xCD, 0xA2, 0x46, 0x58, 0x07, 0x0C, 0xAF, 0x28, 0x83, 0x35, 0x75,
0xF3, 0x6D, 0xDD, 0x69, 0x79, 0x5C, 0x29, 0x04, 0x84, 0x55, 0x0F, 0x6B, 0x47, 0xE2, 0x88, 0x6E,
0x17, 0x4E, 0x5B, 0x0D, 0x97, 0x35, 0x0C, 0x13, 0x8C, 0xF3, 0xA3, 0xE0, 0x94, 0x61, 0x5C, 0x60,
0x18, 0x87, 0x1A, 0x52, 0xDB, 0x90, 0xF8, 0x13, 0xB5, 0x42, 0xC2, 0x65, 0xFB, 0x13, 0x68, 0xE0,
0xC4, 0xB0, 0xC9, 0x90, 0x69, 0x86, 0x8C, 0x94, 0xDC, 0x0B, 0xB3, 0xB4, 0x8C, 0xDE, 0x38, 0x9F,
0xCF, 0x99, 0xA1, 0xD5, 0x8B, 0x7E, 0x27, 0xCD, 0xFA, 0x15, 0xC5, 0xE7, 0x75, 0x9A, 0x3D, 0x14,
0xEB, 0x72, 0x4E, 0x1C, 0xE8, 0xE1, 0x5B, 0xB1, 0xAA, 0x1D, 0x3D, 0xE2, 0x44, 0x27, 0x52, 0x19,
0x7A, 0x00, 0x4D, 0x1D, 0xD4, 0xDA, 0xC0, 0x67, 0x1C, 0x69, 0x03, 0x5B, 0x87, 0xA4, 0x18, 0xB1,
0xC0, 0xA0, 0x4D, 0x52, 0x7A, 0xBD, 0x36, 0x71, 0xED, 0x77, 0xDA, 0xC0, 0x2D, 0xB4, 0xDA, 0x20,
0xEF, 0xAC, 0x01, 0x3B, 0x64, 0x9B, 0xC6, 0xEB, 0x11, 0x9E, 0x61, 0x43, 0x30, 0xAF, 0x46, 0xAD,
0x33, 0xC2, 0xF3, 0x33, 0x42, 0x77, 0xC3, 0x17, 0x03, 0xC2, 0x2E, 0xED, 0x20, 0xCC, 0xBD, 0xA6,
0xD8, 0xAE, 0x49, 0xB4, 0x35, 0x71, 0x5A, 0x13, 0x99, 0x35, 0xC3, 0x99, 0x35, 0xED, 0xC2, 0xDA,
0x54, 0x04, 0x20, 0xF8, 0xDA, 0x40, 0x23, 0xD6, 0x24, 0xF6, 0xDA, 0x80, 0x13, 0x6B, 0x03, 0x9E,
0xAC, 0xC9, 0x85, 0x35, 0xF9, 0xB1, 0x26, 0x61, 0xD7, 0x24, 0xEC, 0x9A, 0xB1, 0xF1, 0x9A, 0x84,
0x05, 0x50, 0x23, 0x41, 0x41, 0x90, 0x72, 0x4F, 0x31, 0x41, 0x28, 0x16, 0xC6, 0xEC, 0x1C, 0xED,
0xD6, 0x79, 0xC0, 0x06, 0x76, 0xEB, 0x71, 0x35, 0x20, 0xF2, 0x62, 0xA0, 0x4A, 0xC5, 0x29, 0xC4,
0x7C, 0x32, 0x02, 0xA1, 0x81, 0x2F, 0x24, 0x48, 0xDC, 0x48, 0xE2, 0x31, 0x4A, 0x62, 0xE1, 0xC2,
0x59, 0x73, 0x00, 0xC7, 0x1D, 0x0C, 0x3B, 0xD7, 0xD3, 0x8D, 0x72, 0x33, 0x50, 0x9B, 0xD3, 0x32,
0x69, 0x38, 0x33, 0xAC, 0x29, 0xCC, 0xBD, 0xEF, 0x5C, 0xAE, 0xE4, 0x05, 0x2E, 0x86, 0x52, 0x14,
0x3D, 0xBC, 0xF8, 0x94, 0x86, 0x8E, 0x26, 0x05, 0x33, 0x47, 0x03, 0xB7, 0x63, 0x9C, 0xA5, 0xBC,
0x98, 0x1B, 0x3F, 0x1B, 0xF7, 0x3E, 0xE9, 0xAC, 0x98, 0xAA, 0x09, 0x7C, 0x7E, 0xF0, 0x1A, 0xA6,
0xE7, 0xFF, 0x7D, 0xEF, 0xF5, 0x4B, 0xF7, 0x6E, 0x0F, 0x7C, 0xBA, 0x78, 0x1D, 0xA2, 0x34, 0x97,
0x77, 0x70, 0xAD, 0x72, 0x52, 0x46, 0x48, 0xA3, 0xC4, 0x4B, 0x93, 0x38, 0x8B, 0x7E, 0x4A, 0x31,
0x01, 0xEE, 0x7C, 0x01, 0xF2, 0x55, 0x14, 0x77, 0x50, 0xD3, 0xF1, 0xC9, 0xCE, 0xD7, 0xCA, 0x4F,
0x07, 0x0A, 0x59, 0x2E, 0x6A, 0x1D, 0x76, 0x8B, 0x96, 0xBB, 0xF9, 0x36, 0x7B, 0x77, 0xEC, 0xE2,
0xD3, 0x13, 0xFB, 0x00, 0x3A, 0x63, 0xE2, 0xE9, 0xE1, 0x00, 0x61, 0xFC, 0xBA, 0x98, 0xD9, 0x25,
0x77, 0xCA, 0x5D, 0x2F, 0xDA, 0xEE, 0x50, 0x4C, 0xC0, 0x96, 0x99, 0x0F, 0x3E, 0x54, 0x4C, 0xE7,
0xCD, 0x6C, 0xB8, 0x37, 0x73, 0x06, 0xC2, 0x76, 0xF1, 0x0E, 0xD6, 0x29, 0x3E, 0xFE, 0x80, 0x29,
0x1B, 0xF2, 0xB1, 0x69, 0x76, 0xA6, 0xD8, 0xD6, 0x1E, 0x3C, 0x2F, 0x85, 0x7B, 0xEB, 0xE0, 0x0D,
0x2D, 0xF9, 0x4B, 0x41, 0x40, 0x59, 0x08, 0xBA, 0x82, 0xF2, 0x80, 0x58, 0x22, 0xAD, 0xE3, 0xED,
0xFB, 0x36, 0x0F, 0xC4, 0x65, 0x15, 0x92, 0x3F, 0xDF, 0x75, 0x75, 0x92, 0x73, 0x92, 0x66, 0x43,
0x52, 0x52, 0xDB, 0x3D, 0x58, 0x1A, 0x4C, 0x00, 0xA3, 0x0C, 0xCB, 0xD2, 0x80, 0x33, 0x08, 0xC4,
0x24, 0x0C, 0x85, 0xE9, 0x41, 0x5E, 0x54, 0x7A, 0x66, 0x97, 0xCC, 0xB0, 0xA4, 0x04, 0x4E, 0x8E,
0x73, 0x70, 0x95, 0xFE, 0x0D, 0x04, 0x26, 0xFE, 0xE7, 0x6E, 0x06, 0xE1, 0x7F, 0x30, 0x6C, 0xFE,
0x2F, 0xB1, 0x76, 0x35, 0x0D, 0x5F, 0x3E, 0xC7, 0xC0, 0x64, 0xC5, 0xB9, 0x84, 0xCD, 0x7E, 0x5F,
0x30, 0x5A, 0xC1, 0x4A, 0x4C, 0xAB, 0x5A, 0x3C, 0x11, 0xE3, 0xE9, 0x0E, 0xB1, 0x1F, 0x70, 0x38,
0xEF, 0xB0, 0x2E, 0x3A, 0x5D, 0x30, 0x0A, 0x64, 0x81, 0x85, 0x81, 0x62, 0xD4, 0x78, 0xC3, 0x1A,
0x58, 0xB0, 0xDC, 0x75, 0x86, 0x88, 0x30, 0xE8, 0x02, 0x44, 0xB5, 0x1A, 0x23, 0x61, 0x11, 0xF1,
0xF4, 0xFB, 0xB0, 0x61, 0xA6, 0xE6, 0x0D, 0x18, 0x53, 0x22, 0xEC, 0x34, 0x1C, 0x4B, 0x2A, 0x27,
0x79, 0xC4, 0xE7, 0x45, 0xAA, 0x42, 0x86, 0xA6, 0xE7, 0xDA, 0x05, 0xAA, 0x3A, 0x53, 0xFE, 0xFF,
0xF0, 0x79, 0xE6, 0xC2, 0xD7, 0x04, 0x14, 0xF9, 0x43, 0xE3, 0xAA, 0x02, 0x76, 0x96, 0x2A, 0x86,
0xB8, 0xCC, 0x13, 0xA9, 0x1C, 0x2C, 0x6A, 0x60, 0xFF, 0x9A, 0xFE, 0x3B, 0x9D, 0x69, 0x14, 0x93,
0x3C, 0x49, 0xC9, 0x34, 0x8F, 0xB2, 0x86, 0x55, 0x0B, 0x46, 0xEF, 0xE9, 0x47, 0x24, 0xC5, 0xBF,
0x36, 0x14, 0x4B, 0xD6, 0x83, 0xED, 0x52, 0xAD, 0x90, 0x4F, 0x01, 0x25, 0x34, 0xFE, 0x8C, 0x19,
0x09, 0xFB, 0x69, 0x3A, 0x34, 0x56, 0xE5, 0x30, 0xB2, 0xA0, 0xAB, 0x4D, 0x4B, 0xA4, 0x1A, 0x2C,
0x7C, 0x61, 0x69, 0xCD, 0x90, 0x6C, 0xC3, 0x08, 0xED, 0x82, 0xD9, 0x73, 0x41, 0xBE, 0x02, 0x89,
0xA8, 0x15, 0xEF, 0x09, 0xC3, 0x9C, 0x96, 0x18, 0xA1, 0x99, 0x2C, 0x5F, 0x38, 0xC5, 0x53, 0x1B,
0x06, 0xF2, 0x9A, 0xD8, 0x26, 0x3F, 0xE5, 0x76, 0xBE, 0x2C, 0xE7, 0x94, 0x8D, 0x71, 0x8E, 0xAB,
0x26, 0xBC, 0x8E, 0x90, 0xEE, 0x8B, 0x37, 0x4B, 0x4A, 0xBF, 0xD9, 0x15, 0x51, 0xE6, 0x6C, 0xF4,
0x30, 0x2F, 0xAF, 0xDE, 0xFC, 0x47, 0xA4, 0x7A, 0x25, 0x15, 0xB0, 0x16, 0x50, 0x30, 0x70, 0x52,
0xC5, 0x8A, 0x02, 0xF3, 0x56, 0x6E, 0x37, 0xE1, 0xCD, 0x63, 0xC9, 0xCA, 0xB2, 0xD9, 0x84, 0xB0,
0x14, 0xC2, 0x4C, 0x83, 0x35, 0x66, 0xA9, 0x7C, 0x76, 0x73, 0x92, 0x3D, 0xCC, 0x24, 0x38, 0x9B,
0x3B, 0xA6, 0xCA, 0x15, 0x05, 0xA5, 0xF3, 0x9C, 0x15, 0xB2, 0x33, 0xEB, 0x8C, 0xD2, 0x71, 0x3D,
0x5B, 0x18, 0xC8, 0x22, 0x93, 0xD5, 0xFC, 0xDB, 0x15, 0xCB, 0x25, 0x6F, 0xE5, 0xC6, 0x6C, 0xB8,
0x81, 0xB9, 0xE2, 0x19, 0x83, 0xAB, 0xAE, 0xB9, 0x78, 0x95, 0x8B, 0x66, 0xCC, 0x3E, 0x6F, 0x13,
0xBF, 0x5F, 0xC5, 0xD8, 0x4D, 0x1F, 0xB1, 0x24, 0x1E, 0x40, 0xB5, 0x1A, 0xBF, 0xE3, 0xBE, 0x7A,
0x16, 0xC3, 0xD7, 0xDC, 0xC0, 0x91, 0x2B, 0x69, 0x9C, 0x4A, 0xD5, 0x7E, 0xC9, 0xBD, 0x97, 0xBC,
0xFA, 0x43, 0x70, 0x21, 0xA9, 0xC3, 0x16, 0x1B, 0xFA, 0x2B, 0x57, 0x22, 0xB4, 0x39, 0xC2, 0x94,
0x36, 0x78, 0x7C, 0xAB, 0xF7, 0x5C, 0x31, 0x66, 0xEF, 0xCD, 0x6E, 0x3C, 0x34, 0x3E, 0x8D, 0xF2,
0x93, 0x63, 0x03, 0xD5, 0x60, 0x0C, 0x66, 0xCE, 0xA5, 0xC1, 0xB8, 0xAA, 0x00, 0xEF, 0x52, 0x93,
0xD8, 0xF1, 0x34, 0xB4, 0xED, 0x29, 0x0B, 0x53, 0x37, 0xAF, 0xA8, 0xD1, 0x5D, 0x05, 0xB2, 0x08,
0x02, 0x96, 0x15, 0x2C, 0x47, 0x6D, 0x59, 0x8E, 0x6A, 0x5C, 0x39, 0xAA, 0x90, 0x22, 0x69, 0xBE,
0xAB, 0x49, 0x95, 0xCF, 0x02, 0x57, 0x9E, 0xC9, 0x5D, 0xED, 0xAA, 0xE6, 0xB0, 0x01, 0xF6, 0xFF,
0xA8, 0xC2, 0x54, 0xB7, 0x93, 0xA5, 0x3A, 0x35, 0x97, 0xFF, 0x1F, 0x78, 0xDC, 0xD6, 0xF4, 0x17,
0x0D, 0x7D, 0x2E, 0xE5, 0x2F, 0xCD, 0x2F, 0xD7, 0xE3, 0x2E, 0xB5, 0xDC, 0xF5, 0x61, 0xDF, 0x70,
0xBC, 0xDE, 0x72, 0x2C, 0x92, 0xBE, 0x66, 0x9E, 0xF0, 0xBA, 0x9A, 0xE5, 0xE2, 0x85, 0x20, 0x1D,
0xE3, 0x12, 0xF4, 0xFA, 0xAC, 0x0C, 0xB7, 0xF7, 0x6C, 0xC9, 0xCA, 0x7F, 0x89, 0x39, 0x75, 0x89,
0xC9, 0xD3, 0x2B, 0x36, 0x13, 0x1F, 0x7E, 0x65, 0x17, 0xB8, 0x4B, 0x5D, 0xC2, 0xDC, 0x54, 0x65,
0x34, 0xDC, 0xB0, 0x6E, 0x2A, 0x55, 0x4C, 0xC8, 0xDF, 0xF7, 0xC7, 0x1A, 0x29, 0xF3, 0x52, 0x22,
0x17, 0x6C, 0x6F, 0x0D, 0xCE, 0xB7, 0x62, 0x65, 0xC9, 0x9D, 0x97, 0xB2, 0xF4, 0x6A, 0x55, 0xCD,
0xB5, 0x14, 0xA9, 0xAA, 0xEC, 0x69, 0x9F, 0x2F, 0x8A, 0xC8, 0x5D, 0x62, 0x9B, 0xDF, 0xFB, 0xE1,
0x9C, 0xE0, 0xA4, 0x0C, 0x0A, 0xA5, 0xBA, 0xE4, 0x1D, 0x3B, 0xD7, 0x0A, 0x41, 0x14, 0x4B, 0x66,
0x5C, 0x31, 0x6F, 0x69, 0xB7, 0x2C, 0x2B, 0xAD, 0x46, 0x7D, 0x63, 0x7B, 0xF7, 0xD2, 0xFB, 0xD7,
0x6D, 0xF3, 0x5E, 0xC6, 0xA8, 0xF4, 0x84, 0x5E, 0xE3, 0x7D, 0xA1, 0x97, 0x75, 0xF4, 0x4A, 0x9F,
0xA1, 0x57, 0x7C, 0x19, 0x08, 0xBD, 0x92, 0x1D, 0xBD, 0x8A, 0x1D, 0xBD, 0x7E, 0xBB, 0xA3, 0x57,
0xBC, 0x63, 0xCC, 0x5C, 0x73, 0xBC, 0x4E, 0xDF, 0x14, 0xFF, 0x1E, 0xF2, 0x16, 0xD1, 0xF9, 0x7B,
0x15, 0x08, 0x79, 0x53, 0xBD, 0x40, 0x22, 0xD8, 0x22, 0x5B, 0x42, 0x4E, 0x7B, 0xBD, 0xBD, 0x67,
0x20, 0xA5, 0x23, 0xE4, 0xEE, 0x9C, 0x34, 0x7E, 0xDD, 0xF6, 0x4D, 0xC8, 0x76, 0xE1, 0x6A, 0x7C,
0x62, 0x94, 0xF4, 0x4B, 0xFF, 0xD4, 0x37, 0x25, 0x39, 0x2D, 0xD4, 0x6E, 0xD4, 0x5C, 0x25, 0xD4,
0x7F, 0xC4, 0x9D, 0xD1, 0xA8, 0xFF, 0xBD, 0x51, 0x76, 0x50, 0x55, 0x23, 0x55, 0xAA, 0xEC, 0x85,
0xCD, 0x1F, 0x32, 0xB8, 0xA0, 0x0C, 0xCC, 0xC8, 0x60, 0xD3, 0x33, 0xC4, 0xF5, 0x19, 0x8C, 0x79,
0x06, 0x1E, 0x65, 0x44, 0x0C, 0x22, 0xA2, 0xE1, 0xF7, 0x34, 0x72, 0x31, 0x1D, 0xB0, 0x42, 0x35,
0xA2, 0x71, 0x9B, 0x69, 0x9D, 0x41, 0xC4, 0x32, 0x98, 0xFF, 0x0C, 0x96, 0xE2, 0xA6, 0x7E, 0xDE,
0x35, 0xEF, 0xEA, 0x36, 0xCC, 0x41, 0x3A, 0xA9, 0xA3, 0xC2, 0xFE, 0x0E, 0x0E, 0x01, 0x26, 0xE1,
0x0F, 0x8F, 0x4F, 0x10, 0xD0, 0xF8, 0x88, 0x65, 0x3A, 0x50, 0xDC, 0xC2, 0x52, 0x8B, 0xDB, 0x95,
0x07, 0x02, 0xC8, 0xB7, 0xFB, 0x98, 0x31, 0x28, 0x8F, 0xF6, 0x7C, 0x9E, 0xDE, 0x94, 0xCF, 0xB3,
0x58, 0x43, 0x7F, 0x3C, 0x16, 0x91, 0x82, 0xF1, 0x9E, 0x44, 0xFC, 0x05, 0xB5, 0x2E, 0xA5, 0x58,
0xF3, 0xDC, 0x70, 0x06, 0x0C, 0xDE, 0xA3, 0x1D, 0x90, 0xAF, 0x71, 0x2D, 0x5D, 0xA4, 0xB3, 0x0C,
0x0B, 0xB6, 0x37, 0x46, 0x37, 0xD5, 0xBE, 0xF6, 0x2E, 0xD7, 0x11, 0x10, 0x26, 0xC1, 0xFE, 0x18,
0x0B, 0x5A, 0x1C, 0x58, 0xD0, 0x8C, 0x16, 0x34, 0x13, 0xC5, 0xE7, 0x1A, 0xAF, 0x60, 0xC3, 0x9F,
0xD6, 0xE0, 0xB7, 0x65, 0xF1, 0x44, 0x83, 0x13, 0x6A, 0xF0, 0x4B, 0xCD, 0xED, 0x0C, 0x6E, 0x47,
0x06, 0x99, 0xF4, 0xD1, 0xE6, 0xEF, 0x10, 0x57, 0x62, 0x47, 0x2E, 0x76, 0x7B, 0xE9, 0xED, 0x8E,
0x50, 0x26, 0x15, 0x53, 0xE4, 0x9A, 0x0F, 0xA4, 0x9C, 0x7E, 0xBE, 0xA8, 0x3D, 0xE3, 0x29, 0xA9,
0x22, 0xB7, 0xCB, 0x61, 0x60, 0xE9, 0x2C, 0x27, 0x43, 0x02, 0xD6, 0x51, 0xA3, 0xE6, 0x4C, 0x9D,
0xD4, 0x2C, 0x22, 0xEF, 0x0E, 0x65, 0x3A, 0xDE, 0x9E, 0x14, 0x51, 0x12, 0x6B, 0x0A, 0x6B, 0xFE,
0xF4, 0xAB, 0xBB, 0xAF, 0x52, 0x6E, 0xE1, 0xC9, 0x0D, 0x6C, 0x70, 0x0A, 0x46, 0xB3, 0xE8, 0x99,
0xA1, 0xE9, 0x57, 0x6C, 0x05, 0x5A, 0x05, 0x84, 0xAD, 0xCE, 0x7D, 0x9B, 0xB6, 0xB7, 0x2C, 0x36,
0xA3, 0x7D, 0x3B, 0xA4, 0x34, 0x76, 0x17, 0x73, 0x41, 0x76, 0x3E, 0xD1, 0x58, 0xCE, 0x45, 0x46,
0x77, 0x94, 0xC9, 0x30, 0x9B, 0x31, 0x75, 0xE7, 0x80, 0x37, 0x70, 0x67, 0x86, 0x51, 0xCF, 0x9A,
0x44, 0x3D, 0x97, 0xB3, 0x2B, 0x09, 0x88, 0xCE, 0x96, 0x13, 0xB4, 0x98, 0x76, 0x9B, 0xE5, 0xF0,
0x33, 0x66, 0xF8, 0x61, 0xCB, 0x08, 0x40, 0x84, 0x37, 0x18, 0x45, 0x72, 0x23, 0x4B, 0xD9, 0x2D,
0xE3, 0x47, 0x58, 0x06, 0x31, 0xF9, 0x07, 0x39, 0x4F, 0x91, 0x12, 0xB2, 0x1C, 0x9D, 0x28, 0xF3,
0x61, 0x9B, 0x85, 0xA0, 0x5B, 0xD0, 0x9D, 0x05, 0xED, 0x9A, 0x95, 0x04, 0xA6, 0xB7, 0x7E, 0x3F,
0xF8, 0x1D, 0x66, 0xDE, 0xF6, 0xEA, 0x2B, 0xCC, 0x3D, 0x75, 0xEA, 0x09, 0x28, 0xB9, 0x69, 0x25,
0xB7, 0x2B, 0x1A, 0x39, 0x77, 0x20, 0x57, 0x1B, 0x89, 0xCE, 0x11, 0x90, 0x2F, 0xC7, 0x53, 0x1B,
0x97, 0x53, 0xFA, 0x72, 0x7A, 0x2F, 0x85, 0xA2, 0x82, 0xE8, 0xA7, 0x0C, 0x3E, 0xE3, 0x69, 0x39,
0xFB, 0x93, 0xEC, 0x50, 0xBE, 0xEA, 0x13, 0xF8, 0x81, 0x8D, 0x94, 0x9B, 0x43, 0x12, 0x1E, 0xA1,
0x0E, 0x06, 0x8D, 0x64, 0x6B, 0xA1, 0x44, 0x18, 0xAF, 0xA2, 0x18, 0xD3, 0x13, 0x47, 0xB1, 0xB0,
0x5D, 0x28, 0xE6, 0xCB, 0xFF, 0x3A, 0xEC, 0x29, 0x76, 0x46, 0x5F, 0xA3, 0xE9, 0x66, 0x52, 0x49,
0xC8, 0x37, 0x0B, 0xC5, 0xC2, 0x5D, 0x0C, 0xBC, 0x3F, 0xDF, 0xDA, 0x1F, 0xCF, 0xD1, 0x1D, 0xBF,
0x4F, 0xCC, 0x2A, 0x76, 0xE3, 0xC9, 0x05, 0xBD, 0x76, 0xCD, 0x76, 0xF3, 0xF8, 0xC4, 0x9D, 0x40,
0xA8, 0x8D, 0x8B, 0xA4, 0x39, 0x9E, 0x23, 0x93, 0xF9, 0x24, 0x62, 0x74, 0x6E, 0x94, 0xAF, 0x79,
0xB8, 0x06, 0x35, 0x52, 0xC5, 0xF1, 0x31, 0x8F, 0xE4, 0x9A, 0x4A, 0x0A, 0x64, 0x3F, 0x60, 0x1A,
0x10, 0x6F, 0xF9, 0xCB, 0xF3, 0x34, 0xE0, 0x42, 0x7D, 0x67, 0x90, 0x53, 0xC0, 0x5E, 0x0E, 0x3A,
0xCE, 0x9F, 0xA7, 0x2B, 0xF0, 0xB4, 0x9F, 0xF3, 0x74, 0xC5, 0x93, 0xA3, 0x8D, 0xA3, 0x97, 0x5D,
0xEA, 0x97, 0x10, 0xC8, 0x92, 0xEC, 0xC3, 0xCE, 0x89, 0xE4, 0xB3, 0xB7, 0x54, 0x09, 0x94, 0xFE,
0x9C, 0x05, 0x88, 0x04, 0xBE, 0x28, 0x77, 0x27, 0xAF, 0x4F, 0xDD, 0x18, 0xCB, 0x7C, 0xB4, 0xE3,
0xD1, 0x83, 0x2B, 0x51, 0x84, 0xC8, 0xFB, 0x11, 0x22, 0x7F, 0x1E, 0xE5, 0xE7, 0x27, 0xC8, 0x51,
0xFE, 0x52, 0x55, 0xCC, 0xFB, 0xBD, 0x3B, 0xEA, 0xFF, 0x00, 0xD5, 0x5A, 0x3D, 0x8C, 0xDC, 0xD6,
0xBD, 0x5A, 0x3D, 0x8C, 0xE5, 0x56, 0x15, 0xBE, 0xB6, 0x77, 0xD7, 0x12, 0x16, 0x72, 0x8A, 0x2C,
0x44, 0x6C, 0xE1, 0x26, 0x55, 0x22, 0xE2, 0x36, 0xC5, 0x8E, 0xAF, 0xA3, 0x88, 0x3E, 0xC5, 0x4E,
0x56, 0x62, 0x8A, 0xD0, 0x12, 0x09, 0x4D, 0x93, 0x82, 0x62, 0xE6, 0xD9, 0x90, 0x82, 0x22, 0x05,
0x2B, 0xA1, 0x20, 0xA4, 0x34, 0x14, 0x09, 0x82, 0xB7, 0xC5, 0x36, 0x14, 0x20, 0xBD, 0x79, 0xB6,
0x40, 0xA2, 0xA1, 0x88, 0x41, 0x42, 0x94, 0x38, 0x62, 0xB7, 0x48, 0x28, 0x62, 0x81, 0x23, 0xFC,
0x36, 0xB6, 0x2F, 0xDF, 0x77, 0xAE, 0xFD, 0xDE, 0xCC, 0xEE, 0xEC, 0x6F, 0x56, 0xF8, 0xF9, 0x1E,
0xCF, 0xF3, 0xBB, 0x3F, 0xE7, 0x9E, 0xFF, 0x73, 0xEE, 0x68, 0xA5, 0xCF, 0xF9, 0x24, 0xEA, 0x95,
0xE9, 0xF3, 0x6D, 0x75, 0x41, 0xF1, 0xD2, 0xAA, 0x48, 0x8B, 0x3C, 0xC3, 0xB7, 0x55, 0x50, 0x4D,
0xB7, 0x5F, 0xE2, 0x3E, 0xC2, 0xED, 0xF1, 0x7E, 0xD5, 0xCB, 0x95, 0xFA, 0xAD, 0x53, 0xA3, 0x73,
0xEB, 0x11, 0x36, 0x5E, 0xA5, 0x94, 0xD3, 0x78, 0xA5, 0x52, 0x6E, 0xE3, 0x13, 0xD6, 0x01, 0xA0,
0x57, 0x05, 0xB9, 0xFA, 0xDA, 0xCD, 0xBB, 0xB7, 0x3E, 0x4B, 0x43, 0x83, 0x2B, 0xBF, 0x78, 0xF0,
0xB7, 0xC3, 0xFD, 0x54, 0x05, 0x79, 0x8C, 0x41, 0x61, 0x9A, 0x08, 0xDC, 0x03, 0x8C, 0xD4, 0x55,
0xC0, 0x58, 0x1D, 0x61, 0x79, 0x74, 0x1C, 0x27, 0xA8, 0xF6, 0x54, 0x8B, 0xF7, 0x57, 0x55, 0x03,
0x78, 0xE4, 0x10, 0xAE, 0x5C, 0xC2, 0xCE, 0xB5, 0x8B, 0x57, 0xF3, 0xE2, 0xCA, 0x41, 0xC3, 0x6A,
0xD2, 0xAE, 0xDC, 0x50, 0xBE, 0x31, 0x55, 0x64, 0x4C, 0x93, 0x7D, 0xF0, 0xF3, 0xE6, 0x04, 0xEF,
0xFA, 0xA9, 0x0D, 0x68, 0xD9, 0x7B, 0xB9, 0x8A, 0x4D, 0x8E, 0x3E, 0x9D, 0x52, 0x19, 0xFE, 0x08,
0x4C, 0xA3, 0x82, 0x9F, 0x74, 0x18, 0xDB, 0xA3, 0xAD, 0xF8, 0x4C, 0x83, 0x1F, 0xF5, 0xE3, 0xC1,
0xAD, 0x16, 0x68, 0x54, 0x19, 0x31, 0xB9, 0x92, 0xEF, 0xE6, 0x67, 0xB3, 0x97, 0x3B, 0x72, 0x07,
0x2A, 0x34, 0x25, 0x90, 0x57, 0xF1, 0xEB, 0x35, 0x30, 0x56, 0x09, 0x10, 0x5D, 0x11, 0xF7, 0x86,
0xD8, 0x1F, 0x01, 0xCD, 0x46, 0x1E, 0x24, 0x0C, 0x1E, 0x61, 0x2E, 0x8F, 0x48, 0x36, 0xD6, 0x26,
0x97, 0x6E, 0xA8, 0x3D, 0xA7, 0x59, 0xE9, 0x5E, 0xBD, 0xE2, 0x36, 0xED, 0xBA, 0x50, 0xD1, 0x07,
0x7F, 0x6D, 0xDA, 0x30, 0x0D, 0xCC, 0xBB, 0x7F, 0x69, 0xC2, 0xDC, 0xF9, 0x7D, 0xF8, 0x87, 0x86,
0xC4, 0x8C, 0xDC, 0x9A, 0x13, 0x24, 0x9E, 0x3C, 0x8E, 0xEC, 0xA3, 0x93, 0x97, 0x6E, 0xE3, 0xB2,
0x8B, 0x57, 0x39, 0x18, 0xA0, 0xF0, 0x57, 0x17, 0xA5, 0x44, 0x2B, 0x1B, 0xC9, 0x4D, 0x15, 0x75,
0x82, 0xAB, 0x93, 0xCE, 0x58, 0x47, 0x86, 0x7F, 0x16, 0x06, 0x34, 0x74, 0xC6, 0x03, 0xE0, 0xE8,
0xB5, 0x0E, 0xFA, 0x00, 0xCD, 0x7E, 0x07, 0xF9, 0xC6, 0xE9, 0x3C, 0xD2, 0x7F, 0x88, 0x48, 0x6D,
0x5D, 0x70, 0xDB, 0xD1, 0xC0, 0x19, 0x32, 0xBE, 0x76, 0xCD, 0xA0, 0xBC, 0x3A, 0x68, 0xB3, 0xDC,
0xAF, 0xA2, 0x32, 0xAA, 0xC3, 0x52, 0xA7, 0x7E, 0x07, 0x88, 0x59, 0x75, 0xAA, 0x49, 0x5B, 0x59,
0xBE, 0x27, 0x0C, 0x7A, 0x15, 0x97, 0x4A, 0x8F, 0x2A, 0xDC, 0x5C, 0x37, 0x18, 0x61, 0x4C, 0x17,
0xD6, 0xC0, 0x21, 0xF7, 0x7A, 0xE5, 0xBC, 0x41, 0xC6, 0xF2, 0x3A, 0x3A, 0x3A, 0x7A, 0x15, 0x22,
0xB9, 0xC2, 0xC2, 0x8D, 0x5F, 0x85, 0x65, 0x94, 0xC7, 0x29, 0xA5, 0x74, 0x81, 0xCF, 0xC9, 0xA9,
0x0F, 0xBF, 0xF3, 0x7D, 0x9C, 0x87, 0x65, 0x58, 0xF9, 0xB5, 0x4B, 0xA4, 0x8F, 0xD5, 0x85, 0x15,
0xDE, 0x45, 0x69, 0x58, 0xFA, 0xB5, 0xD7, 0xB8, 0xDC, 0x54, 0x8F, 0xCF, 0x3C, 0x60, 0xFE, 0x9C,
0xA8, 0x81, 0xEF, 0x5D, 0xAE, 0xD1, 0x04, 0x55, 0x98, 0x6B, 0x0C, 0x7D, 0xCE, 0x12, 0xC8, 0xAF,
0x6D, 0xA3, 0x4C, 0xA1, 0xBD, 0xE8, 0x95, 0x17, 0xB3, 0x9F, 0xFD, 0x69, 0x2C, 0x20, 0x07, 0xDF,
0x2A, 0xC6, 0x8F, 0x54, 0x88, 0x8D, 0x6B, 0x30, 0xF7, 0x04, 0x9B, 0xEC, 0xE3, 0xDC, 0x6B, 0xC3,
0x4A, 0x68, 0xFF, 0x9C, 0x43, 0xA1, 0x57, 0x8F, 0x00, 0x19, 0xA6, 0x49, 0x05, 0x5C, 0xBE, 0xFE,
0xC5, 0xFE, 0xA3, 0x46, 0x28, 0xE0, 0x13, 0x92, 0x08, 0x25, 0x89, 0x90, 0x40, 0xA0, 0x8E, 0x88,
0x26, 0x39, 0x97, 0xE2, 0xDE, 0xDF, 0x3F, 0xA3, 0xAC, 0x0A, 0x2A, 0xAC, 0xB0, 0x3B, 0x45, 0xD9,
0x23, 0x0B, 0x9D, 0x56, 0x98, 0xA9, 0xA8, 0xA1, 0x01, 0xB6, 0x13, 0x52, 0x88, 0x72, 0x6A, 0x99,
0x48, 0x05, 0xF6, 0x2C, 0xDD, 0x14, 0xF8, 0xEC, 0x4E, 0x5D, 0xFD, 0x87, 0x74, 0xB5, 0x57, 0x0C,
0x91, 0x52, 0xD0, 0x09, 0xE5, 0x51, 0x70, 0x82, 0xDA, 0xE9, 0xD9, 0xF9, 0x84, 0xBD, 0xC1, 0x8D,
0x54, 0x98, 0xA2, 0x88, 0xAE, 0x05, 0x51, 0xFE, 0x30, 0x30, 0xF5, 0x93, 0x61, 0x32, 0x01, 0xA6,
0x5A, 0x63, 0x52, 0xA0, 0xE5, 0x6D, 0xAE, 0x74, 0x5C, 0x08, 0x08, 0xC5, 0x22, 0xAD, 0x14, 0x11,
0xEC, 0x04, 0x92, 0x58, 0xD4, 0xDE, 0x97, 0x75, 0xF4, 0xCF, 0x5A, 0xBF, 0x5E, 0x7F, 0x1D, 0x6F,
0x9E, 0xE2, 0x16, 0x1A, 0x0E, 0x50, 0x7B, 0x53, 0xEA, 0x3B, 0x87, 0x4D, 0x02, 0xF9, 0xBF, 0x00,
0x76, 0x4E, 0xF4, 0x98, 0x9A, 0x33, 0xD1, 0xC5, 0x6F, 0x2C, 0x3D, 0x34, 0xB0, 0x58, 0xE3, 0x15,
0x5E, 0x07, 0xC4, 0x2A, 0xBF, 0xEF, 0x6B, 0xB1, 0x5C, 0x2E, 0x29, 0x28, 0x6C, 0x56, 0xE3, 0x8A,
0x91, 0xCA, 0x86, 0x4D, 0x60, 0x99, 0x16, 0xFC, 0x71, 0x39, 0xB7, 0x12, 0x0D, 0x3A, 0xD9, 0x02,
0xBE, 0x93, 0xC5, 0x03, 0xAC, 0xF4, 0x76, 0x81, 0xA5, 0xCC, 0xA0, 0xA6, 0xBD, 0xAB, 0x2B, 0x32,
0x62, 0x14, 0x9D, 0x53, 0x13, 0x37, 0x4E, 0x81, 0x0C, 0x74, 0x71, 0x06, 0xB5, 0x4E, 0xD1, 0xAF,
0xE0, 0x88, 0xC8, 0x8C, 0xC0, 0xF3, 0xB2, 0xA8, 0xBA, 0x4C, 0xEA, 0x11, 0x84, 0x04, 0x31, 0xC1,
0x9E, 0xEC, 0xAE, 0xA7, 0x9D, 0x06, 0xF0, 0x2B, 0x80, 0x10, 0x56, 0x0F, 0x9C, 0xEE, 0xC9, 0xE6,
0x9E, 0xE2, 0xD0, 0x5B, 0x1B, 0xE0, 0x57, 0x00, 0xB0, 0x27, 0xA2, 0xBE, 0xBD, 0xC8, 0x6E, 0x2B,
0xE0, 0xA5, 0xE5, 0x72, 0xFC, 0xE5, 0x84, 0xF8, 0xFD, 0x00, 0xFB, 0xAD, 0x62, 0xA0, 0x12, 0xF3,
0x39, 0xE1, 0x39, 0xB7, 0x64, 0x6E, 0x6F, 0xDF, 0x50, 0x99, 0xB5, 0xF2, 0xD5, 0x05, 0x15, 0x51,
0x48, 0x3B, 0x75, 0x1F, 0x07, 0xD8, 0xDC, 0x2E, 0x05, 0x07, 0x36, 0xAF, 0x65, 0x15, 0xB5, 0x26,
0xA6, 0xC5, 0xBE, 0x3C, 0xCB, 0xA2, 0xC3, 0x6F, 0x11, 0x41, 0x71, 0xE7, 0x9A, 0x72, 0x49, 0x34,
0x0A, 0xB1, 0x48, 0xB0, 0xB0, 0x0C, 0xA6, 0x40, 0x59, 0x21, 0x56, 0xFA, 0x12, 0xE8, 0xA2, 0x63,
0x18, 0x7A, 0xBD, 0x3E, 0xC0, 0x8E, 0x6A, 0xD0, 0x54, 0xE7, 0xA7, 0xC4, 0x35, 0x3A, 0x05, 0x20,
0x90, 0x31, 0x8C, 0x08, 0x7E, 0x03, 0x6B, 0xD7, 0xCA, 0x1B, 0xFC, 0x9E, 0xFC, 0x68, 0x94, 0xB8,
0x0D, 0x91, 0x45, 0xE1, 0xF1, 0x0E, 0xCC, 0xFC, 0xD8, 0x32, 0x94, 0x92, 0x33, 0x61, 0x53, 0x6F,
0xB1, 0xC9, 0xE7, 0x6E, 0x8B, 0x59, 0x0C, 0x9C, 0x7B, 0xFA, 0x6E, 0x31, 0x8F, 0xB7, 0x7D, 0xD7,
0x67, 0x64, 0x64, 0xD2, 0x42, 0xA8, 0x84, 0x43, 0x84, 0xFC, 0xCD, 0x75, 0xF0, 0xA8, 0x42, 0x27,
0x41, 0xD7, 0x2A, 0xD1, 0xA4, 0x49, 0x0B, 0x0A, 0x14, 0x78, 0x39, 0x50, 0xAE, 0x0A, 0x78, 0x28,
0x0C, 0x13, 0xE3, 0x01, 0xE5, 0xED, 0x82, 0x0E, 0xC3, 0x9C, 0x91, 0xEB, 0x08, 0x0D, 0x44, 0x1F,
0xC3, 0x2D, 0x80, 0x52, 0xCA, 0x2C, 0x2D, 0x1C, 0x9C, 0x67, 0x84, 0xC4, 0xE6, 0x86, 0xB2, 0x86,
0x5A, 0xAE, 0xC3, 0x81, 0xF6, 0x1F, 0x93, 0x06, 0x9B, 0xC3, 0x46, 0x45, 0x25, 0xF4, 0x00, 0x0A,
0x22, 0xA2, 0x92, 0x90, 0xC5, 0x34, 0xD9, 0xA0, 0xBC, 0x00, 0x7E, 0x15, 0x77, 0x80, 0x49, 0x1D,
0x93, 0x86, 0x5F, 0x1C, 0x18, 0x4E, 0x0A, 0xBE, 0x14, 0xDD, 0x4E, 0x94, 0x93, 0xFB, 0xE9, 0x07,
0xC4, 0x2F, 0x38, 0x50, 0x88, 0x80, 0x0E, 0x38, 0x30, 0xB7, 0xF1, 0xEA, 0x1A, 0xDA, 0x05, 0x6B,
0xF5, 0x52, 0xAD, 0xCE, 0xBB, 0x76, 0x16, 0xD1, 0x01, 0x5E, 0x71, 0x15, 0x57, 0x42, 0xD0, 0xFB,
0xAE, 0x88, 0x76, 0x35, 0xE7, 0xE6, 0x8F, 0x61, 0x31, 0x3B, 0xB5, 0xDD, 0x9A, 0xF2, 0x29, 0x35,
0x19, 0x9D, 0x01, 0xE9, 0x95, 0xD1, 0x23, 0x8C, 0xA7, 0xFF, 0xD0, 0xD5, 0xF4, 0xBB, 0x8A, 0x3B,
0x11, 0x43, 0x7E, 0x8F, 0x44, 0x47, 0x0B, 0xE2, 0x18, 0x88, 0x3C, 0x48, 0x07, 0x3F, 0x3D, 0x85,
0x18, 0x05, 0x45, 0xCF, 0xB6, 0xE1, 0xDE, 0x0B, 0x4A, 0xB5, 0xBC, 0xB7, 0xCB, 0x3B, 0xDB, 0x79,
0x62, 0xEB, 0x6F, 0xCF, 0xFB, 0xD3, 0xD9, 0xAE, 0x16, 0x8C, 0x34, 0x39, 0x95, 0xF5, 0xD1, 0x2E,
0xF9, 0xA5, 0xBB, 0x79, 0x82, 0x77, 0xCE, 0x2C, 0x76, 0xC9, 0xB4, 0xD4, 0x67, 0xA5, 0x3F, 0x2A,
0xF2, 0x17, 0x45, 0xE0, 0xFC, 0xCA, 0x3A, 0x8A, 0xD3, 0x1E, 0x00, 0x0C, 0x85, 0x6D, 0x51, 0x7E,
0x3B, 0x7D, 0xA5, 0xF9, 0x60, 0x1C, 0x36, 0xB5, 0xE7, 0x4F, 0x4D, 0x48, 0x07, 0x61, 0xA6, 0x36,
0xCE, 0xBF, 0x39, 0xB7, 0x3E, 0x14, 0xD4, 0x0C, 0x95, 0xC6, 0x33, 0xEF, 0x1F, 0x52, 0x5D, 0x6B,
0x04, 0x07, 0xD6, 0x96, 0x41, 0x18, 0x3A, 0x91, 0x79, 0x88, 0xFE, 0x45, 0xC8, 0xBF, 0x5F, 0x32,
0xA0, 0x40, 0x78, 0x14, 0x82, 0xDD, 0x5D, 0x84, 0xE5, 0x11, 0x08, 0xC4, 0xE0, 0x4D, 0xA2, 0x8E,
0x12, 0x30, 0x08, 0x8F, 0x3D, 0xF8, 0x34, 0x79, 0x34, 0x74, 0x86, 0xF6, 0xB1, 0xE2, 0xCB, 0x63,
0xD5, 0xED, 0xA1, 0xCB, 0xA0, 0x60, 0xCC, 0x4F, 0x5C, 0xE3, 0xD5, 0xB1, 0xD2, 0x9F, 0x7F, 0xFC,
0x79, 0x19, 0xA9, 0x70, 0xF4, 0x0C, 0x55, 0xE2, 0x0D, 0x7C, 0xB8, 0x49, 0xAA, 0x5A, 0x6B, 0x91,
0x1E, 0xAD, 0xCB, 0x8F, 0x37, 0xDF, 0xB9, 0x95, 0x93, 0xD9, 0x46, 0x88, 0xFA, 0xB2, 0xF9, 0x70,
0x2B, 0x93, 0x34, 0x11, 0xE4, 0xAA, 0x08, 0x90, 0xB0, 0xD9, 0xF9, 0x2F, 0xD5, 0xD9, 0xFB, 0x37,
0xB5, 0xD5, 0xFB, 0x44, 0x84, 0xB4, 0x12, 0x0B, 0x5B, 0x9E, 0x88, 0x86, 0x2F, 0xC4, 0xE0, 0x58,
0x7B, 0x07, 0x33, 0x02, 0x81, 0x26, 0xE9, 0xC4, 0x49, 0x42, 0x61, 0x61, 0xA0, 0xA1, 0x34, 0xD0,
0x5B, 0xEF, 0xEE, 0xF2, 0x66, 0x4B, 0x62, 0x83, 0x04, 0xB6, 0x43, 0x54, 0x52, 0xD5, 0x39, 0x44,
0xDB, 0xE1, 0xE9, 0x42, 0x26, 0xB4, 0x93, 0x17, 0xE6, 0x53, 0x5A, 0x20, 0x8C, 0x07, 0x60, 0x4C,
0x03, 0xBD, 0xC4, 0x2F, 0x05, 0x81, 0x3E, 0x0B, 0xD6, 0x00, 0x30, 0x15, 0x66, 0x59, 0x48, 0xE7,
0x94, 0xA0, 0xE5, 0x3B, 0x19, 0xA1, 0x53, 0x1A, 0x7B, 0xCE, 0x1A, 0x95, 0x33, 0x88, 0x09, 0x34,
0xDF, 0x19, 0x35, 0x2E, 0x6F, 0x0D, 0x76, 0x84, 0x11, 0x4F, 0x62, 0xA3, 0x5C, 0xFE, 0x09, 0x7B,
0xDE, 0x38, 0xE6, 0xB3, 0x5B, 0x95, 0x37, 0x28, 0x27, 0x0F, 0x49, 0x80, 0xB8, 0x54, 0xD6, 0x2F,
0x5A, 0xDF, 0xB4, 0xDE, 0x3A, 0xB7, 0xE1, 0xAC, 0x53, 0xD8, 0xBD, 0x5B, 0xEF, 0x1C, 0x19, 0x87,
0x69, 0x31, 0x8B, 0xA4, 0x68, 0xD0, 0xC9, 0x3A, 0xEF, 0x1F, 0x94, 0x34, 0xA8, 0x54, 0x79, 0x0A,
0x0D, 0xFC, 0xD8, 0x99, 0x9D, 0x33, 0x6E, 0x83, 0xE8, 0x13, 0x67, 0x52, 0x3D, 0xA0, 0x0F, 0xF3,
0x5A, 0x71, 0x13, 0xF4, 0xD8, 0xFD, 0xEC, 0xDC, 0x2C, 0x18, 0xB6, 0x60, 0xFB, 0x4E, 0xBA, 0x48,
0x67, 0x19, 0x16, 0x36, 0xD3, 0xF6, 0x61, 0x96, 0xC6, 0x65, 0x31, 0xCC, 0x0B, 0x6D, 0xC4, 0xC0,
0xD3, 0xE5, 0xD4, 0x4C, 0x00, 0xCE, 0xF1, 0x4B, 0xDB, 0x66, 0x96, 0xCB, 0x1C, 0x7D, 0x4A, 0xB6,
0x87, 0xF5, 0x63, 0x1B, 0x97, 0xCB, 0x1B, 0xD3, 0x9C, 0xF5, 0xE3, 0xCE, 0xFF, 0xA4, 0x6B, 0xCC,
0x6D, 0x72, 0x91, 0x34, 0xDB, 0x11, 0x09, 0x67, 0xEE, 0xDE, 0xEC, 0x3D, 0x48, 0xFC, 0x8B, 0x8C,
0x60, 0x45, 0xDA, 0xAC, 0x70, 0x3F, 0x80, 0x73, 0xCB, 0xDF, 0x90, 0x28, 0x46, 0x00, 0xDF, 0x65,
0xEC, 0xA2, 0xD9, 0x59, 0x93, 0x73, 0x9A, 0xAC, 0xD7, 0x08, 0xE0, 0x95, 0xA6, 0x6F, 0xD4, 0xAE,
0xB9, 0xF3, 0xA6, 0x91, 0x10, 0x81, 0x7A, 0x04, 0x23, 0x78, 0xF1, 0x0D, 0x86, 0xA1, 0x8E, 0xF0,
0xE6, 0xB1, 0x01, 0xF6, 0x79, 0x93, 0x19, 0x97, 0xB1, 0xE0, 0x89, 0xC6, 0xEE, 0x00, 0x72, 0x0A,
0x78, 0x3B, 0x7F, 0x43, 0x97, 0xFA, 0x34, 0x77, 0x4B, 0x67, 0x49, 0x7F, 0x89, 0xB0, 0x3E, 0x7C,
0x76, 0x77, 0x9C, 0xEF, 0x17, 0x0A, 0x01, 0x61, 0xD1, 0xC2, 0xDA, 0x91, 0x38, 0xA2, 0xDB, 0x99,
0xD5, 0xD6, 0x9E, 0xCB, 0x8A, 0x27, 0xEF, 0xAD, 0x1F, 0x05, 0xA7, 0x7A, 0xC6, 0x96, 0x3D, 0xC3,
0x87, 0x9E, 0xD4, 0xEE, 0x49, 0xFC, 0x91, 0x5A, 0xC1, 0xF0, 0x5B, 0x99, 0xF7, 0xA0, 0x81, 0x23,
0xC3, 0xA6, 0x9E, 0x4C, 0xEB, 0xC9, 0x48, 0x49, 0xB1, 0x30, 0x0B, 0x03, 0x68, 0x9F, 0xF3, 0xB9,
0x9C, 0x19, 0x5A, 0x3D, 0xEB, 0x77, 0x54, 0xAD, 0x1E, 0x53, 0x7C, 0x9E, 0xA4, 0x99, 0xD3, 0x62,
0x9D, 0xD3, 0x23, 0x31, 0xD8, 0xF0, 0x24, 0x80, 0xF5, 0x25, 0x6C, 0x82, 0x03, 0xA7, 0x13, 0x29,
0x7A, 0x7A, 0x00, 0x4D, 0x1D, 0xD4, 0xBA, 0x87, 0xCF, 0xD8, 0xD3, 0x3D, 0x6C, 0x1D, 0x72, 0x5F,
0xC4, 0x02, 0x9D, 0xEE, 0xA3, 0xDC, 0x69, 0x75, 0x1F, 0x96, 0x6E, 0xA3, 0x7B, 0xB8, 0x85, 0x5A,
0xF7, 0x48, 0x2F, 0x4B, 0xC0, 0x06, 0x49, 0x65, 0xEF, 0xB4, 0x08, 0xCF, 0xB0, 0x21, 0x98, 0xD7,
0x5E, 0xAD, 0x12, 0xC2, 0xC5, 0x31, 0xA1, 0xBD, 0xE1, 0x8B, 0x01, 0x61, 0x97, 0xB6, 0x10, 0xE6,
0x5E, 0x53, 0x6C, 0x57, 0x24, 0xDA, 0x8A, 0x38, 0xAD, 0x88, 0xCC, 0x0A, 0xC6, 0x4D, 0xAD, 0x68,
0x17, 0x56, 0x7D, 0x41, 0x00, 0x82, 0xAF, 0x7A, 0x68, 0xC4, 0x8A, 0xC4, 0x5E, 0xF5, 0xE0, 0xC4,
0xAA, 0x07, 0x4F, 0x56, 0xE4, 0xC2, 0x8A, 0xFC, 0x58, 0x91, 0xB0, 0x2B, 0x12, 0x76, 0xC5, 0xD8,
0x78, 0x45, 0xC2, 0x02, 0xA8, 0x81, 0x20, 0x23, 0x88, 0xB9, 0xA7, 0x90, 0xC0, 0x17, 0x0B, 0xD3,
0x6F, 0x1D, 0xED, 0xC6, 0x7A, 0xC0, 0x0A, 0x76, 0xEB, 0x4E, 0xD1, 0x29, 0xBF, 0x63, 0xA0, 0x4A,
0xC5, 0xC9, 0xC4, 0x7C, 0x32, 0x02, 0xA1, 0x81, 0x27, 0x73, 0xC3, 0x6A, 0x2D, 0xF1, 0x22, 0xF5,
0xCD, 0x6F, 0x6C, 0x38, 0xDB, 0x9F, 0x82, 0xC3, 0x16, 0xFA, 0x8D, 0xED, 0x69, 0x47, 0xD9, 0x19,
0xA8, 0xCD, 0x71, 0x1E, 0x55, 0x9C, 0x19, 0xD6, 0x14, 0xE6, 0xDE, 0xB5, 0x2E, 0x57, 0xF2, 0x02,
0x1B, 0x43, 0x29, 0x8A, 0x1E, 0x5E, 0x7C, 0x48, 0x43, 0x47, 0x93, 0x82, 0x99, 0x83, 0x8E, 0xDB,
0xE9, 0xAD, 0xA5, 0x3C, 0x99, 0x1A, 0xFF, 0xEE, 0xED, 0xFB, 0xA8, 0x31, 0x62, 0xAA, 0x46, 0xF0,
0xF9, 0xC6, 0x13, 0x98, 0x9E, 0xFF, 0xFB, 0xDE, 0xCB, 0x47, 0xEE, 0xDD, 0x9C, 0xF2, 0xE9, 0xE2,
0x75, 0x88, 0xD2, 0x54, 0xC5, 0xC1, 0xB5, 0x4C, 0x49, 0x19, 0x21, 0x8D, 0x12, 0x2F, 0x4D, 0xE2,
0xCC, 0xFA, 0x29, 0x35, 0x03, 0xB8, 0xF3, 0x19, 0xC8, 0x57, 0x51, 0xDC, 0x4E, 0x8D, 0xFB, 0x87,
0x5B, 0x5F, 0x2B, 0x3F, 0x9D, 0x52, 0xC8, 0x7C, 0x56, 0x6B, 0xBF, 0x99, 0xB5, 0xDC, 0xCE, 0xB7,
0xDE, 0xB9, 0x63, 0x1B, 0x9F, 0x1E, 0x9A, 0x1B, 0xD0, 0x99, 0x3E, 0x1C, 0x6F, 0x76, 0x10, 0xC6,
0xE7, 0xC5, 0xCC, 0xCE, 0xB9, 0x53, 0x6A, 0x7B, 0xD1, 0x76, 0xFB, 0x62, 0x02, 0x36, 0xCC, 0x7C,
0x98, 0x73, 0x32, 0x6B, 0xEF, 0x27, 0xC3, 0xBD, 0x9E, 0x52, 0x0B, 0xB6, 0x93, 0xE7, 0xB0, 0x4E,
0xF6, 0xFE, 0x5B, 0x4C, 0xD9, 0x90, 0x8F, 0x8D, 0x93, 0x33, 0xC5, 0xB6, 0x76, 0xE0, 0x41, 0x29,
0xDC, 0x33, 0x07, 0x4F, 0x69, 0xC9, 0x1F, 0x09, 0x3C, 0xCA, 0x82, 0xD7, 0x64, 0x94, 0x07, 0xC4,
0x12, 0x71, 0x19, 0x6E, 0xDE, 0x34, 0xA9, 0x27, 0x2E, 0x2B, 0x93, 0xFC, 0xF9, 0x9A, 0x2D, 0x87,
0x2C, 0x48, 0x9A, 0x35, 0x49, 0x49, 0x6D, 0x77, 0x60, 0x69, 0x30, 0x01, 0x8C, 0x32, 0x2C, 0x4B,
0x05, 0xCE, 0x20, 0x10, 0x93, 0x30, 0x14, 0xA6, 0xA7, 0x85, 0x5D, 0x76, 0xFA, 0x6D, 0x32, 0xC3,
0xCA, 0x11, 0x38, 0x39, 0x4C, 0xC1, 0x55, 0xFC, 0x63, 0x08, 0x4C, 0xF8, 0xC7, 0x66, 0x02, 0xFE,
0xDF, 0x19, 0x36, 0xFF, 0x4B, 0xAC, 0x5D, 0x49, 0xC3, 0x97, 0x4E, 0x31, 0x30, 0x59, 0xB1, 0x90,
0xB0, 0xD9, 0x6D, 0x33, 0x46, 0x2B, 0x58, 0x09, 0xB3, 0x21, 0xED, 0xF4, 0x1A, 0x96, 0x3B, 0x90,
0xB9, 0xD5, 0xC0, 0x61, 0xD1, 0x60, 0x5D, 0xD6, 0x4F, 0x18, 0x05, 0xE2, 0xD9, 0x33, 0x50, 0x0C,
0x2A, 0xA7, 0x5B, 0x01, 0x0B, 0x56, 0xB5, 0x8E, 0x11, 0x11, 0x7A, 0x8D, 0x87, 0xA8, 0x56, 0x63,
0x24, 0x2C, 0x22, 0x9E, 0x6E, 0xEB, 0x57, 0x31, 0xA2, 0x43, 0xA7, 0xC3, 0x98, 0x1C, 0x61, 0x67,
0xCF, 0xB1, 0xA4, 0x72, 0x94, 0x06, 0x7C, 0x9E, 0xC4, 0xCA, 0x67, 0x68, 0xBA, 0xD0, 0x36, 0x50,
0xD5, 0x89, 0x72, 0xFF, 0xC3, 0xE7, 0xB1, 0x0D, 0x5F, 0x23, 0x50, 0xE4, 0xCB, 0xCA, 0x56, 0x05,
0xCC, 0x24, 0x55, 0x0C, 0x71, 0x99, 0x27, 0x52, 0x39, 0x58, 0xF1, 0xD0, 0xAC, 0xC9, 0xC0, 0x7F,
0xC7, 0x13, 0x8D, 0x42, 0x92, 0x27, 0xCA, 0x99, 0xE6, 0x51, 0xD6, 0xB0, 0x6A, 0xC6, 0xE8, 0x3D,
0x7E, 0x97, 0xA4, 0xF8, 0x73, 0x45, 0xB1, 0x64, 0xD9, 0xD7, 0xCC, 0xD5, 0x0A, 0xF9, 0xCB, 0xA3,
0x84, 0x86, 0x1F, 0x33, 0x23, 0x61, 0x3F, 0x4D, 0x87, 0xC6, 0xE2, 0x1B, 0x46, 0x66, 0x74, 0xB5,
0x71, 0x8E, 0x54, 0x83, 0xF5, 0x2D, 0x2C, 0xAD, 0x19, 0x92, 0xAD, 0x19, 0xA1, 0x9D, 0xB0, 0xF6,
0x92, 0x91, 0xAF, 0x40, 0x22, 0xA8, 0xC5, 0x7B, 0xC2, 0x30, 0xC7, 0x39, 0x46, 0x68, 0x16, 0x5C,
0x4E, 0xAC, 0xE2, 0xA9, 0x35, 0x03, 0x79, 0x4D, 0x6C, 0xA3, 0x9F, 0x72, 0x3B, 0x9F, 0xE7, 0x53,
0xCA, 0xC6, 0x38, 0x67, 0x4A, 0x97, 0x9F, 0x00, 0xEC, 0x8A, 0x73, 0xD9, 0x94, 0x0A, 0xAC, 0xB7,
0x45, 0x94, 0x29, 0x1B, 0x9D, 0x03, 0x27, 0x29, 0x9F, 0x3E, 0xFD, 0x8F, 0x48, 0xF5, 0x72, 0x2A,
0x60, 0x29, 0x20, 0x63, 0xE0, 0xA4, 0xB2, 0x25, 0x05, 0xE6, 0x99, 0xDC, 0x76, 0xC2, 0xCB, 0xFB,
0x92, 0x95, 0x25, 0x93, 0x09, 0x61, 0x29, 0x84, 0x99, 0x06, 0x4B, 0xC9, 0x52, 0xE0, 0x6C, 0xA6,
0x24, 0xBB, 0x9B, 0x48, 0x70, 0x3C, 0x75, 0x8C, 0xA7, 0x4A, 0x85, 0x74, 0x9E, 0xB2, 0x42, 0x76,
0x66, 0x39, 0x51, 0x3A, 0xAE, 0x26, 0x0B, 0x03, 0x59, 0x64, 0xB2, 0x9A, 0xBE, 0x54, 0xB0, 0x5C,
0xF2, 0x4C, 0x6E, 0xCC, 0x86, 0x1B, 0x98, 0x2B, 0x1E, 0x25, 0x88, 0x20, 0x2A, 0x1B, 0xAF, 0x72,
0xD1, 0x84, 0xD9, 0xE7, 0x55, 0xE2, 0xF7, 0xBB, 0x10, 0xBB, 0x69, 0x03, 0x56, 0xBE, 0x3D, 0xA8,
0x56, 0xE5, 0x36, 0xDC, 0x57, 0xCB, 0x9A, 0xF7, 0x8A, 0x1B, 0xD8, 0xE3, 0xB6, 0xE5, 0x34, 0x21,
0x4C, 0xB7, 0xB9, 0xF7, 0x9C, 0x57, 0xBF, 0x0D, 0x2E, 0x44, 0xA5, 0x5F, 0x63, 0x43, 0x3F, 0x54,
0x36, 0x41, 0x4D, 0x11, 0xA6, 0xD4, 0xDE, 0x9D, 0x2B, 0xAD, 0x23, 0x05, 0xBD, 0x53, 0xDE, 0xEC,
0xD2, 0xCD, 0xDE, 0xA5, 0x51, 0xBE, 0xBB, 0xDF, 0x43, 0x35, 0x18, 0x83, 0xF5, 0x0B, 0x69, 0x30,
0xAE, 0xCA, 0xC3, 0xBB, 0xB8, 0x8F, 0xCC, 0x70, 0xE4, 0x9B, 0xFA, 0x88, 0x85, 0xA9, 0xCB, 0xE7,
0xD4, 0xE8, 0xCE, 0x03, 0x49, 0x00, 0x01, 0x4B, 0x32, 0x96, 0xA3, 0x36, 0x2C, 0x47, 0x55, 0xB6,
0x1C, 0x95, 0xF1, 0x57, 0xFD, 0xD0, 0xEA, 0xA9, 0xFC, 0x80, 0x2E, 0x52, 0xB7, 0x29, 0x39, 0xAC,
0x83, 0xFD, 0xDF, 0x2B, 0x30, 0xD5, 0xD5, 0x68, 0xAE, 0x4E, 0x4D, 0x55, 0xFE, 0x1B, 0x0E, 0xB7,
0x35, 0x7E, 0xB7, 0xA2, 0xCF, 0xA5, 0xFC, 0xC5, 0xE9, 0xD9, 0x7A, 0xDC, 0x99, 0x96, 0xDA, 0x3E,
0xEC, 0xEB, 0x0F, 0x17, 0x6B, 0x8E, 0x45, 0xD2, 0x57, 0x4D, 0x13, 0x5E, 0x54, 0x73, 0x05, 0xEB,
0x61, 0x20, 0x1E, 0xC2, 0x1C, 0xF4, 0xFA, 0x38, 0xF7, 0x37, 0xD7, 0x4D, 0xCE, 0x02, 0x7F, 0x8E,
0x39, 0x75, 0x8E, 0xC9, 0xE3, 0x73, 0x36, 0x13, 0x9E, 0xFE, 0xCA, 0x2E, 0xAC, 0x3A, 0xE5, 0x30,
0x37, 0x45, 0x1E, 0x74, 0x97, 0x8C, 0x9D, 0x4A, 0x65, 0x23, 0xF2, 0xF7, 0xDD, 0xE9, 0x45, 0xCC,
0xBC, 0x94, 0xC8, 0x49, 0x8D, 0x58, 0x7C, 0x2B, 0x56, 0x5E, 0x6F, 0xAB, 0xCF, 0x00, 0xCB, 0x65,
0x31, 0xD5, 0x52, 0xA4, 0xAA, 0xB2, 0xA3, 0x7D, 0x3A, 0x2B, 0x22, 0x77, 0x89, 0x6D, 0xBE, 0xF6,
0x83, 0x29, 0xC1, 0x89, 0x19, 0x14, 0x4A, 0x75, 0xC9, 0xD9, 0xB7, 0xAE, 0x15, 0x82, 0x28, 0x96,
0x6C, 0x2A, 0x05, 0xCF, 0xED, 0x8A, 0x61, 0xA5, 0xB5, 0x57, 0xDF, 0xD8, 0x5C, 0x3B, 0xF3, 0xFE,
0x49, 0xDB, 0xB4, 0x97, 0x21, 0xC8, 0x1D, 0xA1, 0xD7, 0x70, 0x20, 0xF4, 0x32, 0x96, 0x5E, 0xF1,
0x7D, 0xF4, 0x0A, 0xCF, 0x02, 0xA1, 0x57, 0xB4, 0xA5, 0x57, 0xB6, 0xA5, 0xD7, 0x3F, 0xB6, 0xF4,
0x0A, 0xB7, 0x8C, 0x89, 0x25, 0xD7, 0x26, 0x0B, 0x8D, 0x0A, 0x3F, 0x85, 0xBC, 0x05, 0x74, 0xFE,
0x4E, 0x01, 0x42, 0x5E, 0x56, 0x0F, 0x91, 0x08, 0xB6, 0xC0, 0xE4, 0x90, 0xD3, 0x56, 0x6F, 0xAE,
0xF7, 0x90, 0xD2, 0x01, 0x72, 0xB7, 0x20, 0x8D, 0x9F, 0xB4, 0xBD, 0x00, 0xD9, 0xCE, 0x6C, 0x8D,
0x4F, 0x8C, 0x92, 0x7E, 0xE4, 0x47, 0xBD, 0x20, 0xC9, 0x69, 0xA6, 0xB6, 0xA3, 0xA6, 0x2A, 0xA1,
0xFE, 0x0A, 0x77, 0x42, 0xA3, 0xFE, 0xAB, 0x5E, 0x99, 0x4E, 0x15, 0x95, 0x54, 0xA9, 0x92, 0x87,
0x36, 0xB7, 0x4B, 0xE0, 0x82, 0x12, 0x30, 0x23, 0x81, 0x4D, 0x4F, 0x10, 0xD7, 0x27, 0x30, 0xE6,
0x09, 0x78, 0x94, 0x10, 0x31, 0x88, 0x88, 0x86, 0xDF, 0xD3, 0xC8, 0xC5, 0xB4, 0xC7, 0x0A, 0xD5,
0x80, 0xC6, 0x6D, 0xC6, 0x65, 0x02, 0x11, 0xE3, 0xC1, 0x42, 0x02, 0x4B, 0x71, 0x59, 0x3F, 0xE8,
0x9A, 0x76, 0x75, 0x15, 0xE6, 0x20, 0x1E, 0xD5, 0x5E, 0x66, 0x3E, 0xE1, 0x99, 0xC4, 0xE6, 0xE0,
0xCB, 0x3B, 0x87, 0x08, 0x68, 0x5C, 0xC4, 0x32, 0x0D, 0x28, 0x6E, 0x60, 0xA9, 0xC5, 0xED, 0xCA,
0x03, 0x01, 0xE4, 0xB3, 0x7D, 0x4C, 0x18, 0xE4, 0x7B, 0x3B, 0x3E, 0x8F, 0x4F, 0xCB, 0xE7, 0xB9,
0x3E, 0x9D, 0x53, 0xAF, 0x5A, 0xE5, 0x0D, 0xD7, 0x25, 0xE2, 0xCF, 0xA8, 0x75, 0x31, 0xC5, 0x9A,
0xC7, 0x83, 0x13, 0x60, 0xF0, 0x1E, 0x6C, 0x81, 0x7C, 0x0D, 0x4B, 0xE9, 0x22, 0x9D, 0x65, 0x98,
0xB7, 0xB9, 0x34, 0xD8, 0xA9, 0x76, 0xB5, 0x77, 0xB9, 0xF6, 0x80, 0x30, 0x09, 0xF6, 0x55, 0x2C,
0x68, 0x76, 0xCA, 0x82, 0x26, 0xB4, 0xA0, 0x89, 0x28, 0x3E, 0xD7, 0x78, 0x0C, 0x1B, 0x7E, 0xAF,
0x06, 0x3F, 0x2B, 0x8B, 0x27, 0x1A, 0x1C, 0x51, 0x83, 0x1F, 0x69, 0x6E, 0x27, 0x70, 0x35, 0xE8,
0x91, 0x49, 0xEF, 0xAD, 0x7F, 0x81, 0xB8, 0x12, 0x3B, 0xB2, 0xB1, 0xDB, 0x23, 0x6F, 0x7B, 0x84,
0x32, 0xAA, 0x90, 0x22, 0x57, 0xBD, 0x25, 0xE5, 0xF4, 0xC5, 0xAC, 0xF6, 0x8C, 0xA7, 0xA4, 0x8A,
0x5C, 0xDB, 0x33, 0x3F, 0x87, 0xC4, 0x8F, 0xA6, 0x90, 0x80, 0x75, 0xD4, 0xA0, 0x3A, 0x56, 0x87,
0x25, 0x8B, 0xC8, 0xDB, 0x43, 0x99, 0x86, 0xB7, 0x23, 0x45, 0x94, 0xC8, 0xF4, 0x99, 0xE9, 0xBF,
0xF9, 0xC5, 0xB5, 0xC7, 0x29, 0xB7, 0xF0, 0xE4, 0x06, 0x36, 0x38, 0x06, 0xA3, 0x59, 0xF4, 0x4C,
0xD0, 0xF4, 0x63, 0xB6, 0x0C, 0xAD, 0x00, 0xC2, 0x46, 0xA7, 0xAE, 0x89, 0xEB, 0x2B, 0x06, 0x9B,
0xD1, 0xAE, 0xE9, 0x62, 0x1A, 0xBB, 0x93, 0xA9, 0x20, 0x6B, 0x19, 0x4E, 0x2B, 0xB8, 0x20, 0xBD,
0x06, 0x86, 0x36, 0xB0, 0x81, 0x08, 0xB3, 0x19, 0x53, 0x37, 0x16, 0x38, 0x1D, 0x77, 0xD6, 0x33,
0xEA, 0x59, 0x91, 0xA8, 0x0B, 0x39, 0xBB, 0x92, 0x80, 0xE8, 0x78, 0x3E, 0x41, 0x0B, 0x69, 0xB7,
0x59, 0x0E, 0x3F, 0x66, 0x86, 0xEF, 0xD7, 0x8C, 0x00, 0x44, 0x78, 0xBD, 0x41, 0x24, 0x37, 0x30,
0x94, 0xDD, 0x3C, 0xBC, 0x85, 0x65, 0x10, 0x93, 0xBF, 0x95, 0xF2, 0x14, 0x29, 0x22, 0xCB, 0xD1,
0x89, 0x32, 0xEF, 0xD7, 0x89, 0x0F, 0xBA, 0x79, 0xCD, 0xB1, 0x57, 0xAF, 0x58, 0x49, 0x60, 0x7A,
0xEB, 0xB6, 0x9D, 0xDB, 0x60, 0xE6, 0x4D, 0xAB, 0xBE, 0xC0, 0xDC, 0x63, 0xA3, 0xEE, 0x82, 0x92,
0xEB, 0x5A, 0x72, 0xBB, 0xAC, 0x92, 0x73, 0x07, 0x72, 0xB5, 0x92, 0xE8, 0x1C, 0x01, 0xF9, 0x7C,
0x3C, 0xB5, 0xB6, 0x39, 0xA5, 0x2B, 0x87, 0xF4, 0x52, 0x28, 0xCA, 0x88, 0x7E, 0xCC, 0xE0, 0x33,
0x1C, 0xE7, 0xB3, 0x3F, 0xC9, 0x0E, 0xE5, 0xAB, 0x3E, 0xE4, 0x11, 0xA6, 0x94, 0x9B, 0x7D, 0x12,
0x1E, 0xA1, 0x0E, 0x06, 0x0D, 0x64, 0x6B, 0xA6, 0x44, 0x18, 0xCF, 0xA3, 0x98, 0x1C, 0xEF, 0x0A,
0xC5, 0x10, 0x32, 0x4D, 0x14, 0x73, 0xE5, 0x5F, 0x1A, 0x76, 0x14, 0x3B, 0xA6, 0xAF, 0xD1, 0x74,
0x33, 0xB1, 0x24, 0xE4, 0xEB, 0x99, 0x62, 0xFE, 0x36, 0x06, 0x96, 0xF3, 0x2D, 0xBF, 0x3E, 0x73,
0xC8, 0x45, 0x77, 0xFC, 0x26, 0x31, 0x2B, 0xD8, 0x8D, 0x27, 0x17, 0xF4, 0xDA, 0x25, 0xDB, 0xE5,
0xFD, 0xC3, 0xDD, 0x99, 0xAB, 0x63, 0xFF, 0x2D, 0x41, 0x46, 0x46, 0xD3, 0x49, 0xC4, 0x60, 0xDD,
0x28, 0x5F, 0x43, 0x7B, 0xE4, 0xBC, 0x2C, 0xDB, 0xDF, 0x4F, 0x59, 0x56, 0x2D, 0xA4, 0x40, 0xF6,
0x7D, 0xA6, 0x01, 0xE1, 0xC6, 0x9E, 0x70, 0x9D, 0x7F, 0x9F, 0xA8, 0x97, 0x3B, 0x39, 0x05, 0x6C,
0xE5, 0xA0, 0x63, 0xF1, 0x20, 0x5D, 0x81, 0xA7, 0xBD, 0xCD, 0xD3, 0x15, 0x47, 0x8E, 0x36, 0xF6,
0x1E, 0x75, 0xA9, 0x5F, 0x43, 0x20, 0x73, 0xB2, 0x0F, 0x3B, 0x27, 0x92, 0xF7, 0xDF, 0x52, 0x25,
0x50, 0xFA, 0x36, 0x0B, 0x10, 0x11, 0x7C, 0x51, 0x6A, 0x4F, 0x5E, 0xEF, 0xB9, 0x31, 0x96, 0xF9,
0x68, 0xC3, 0xA3, 0x07, 0x5B, 0xA2, 0xF0, 0x91, 0xF7, 0x23, 0x44, 0xBE, 0x1D, 0xA4, 0x8B, 0x43,
0xE4, 0x28, 0xDF, 0xE3, 0x31, 0xB6, 0x1C, 0xCB, 0xFD, 0x0F, 0xD5, 0x5A, 0x3D, 0x8C, 0xDC, 0xD6,
0x11, 0x1E, 0x92, 0xD2, 0xD1, 0x10, 0x81, 0x50, 0x85, 0x64, 0x04, 0x16, 0x2C, 0xBA, 0x48, 0x11,
0x0B, 0xB6, 0xD8, 0xBA, 0xB8, 0x5B, 0xD2, 0x48, 0x91, 0x22, 0x85, 0x55, 0xE8, 0x2C, 0x20, 0x2E,
0x54, 0xC7, 0x41, 0x70, 0x29, 0x5C, 0xA8, 0x58, 0x2D, 0xE9, 0x2A, 0x65, 0xAE, 0x71, 0x95, 0x46,
@@ -2225,85 +2225,85 @@ static const EpdGlyph notosans_16_regularGlyphs[] = {
{ 9, 4, 172, 1, 11, 9, 898 }, // -
{ 5, 6, 143, 2, 5, 8, 907 }, // .
{ 13, 24, 198, 0, 24, 78, 915 }, // /
{ 17, 26, 305, 1, 25, 111, 993 }, // 0
{ 10, 24, 305, 2, 24, 60, 1104 }, // 1
{ 17, 25, 305, 1, 25, 107, 1164 }, // 2
{ 17, 26, 305, 1, 25, 111, 1271 }, // 3
{ 19, 24, 305, 0, 24, 114, 1382 }, // 4
{ 16, 25, 305, 2, 24, 100, 1496 }, // 5
{ 17, 26, 305, 1, 25, 111, 1596 }, // 6
{ 17, 24, 305, 1, 24, 102, 1707 }, // 7
{ 17, 26, 305, 1, 25, 111, 1809 }, // 8
{ 17, 26, 305, 1, 25, 111, 1920 }, // 9
{ 5, 20, 143, 2, 19, 25, 2031 }, // :
{ 6, 24, 143, 1, 19, 36, 2056 }, // ;
{ 17, 18, 305, 1, 21, 77, 2092 }, // <
{ 17, 10, 305, 1, 17, 43, 2169 }, // =
{ 17, 18, 305, 1, 21, 77, 2212 }, // >
{ 14, 26, 231, 0, 25, 91, 2289 }, // ?
{ 28, 27, 479, 1, 24, 189, 2380 }, // @
{ 22, 24, 341, 0, 24, 132, 2569 }, // A
{ 17, 24, 347, 3, 24, 102, 2701 }, // B
{ 19, 26, 337, 2, 25, 124, 2803 }, // C
{ 20, 24, 389, 3, 24, 120, 2927 }, // D
{ 14, 24, 296, 3, 24, 84, 3047 }, // E
{ 14, 24, 277, 3, 24, 84, 3131 }, // F
{ 20, 26, 388, 2, 25, 130, 3215 }, // G
{ 19, 24, 395, 3, 24, 114, 3345 }, // H
{ 9, 24, 181, 1, 24, 54, 3459 }, // I
{ 10, 31, 146, -3, 24, 78, 3513 }, // J
{ 18, 24, 330, 3, 24, 108, 3591 }, // K
{ 14, 24, 279, 3, 24, 84, 3699 }, // L
{ 24, 24, 484, 3, 24, 144, 3783 }, // M
{ 20, 24, 405, 3, 24, 120, 3927 }, // N
{ 22, 26, 416, 2, 25, 143, 4047 }, // O
{ 16, 24, 323, 3, 24, 96, 4190 }, // P
{ 22, 31, 416, 2, 25, 171, 4286 }, // Q
{ 18, 24, 332, 3, 24, 108, 4457 }, // R
{ 16, 26, 293, 1, 25, 104, 4565 }, // S
{ 19, 24, 296, 0, 24, 114, 4669 }, // T
{ 19, 25, 390, 3, 24, 119, 4783 }, // U
{ 20, 24, 320, 0, 24, 120, 4902 }, // V
{ 31, 24, 496, 0, 24, 186, 5022 }, // W
{ 20, 24, 312, 0, 24, 120, 5208 }, // X
{ 19, 24, 302, 0, 24, 114, 5328 }, // Y
{ 17, 24, 305, 1, 24, 102, 5442 }, // Z
{ 9, 30, 175, 2, 24, 68, 5544 }, // [
{ 13, 24, 198, 0, 24, 78, 5612 }, // <backslash>
{ 9, 30, 175, 0, 24, 68, 5690 }, // ]
{ 17, 16, 305, 1, 24, 68, 5758 }, // ^
{ 16, 3, 237, -1, -3, 12, 5826 }, // _
{ 8, 6, 150, 1, 26, 12, 5838 }, // `
{ 15, 20, 299, 1, 19, 75, 5850 }, // a
{ 17, 27, 328, 2, 26, 115, 5925 }, // b
{ 14, 20, 256, 1, 19, 70, 6040 }, // c
{ 17, 27, 328, 1, 26, 115, 6110 }, // d
{ 17, 20, 301, 1, 19, 85, 6225 }, // e
{ 13, 26, 183, 0, 26, 85, 6310 }, // f
{ 17, 27, 328, 1, 19, 115, 6395 }, // g
{ 16, 26, 330, 2, 26, 104, 6510 }, // h
{ 5, 25, 138, 2, 25, 32, 6614 }, // i
{ 9, 33, 138, -2, 25, 75, 6646 }, // j
{ 16, 26, 285, 2, 26, 104, 6721 }, // k
{ 4, 26, 138, 2, 26, 26, 6825 }, // l
{ 27, 19, 499, 2, 19, 129, 6851 }, // m
{ 16, 19, 330, 2, 19, 76, 6980 }, // n
{ 18, 20, 323, 1, 19, 90, 7056 }, // o
{ 17, 27, 328, 2, 19, 115, 7146 }, // p
{ 17, 27, 328, 1, 19, 115, 7261 }, // q
{ 12, 19, 220, 2, 19, 57, 7376 }, // r
{ 14, 20, 255, 1, 19, 70, 7433 }, // s
{ 12, 23, 193, 0, 22, 69, 7503 }, // t
{ 16, 19, 330, 2, 18, 76, 7572 }, // u
{ 17, 18, 271, 0, 18, 77, 7648 }, // v
{ 26, 18, 419, 0, 18, 117, 7725 }, // w
{ 18, 18, 282, 0, 18, 81, 7842 }, // x
{ 17, 26, 272, 0, 18, 111, 7923 }, // y
{ 14, 18, 251, 1, 18, 63, 8034 }, // z
{ 12, 30, 203, 0, 24, 90, 8097 }, // {
{ 4, 35, 294, 7, 26, 35, 8187 }, // |
{ 11, 30, 203, 1, 24, 83, 8222 }, // }
{ 17, 5, 305, 1, 14, 22, 8305 }, // ~
{ 17, 26, 311, 1, 25, 111, 993 }, // 0
{ 10, 24, 235, 0, 24, 60, 1104 }, // 1
{ 16, 25, 296, 1, 25, 100, 1164 }, // 2
{ 17, 26, 305, 1, 25, 111, 1264 }, // 3
{ 19, 24, 305, 0, 24, 114, 1375 }, // 4
{ 16, 25, 305, 2, 24, 100, 1489 }, // 5
{ 17, 26, 305, 1, 25, 111, 1589 }, // 6
{ 17, 24, 272, 0, 24, 102, 1700 }, // 7
{ 17, 26, 314, 1, 25, 111, 1802 }, // 8
{ 17, 26, 305, 1, 25, 111, 1913 }, // 9
{ 5, 20, 143, 2, 19, 25, 2024 }, // :
{ 6, 24, 143, 1, 19, 36, 2049 }, // ;
{ 17, 18, 305, 1, 21, 77, 2085 }, // <
{ 17, 10, 305, 1, 17, 43, 2162 }, // =
{ 17, 18, 305, 1, 21, 77, 2205 }, // >
{ 14, 26, 231, 0, 25, 91, 2282 }, // ?
{ 28, 27, 479, 1, 24, 189, 2373 }, // @
{ 22, 24, 341, 0, 24, 132, 2562 }, // A
{ 17, 24, 347, 3, 24, 102, 2694 }, // B
{ 19, 26, 337, 2, 25, 124, 2796 }, // C
{ 20, 24, 389, 3, 24, 120, 2920 }, // D
{ 14, 24, 296, 3, 24, 84, 3040 }, // E
{ 14, 24, 277, 3, 24, 84, 3124 }, // F
{ 20, 26, 388, 2, 25, 130, 3208 }, // G
{ 19, 24, 395, 3, 24, 114, 3338 }, // H
{ 9, 24, 181, 1, 24, 54, 3452 }, // I
{ 10, 31, 146, -3, 24, 78, 3506 }, // J
{ 18, 24, 330, 3, 24, 108, 3584 }, // K
{ 14, 24, 279, 3, 24, 84, 3692 }, // L
{ 24, 24, 484, 3, 24, 144, 3776 }, // M
{ 20, 24, 405, 3, 24, 120, 3920 }, // N
{ 22, 26, 416, 2, 25, 143, 4040 }, // O
{ 16, 24, 323, 3, 24, 96, 4183 }, // P
{ 22, 31, 416, 2, 25, 171, 4279 }, // Q
{ 18, 24, 332, 3, 24, 108, 4450 }, // R
{ 16, 26, 293, 1, 25, 104, 4558 }, // S
{ 19, 24, 296, 0, 24, 114, 4662 }, // T
{ 19, 25, 390, 3, 24, 119, 4776 }, // U
{ 20, 24, 320, 0, 24, 120, 4895 }, // V
{ 31, 24, 496, 0, 24, 186, 5015 }, // W
{ 20, 24, 312, 0, 24, 120, 5201 }, // X
{ 19, 24, 302, 0, 24, 114, 5321 }, // Y
{ 17, 24, 305, 1, 24, 102, 5435 }, // Z
{ 9, 30, 175, 2, 24, 68, 5537 }, // [
{ 13, 24, 198, 0, 24, 78, 5605 }, // <backslash>
{ 9, 30, 175, 0, 24, 68, 5683 }, // ]
{ 17, 16, 305, 1, 24, 68, 5751 }, // ^
{ 16, 3, 237, -1, -3, 12, 5819 }, // _
{ 8, 6, 150, 1, 26, 12, 5831 }, // `
{ 15, 20, 299, 1, 19, 75, 5843 }, // a
{ 17, 27, 328, 2, 26, 115, 5918 }, // b
{ 14, 20, 256, 1, 19, 70, 6033 }, // c
{ 17, 27, 328, 1, 26, 115, 6103 }, // d
{ 17, 20, 301, 1, 19, 85, 6218 }, // e
{ 13, 26, 183, 0, 26, 85, 6303 }, // f
{ 17, 27, 328, 1, 19, 115, 6388 }, // g
{ 16, 26, 330, 2, 26, 104, 6503 }, // h
{ 5, 25, 138, 2, 25, 32, 6607 }, // i
{ 9, 33, 138, -2, 25, 75, 6639 }, // j
{ 16, 26, 285, 2, 26, 104, 6714 }, // k
{ 4, 26, 138, 2, 26, 26, 6818 }, // l
{ 27, 19, 499, 2, 19, 129, 6844 }, // m
{ 16, 19, 330, 2, 19, 76, 6973 }, // n
{ 18, 20, 323, 1, 19, 90, 7049 }, // o
{ 17, 27, 328, 2, 19, 115, 7139 }, // p
{ 17, 27, 328, 1, 19, 115, 7254 }, // q
{ 12, 19, 220, 2, 19, 57, 7369 }, // r
{ 14, 20, 255, 1, 19, 70, 7426 }, // s
{ 12, 23, 193, 0, 22, 69, 7496 }, // t
{ 16, 19, 330, 2, 18, 76, 7565 }, // u
{ 17, 18, 271, 0, 18, 77, 7641 }, // v
{ 26, 18, 419, 0, 18, 117, 7718 }, // w
{ 18, 18, 282, 0, 18, 81, 7835 }, // x
{ 17, 26, 272, 0, 18, 111, 7916 }, // y
{ 14, 18, 251, 1, 18, 63, 8027 }, // z
{ 12, 30, 203, 0, 24, 90, 8090 }, // {
{ 4, 35, 294, 7, 26, 35, 8180 }, // |
{ 11, 30, 203, 1, 24, 83, 8215 }, // }
{ 17, 5, 305, 1, 14, 22, 8298 }, // ~
{ 0, 0, 139, 0, 0, 0, 0 }, // U+00A0
{ 5, 26, 143, 2, 19, 33, 0 }, // U+00A1
{ 14, 26, 305, 3, 25, 91, 33 }, // U+00A2
@@ -3303,7 +3303,7 @@ static const EpdUnicodeInterval notosans_16_regularIntervals[] = {
};
static const EpdFontGroup notosans_16_regularGroups[] = {
{ 0, 3626, 9206, 97, 0 },
{ 0, 3626, 9181, 97, 0 },
{ 3626, 3331, 11053, 96, 97 },
{ 6957, 4095, 16723, 128, 193 },
{ 11052, 4104, 15196, 96, 321 },
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff

Some files were not shown because too many files have changed in this diff Show More