Compare commits

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

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

---------

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

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

---

### AI Usage

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

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

---------

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

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

## Additional Context

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

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

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

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

---

### AI Usage

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

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

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

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

## Additional Context

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

---

### AI Usage

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

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

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

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

## Additional Context

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

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

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

## Details

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

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

## Validation

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

Micro fix Ukrainian translation

---

### AI Usage

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

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

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

Update Ukrainian translation


### AI Usage

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

---------

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

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

---

### AI Usage

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

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

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

### AI Usage

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

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

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

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

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

---

### AI Usage

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

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

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

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

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

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

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

## Additional Context

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

---

### AI Usage

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

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

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

---

### AI Usage

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

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

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

---

### AI Usage

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

Did you use AI tools to help write this code? _**YES**_
2026-05-10 11:55:57 -05:00
Zach Nelson fcb8793640 Merge branch 'master' into release/1.3.0 2026-05-10 11:07:12 -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
Zach Nelson 6c97061629 Merge branch 'master' into release/1.3.0 2026-05-09 21:19:15 -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
Zach Nelson 9b2d8388ef Merge branch 'master' into release/1.3.0 2026-05-08 22:08:28 -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
Zach Nelson bb3a44f584 Merge branch 'master' into release/1.3.0 2026-05-08 21:57:15 -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
Zach Nelson bd546093cb chore: Bump version to 1.3.0 2026-05-04 21:20:17 -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
Justin Mitchell 9b3885135f feat: Initial support for the x3 (#875)
## Summary

Adds Xteink X3 hardware support to CrossPoint Reader. The X3 uses the
same SSD1677 e-ink controller as the X4 but with a different panel
(792x528 vs 800x480), different button layout, and an I2C fuel gauge
(BQ27220) instead of ADC-based battery reading.

All X3-specific behavior is gated by runtime device detection — X4
behavior is unchanged.

Depends on community-sdk X3 support: open-x4-epaper/community-sdk#19
(merged).

## Changes

### HAL Layer

**HalGPIO** (`lib/hal/HalGPIO.cpp/.h`)
- I2C-based device fingerprinting at boot: probes for BQ27220 fuel
gauge, DS3231 RTC, and QMI8658 IMU to distinguish X3 from X4
- Detection result cached in NVS for fast subsequent boots
- Exposes `deviceIsX3()` / `deviceIsX4()` helpers used throughout the
codebase
- X3 button mapping (7 GPIOs vs X4's layout)
- USB connection detection and wake classification for X3

**HalDisplay** (`lib/hal/HalDisplay.cpp/.h`)
- Calls `einkDisplay.setDisplayX3()` before init when X3 is detected
- Requests display resync after power button / flash wake events
- Runtime display dimension accessors (`getDisplayWidth()`,
`getDisplayHeight()`, `getBufferSize()`)
- Exposed as global `display` instance for use by image converters

**HalPowerManager** (`lib/hal/HalPowerManager.cpp/.h`)
- X3 battery reading via I2C fuel gauge (BQ27220 at 0x55, SOC register)
- X3 power button uses GPIO hold for deep sleep

### Display & Rendering

**GfxRenderer** (`lib/GfxRenderer/GfxRenderer.cpp/.h`)
- Buffer size and display dimensions are now runtime values (not
compile-time constants) to support both panel sizes
- X3 anti-aliasing tuning: only the darker grayscale level is applied to
avoid washed-out text on the X3 panel. X4 retains both levels via
`deviceIsX4()` gate

**Image Converters** (`lib/JpegToBmpConverter`, `lib/PngToBmpConverter`)
- Cover image prescale target uses runtime display dimensions from HAL
instead of hardcoded 800x480

### UI Themes

**BaseTheme / LyraTheme** (`src/components/themes/`)
- X3 button position mapping for the different physical layout
- Adjusted UI element positioning for 792x528 viewport

### Boot & Init

**main.cpp**
- X3 hardware detection logging
- Adjusted init sequence for X3 (no `HalSystem::begin()` dependency on
X3 path)

**HomeActivity**
- Uses runtime `renderer.getBufferSize()` instead of static
`GfxRenderer::getBufferSize()`

FYI I did not add support for the gyro page turner. That can be it's own
PR.
2026-04-04 10:25:43 -05:00
+7 e6c6e72a24 chore(release): 1.2.0 Release Candidate (#1483)
Compile Release / build-release (push) Canceled after 0s
## Summary

It's been a little while since the last release, but the community has
been incredibly busy. With 155 changes from 48 contributors (30 of which
were new!), there was a lot to cover. Here are some of the highlights:

**🔤 Kerning, Ligatures, and Font Improvements**
Text rendering gets a significant upgrade with proper kerning and
ligature support, fixed-point fractional x-advance for more accurate
character placement, and font compression improvements that reduce flash
usage.

**📝 Footnotes**
Footnote anchor navigation lets you select a footnote reference and jump
to the footnote text, then jump back. Slim footnotes support is also
available for books that use inline footnotes.

**📖 EPUB Optimizer**
A new integrated EPUB optimizer can clean up and reprocess books for
better compatibility with the reader, directly from the device.

**🔋 Battery Charging Indicator**
You can now see when your device is actively charging, with a visual
indicator on the battery icon.

**💾 Crash Diagnostics**
When something goes wrong, the firmware now dumps a crash report to the
SD card — even without USB plugged in. This makes it much easier to
report and diagnose issues.

**🌐 New Languages**
The community continues to expand language support. New in this release:
Turkish, Danish, Finnish, Polish, Dutch, Belarusian, Italian, Ukrainian,
Romanian, Catalan, Vietnamese, and Kazakh — along with significant
improvements to existing translations.

**📂 File Management**
Multi-select file deletion, BMP image viewer in the file browser, hidden
directory browsing, and long-click file deletion from the file browser.

** Performance**
Under the hood, text layout switched from `std::list` to `std::vector`,
HTML entity lookups are now O(log(n)), font rendering is faster, image
decode is 5-20% faster with per-pixel overhead eliminated, and multiple
string allocation hot paths were eliminated. Pre-indexing of the next
chapter also reduces page-turn latency at chapter boundaries.

---

Along with all of the above, there are many other additions including
**WebDAV support**, **auto page turn**, **QR code for current page**,
**split status bar settings**, **screenshot capture**, **JSON-based
settings migration**, **light/dark theme groundwork**, and a long list
of stability fixes and translation improvements.

## What's Changed
### Features
* feat: Support for kerning and ligatures by @znelson in
https://github.com/crosspoint-reader/crosspoint-reader/pull/873
* feat: footnote anchor navigation by @Uri-Tauber in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1245
* feat: slim footnotes support by @Uri-Tauber in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1031
* feat: integrated epub optimizer by @zgredex and @pablohc in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1224
* feat: battery charging indicator (mirroring PR #537) by @jpirnay in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1427
* feat: dump crash report to sdcard by @ngxson in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1145
* feat: Implement silent pre-indexing for the next chapter in
EpubReaderActivity by @LSTAR1900 in
https://github.com/crosspoint-reader/crosspoint-reader/pull/979
* feat: upgrade platform and support webdav by @dexif in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1047
* feat: Auto Page Turn for Epub Reader by @GenesiaW in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1219
* feat: enhance file deletion functionality with multi-select by
@Jessica765 in
https://github.com/crosspoint-reader/crosspoint-reader/pull/682
* feat: Long Click for File Deletion through File Browser by @Levrk in
https://github.com/crosspoint-reader/crosspoint-reader/pull/909
* feat: Take screenshots by @el in
https://github.com/crosspoint-reader/crosspoint-reader/pull/759
* feat: Current page as QR by @el in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1099
* feat: Download links for web server by @el in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1039
* feat: Added BmpViewer activity for viewing .bmp images in file browser
by @Levrk in
https://github.com/crosspoint-reader/crosspoint-reader/pull/887
* feat: User setting for image display by @jpirnay in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1291
* feat: Show hidden directories in browser by @jpirnay in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1288
* feat: Prefer ".sleep" over "sleep" for custom image directory by
@jpirnay in
https://github.com/crosspoint-reader/crosspoint-reader/pull/948
* feat: Allow a local configuration file for custom compiles by @jpirnay
in https://github.com/crosspoint-reader/crosspoint-reader/pull/879
* feat: Migrate binary settings to json by @jpirnay in
https://github.com/crosspoint-reader/crosspoint-reader/pull/920
* feat: split status bar setting by @whyte-j in
https://github.com/crosspoint-reader/crosspoint-reader/pull/733
* feat: wrapped text in GfxRender, implemented in themes so far by
@iandchasse in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1141
* feat: Themed language screen by @CaptainFrito in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1020
* feat: set WiFi hostname to CrossPoint-Reader-XXXXXXXXXXXX by @dexif in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1107
* feat: Add maxAlloc to memory information by @jpirnay in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1152
* feat: replace picojpeg with JPEGDEC for JPEG image decoding by
@martinbrook in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1136
* feat: Add git branch to version information on settings screen by
@jpirnay in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1225
* feat: sort languages in selection menu by @ariel-lindemann in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1071
* feat: Latin Extended-B European glyphs by @znelson in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1157
* feat: Latin Extended-B European glyphs by @znelson in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1167
* feat: Vietnamese glyphs support by @danoooob in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1147
* feat: add Turkish translation by @barbarhan in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1192
* feat: add full Danish translation by @hajisan in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1146
* feat: Add Finnish translations by @plahteenlahti in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1133
* feat: Add Polish Language by @th0m4sek in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1155
* feat: add Dutch translation by @basvdploeg in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1204
* feat: add Belarusian translation by @dexif in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1120
* feat: Add full Italian translations by @andreaturchet in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1144
* feat: add Ukrainian translation by @mirus-ua in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1065
* feat: Add Kazakh (kk) language support by @fsocietyipa in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1377
* feat: added Romanian strings by @ariel-lindemann in
https://github.com/crosspoint-reader/crosspoint-reader/pull/987
* feat: add Catalan strings by @angeldenom in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1049
* feat: Make directories stand out more in local file browser: "[dir]"
instead of "dir" by @jpirnay in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1339
* feat: Add Polish strings for commits #1219,#1169,#1031 +tweaks by
@th0m4sek in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1227
* feat: Polish translation tweaks by @th0m4sek in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1193
### Fixes
* fix: Fix img layout issue / support CSS display:none for elements and
images by @jpirnay in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1443
* fix: Overlapping battery percentage on image pages with anti-aliasing
by @znelson in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1452
* fix: Fix prewarm perf when a page contains many styles by
@adriancaruana in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1451
* fix: use sleep routine from the original firmware by @ngxson in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1298
* fix: Prevent line breaks on common English contractions by @znelson in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1405
* fix: Build with -fno-exceptions by @znelson in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1412
* fix: Reduce flash usage by cleaning up I18n translations by @steka in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1401
* fix: jpeg resource cleanup by @jpirnay in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1320
* fix: back button in settings returns to tab bar first by @Cache8063 in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1354
* fix: Init lastSleepImage (edge case) by @jpirnay in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1360
* fix: Add special handling for apostrophe hyphenation by @jpirnay in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1318
* fix: Fix inter-word spacing rounding error in text layout by @znelson
in https://github.com/crosspoint-reader/crosspoint-reader/pull/1311
* fix: load access fault crash by @Uri-Tauber in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1370
* fix: Fix bootloop logging crash by @jpirnay in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1357
* fix: dump crash log without usb plugged, bump release log to INFO by
@ngxson in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1332
* fix: avoid zip filename overflow by @jpirnay in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1321
* fix: Hanging indent (negative text-indent) and em-unit sizing by
@jpirnay in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1229
* fix: Use fixed-point fractional x-advance and kerning for better text
layout by @znelson in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1168
* fix: use HTTPClient::writeToStream for downloading files from OPDS by
@osteotek in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1207
* fix: make file system operations thread-safe (HalFile) by @ngxson in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1212
* fix: properly implement requestUpdateAndWait() by @ngxson in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1218
* fix: prevent infinite render loop in Calibre Wireless after file
transfer by @pablohc in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1070
* fix: WiFi lifecycle and hyphenation heap defragmentation for KOReader
sync by @jpirnay in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1151
* fix: Fix coverRendered flag by @jpirnay in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1154
* fix: Handle non-ASCII characters in sanitizeFilename by @znelson in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1132
* fix: Update activity was missing "Back" button label by @znelson in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1128
* fix: force auto-hinting for Bookerly to fix inconsistent stem widths
by @adriancaruana in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1098
* fix: image centering bleed by @martinbrook in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1096
* fix: double free WebDAVHandler by @ngxson in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1093
* fix: Consider extra quotation styles when hyphenating quoted words by
@cbix in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1077
* fix: acquire power lock before sleeping by @ngxson in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1125
* fix: Unify inconsistent Wi-Fi/WiFi in Czech translation by @pepastach
in https://github.com/crosspoint-reader/crosspoint-reader/pull/1138
* fix: sdfat warning about redefinition of macro by @ngxson in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1135
* fix: Close leaked file descriptors in SleepActivity and web server by
@brbla in
https://github.com/crosspoint-reader/crosspoint-reader/pull/869
* fix: Enable DESTRUCTOR_CLOSES_FILE flag by @daveallie in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1075
* fix: Change "UI Font Size" to "Reader Font Size" by @divinitycove in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1171
* fix: Hide unusable button hints when viewing empty directory by @Levrk
in https://github.com/crosspoint-reader/crosspoint-reader/pull/1253
* fix: broken translations in status bar settings by @ariel-lindemann in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1188
* fix: clarity issue with ambiguous string `SET` by @ariel-lindemann in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1169
* fix: Crash (Load access fault) when indexing chapters containing
characters unsupported by bold/italic font variants by @Uri-Tauber in
https://github.com/crosspoint-reader/crosspoint-reader/pull/997
* fix: Increase PNGdec buffer size to support wide images by @osteotek
in https://github.com/crosspoint-reader/crosspoint-reader/pull/995
* fix: Use HalPowerManager for battery percentage by @vjapolitzer in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1005
* fix: Fix dangling pointer by @Uri-Tauber in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1010
* fix: re-implementing Cover Outlines for the new Lyra Themes by @Levrk
in https://github.com/crosspoint-reader/crosspoint-reader/pull/1017
* fix: use double FAST_REFRESH to prevent washout on large grey images
by @martinbrook in
https://github.com/crosspoint-reader/crosspoint-reader/pull/957
* fix: Fixed Image Sizing When No Width is Set by @DestinySpeaker in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1002
* fix: Strip unused CSS rules by @daveallie in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1014
* fix: continue reading card classic theme by @pablohc in
https://github.com/crosspoint-reader/crosspoint-reader/pull/990
* fix: Destroy CSS Cache file when invalid by @daveallie in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1018
* fix: Shorten "Forget Wifi" button labels to fit on button by
@lukestein in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1045
* fix: improve Spanish translations by @pablohc in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1054
* fix: Fixed book title in home screen by @DestinySpeaker in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1013
* fix: Fix hyphenation and rendering of decomposed characters by
@jpirnay in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1037
* fix: Improve and add Spanish translations by @DaniPhii in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1338
* fix: improve and add Spanish translations by @DaniPhii in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1254
* fix: improve and add Swedish translations by @steka in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1317
* fix: Extend missing / amend existing German translations by @jpirnay
in https://github.com/crosspoint-reader/crosspoint-reader/pull/1226
* fix: update french.yaml file to have a better French translation of
the CFW by @Spigaw in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1130
* fix: added romanian translation to new strings by @ariel-lindemann in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1105
* fix: add missing romanian strings by @ariel-lindemann in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1187
* fix: add new Ukrainian translation line for STR_SCREENSHOT_BUTTON by
@mirus-ua in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1149
* fix: Dutch translation prefix correction by @basvdploeg in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1223
* fix: Small typo in i18n.md regarding C++ identifiers by
@victordomingos in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1210
* fix: typo in USER_GUIDE.md by @arnaugamez in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1036
* fix: add missing keyboard metrics to Lyra3CoversTheme by @dexif in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1101

### Internal
* perf: font-compression improvements by @adriancaruana in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1056
* perf: Improve font drawing performance by @jpirnay in
https://github.com/crosspoint-reader/crosspoint-reader/pull/978
* perf: Replace std::list with std::vector in text layout by @znelson in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1038
* perf: Optimize HTML entities lookup to O(log(n)) by @Uri-Tauber in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1194
* perf: UITheme::getMetrics const and const-ref usage by @znelson in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1094
* perf: Avoid creating strings for file extension checks by @znelson in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1303
* perf: Eliminate per-pixel overheads in image rendering by @martinbrook
in https://github.com/crosspoint-reader/crosspoint-reader/pull/1293
* perf: Update github actions for optimal performance with pioarduino by
@Jason2866 in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1080
* style: Phase 1 - Simple light dark themes by @cdmoro in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1006
* refactor: implement ActivityManager by @ngxson in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1016
* refactor: Simplify REPLACEMENT_GLYPH fallback by @znelson in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1119
* refactor: Simplify new setting introduction by @jpirnay in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1086
* refactor: Use std binary search algorithms for font lookups by
@znelson in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1202
* refactor: rename MyLibrary to FileBrowser by @osteotek in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1260
* refactor: Avoid rebuilding cache path strings by @znelson in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1300
* refactor: reader utils by @Uri-Tauber in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1329
* chore: Remove miniz and modularise inflation logic by @daveallie in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1073
* chore: Resolve several build warnings by @daveallie in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1076
* chore: Removed generated language headers by @znelson in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1156
* chore: Added generated lang headers to .gitignore by @znelson in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1158
* chore: remove redundant xTaskCreate by @ngxson in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1264
* chore: Removed unused PlatformIO include directory placeholder by
@znelson in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1417
* chore: micro-optimisation: early exit on fillUncompressedSizes by
@jpirnay in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1322
* chore: change label while on settings tab actions by @jpirnay in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1325
* chore: add firmware size history script by @znelson in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1235
* chore: Add powershell script for clang-formatting by @jpirnay in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1472
* chore: Removed unused ConfirmationActivity member by @znelson in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1234
* chore: Update russian.yaml by @madebyKir in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1198
* chore: new Ukrainian translation lines by @mirus-ua in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1199
* chore: new Ukrainian localization strings by @mirus-ua in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1270
* chore: Polish localization for STR_DELETE by @JonaszPotoniec in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1323
* chore: Image settings Polish localization by @znelson in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1299
* chore: add missing Catalan strings by @angeldenom in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1302
* chore: add missing translations for Romanian by @ariel-lindemann in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1265
* chore: Add Portuguese (Portugal) translator to the list by
@victordomingos in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1211
* chore: Reduce flash usage by cleaning up I18n translations by @steka
in https://github.com/crosspoint-reader/crosspoint-reader/pull/1401
* docs: Add lightweight contributor onboarding documentation by @bilalix
in https://github.com/crosspoint-reader/crosspoint-reader/pull/894
* docs: ActivityManager migration guide by @znelson in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1222
* docs: USER_GUIDE.md update for 1.1.0 by @divinitycove in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1108
* docs: add quick KOReader sync setup guide by @wjhrdy in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1181
* docs: image support marked as completed by @ariel-lindemann in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1008
* feat: aiagent context definition by @jpirnay in
https://github.com/crosspoint-reader/crosspoint-reader/pull/922
* chore: Update SKILL.md to reflect generated i18n files are gitignored
by @znelson in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1423
* fix: ActivityManager tweaks by @znelson in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1220
* fix: Correct relative file paths in SKILL.md documentation by @pablohc
in https://github.com/crosspoint-reader/crosspoint-reader/pull/1304
* fix: add Technically Unsupported section to SCOPE.md by @Uri-Tauber in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1295

## New Contributors
* @DestinySpeaker made their first contribution in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1002
* @arnaugamez made their first contribution in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1036
* @angeldenom made their first contribution in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1049
* @cdmoro made their first contribution in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1006
* @bilalix made their first contribution in
https://github.com/crosspoint-reader/crosspoint-reader/pull/894
* @Jessica765 made their first contribution in
https://github.com/crosspoint-reader/crosspoint-reader/pull/682
* @brbla made their first contribution in
https://github.com/crosspoint-reader/crosspoint-reader/pull/869
* @dexif made their first contribution in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1047
* @mirus-ua made their first contribution in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1065
* @cbix made their first contribution in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1077
* @divinitycove made their first contribution in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1108
* @pepastach made their first contribution in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1138
* @Jason2866 made their first contribution in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1080
* @andreaturchet made their first contribution in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1144
* @Spigaw made their first contribution in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1130
* @iandchasse made their first contribution in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1141
* @th0m4sek made their first contribution in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1155
* @plahteenlahti made their first contribution in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1133
* @hajisan made their first contribution in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1146
* @madebyKir made their first contribution in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1198
* @victordomingos made their first contribution in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1210
* @basvdploeg made their first contribution in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1204
* @wjhrdy made their first contribution in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1181
* @DaniPhii made their first contribution in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1254
* @steka made their first contribution in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1317
* @barbarhan made their first contribution in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1192
* @JonaszPotoniec made their first contribution in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1323
* @Cache8063 made their first contribution in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1354
* @fsocietyipa made their first contribution in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1377
* @LSTAR1900 made their first contribution in
https://github.com/crosspoint-reader/crosspoint-reader/pull/979
* @zgredex made their first contribution in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1224

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

---------

Co-authored-by: jpirnay <jens@pirnay.com>
Co-authored-by: Dani Poveda <daniphii@outlook.com>
Co-authored-by: Baris Albayrak <80099286+barbarhan@users.noreply.github.com>
Co-authored-by: Barış Albayrak <barisa@pop-os.lan>
Co-authored-by: Stefan Blixten Karlsson <sbkarlsson@gmail.com>
Co-authored-by: Àngel <153315454+angeldenom@users.noreply.github.com>
Co-authored-by: Jonasz Potoniec <jonasz@potoniec.eu>
Co-authored-by: Егор Мартынов <martynovegorOF@yandex.ru>
Co-authored-by: Mirus <mirusim@gmail.com>
Co-authored-by: Spigaw <73850535+Spigaw@users.noreply.github.com>
Co-authored-by: ariel-lindemann <41641978+ariel-lindemann@users.noreply.github.com>
Co-authored-by: Nima Salami <54304457+hajisan@users.noreply.github.com>
Co-authored-by: Arthur Tazhitdinov <lisnake@gmail.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Bas van der Ploeg <bas@basvanderploeg.nl>
Co-authored-by: martin brook <martin.brook100@googlemail.com>
2026-04-03 17:33:02 -05:00
Poc031205 1d219ae27e feat: Add Hungarian language file (hungarian.yaml) (#1545)
Full Hungarian localization for the firmware with all UI elements and
system messages translated.

## Summary

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

Added hungarian.yaml language file
Translated all UI elements and system messages into Hungarian

## 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 am a native Hungarian speaker. The initial translation was assisted by
AI, but I reviewed and corrected all translation errors to ensure a
natural and accurate Hungarian localization.

### 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-03 13:03:43 -04:00
Tadasandcoderabbitai[bot] c4f11015f1 feat: Add Lithuanian transilation (#1526)
## Summary

* Add Lithuanian transilation

### 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: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2026-04-03 13:02:12 -04:00
Dexif abd2266048 chore: complete Belarusian translation (#1548)
Add 30 missing translation keys to belarusian.yaml

for #1483
2026-04-03 13:01:06 -04:00
martin brook 1df543d48d perf: Eliminate per-pixel overheads in image rendering (#1293)
## Summary

Replace per-pixel getRenderMode() + rotateCoordinates() + bounds checks
with a DirectPixelWriter struct that pre-computes orientation and render
mode state once per row. Use bitwise ops instead of division/modulo for
cache pixel packing. Skip PNG cache allocation when buffer exceeds 48KB
(framebuffer size) since PNG decode is fast enough that caching provides
minimal benefit, and the large buffer competes with the 44KB PNG decoder
for heap.

## Additional Context
Measured improvements on ESP32-C3 @ 160MHz:
- JPEG decode: 5-7% faster (1:1 scale)
- PNG decode: 15-20% faster (1:1 scale)
- Cache renders: 3-6% faster across both formats
- Eliminates "Failed to allocate cache buffer" errors for large PNGs

---

### 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-03-30 11:03:49 -05:00
Bas van der Ploeg 63961625a2 chore: update Dutch translations (#1503)
## Summary

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

## 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**_
2026-03-30 10:49:14 -05:00
ariel-lindemann 34484300e4 chore: update RO translation for release v1.2.0 (#1504) 2026-03-26 08:49:20 +02:00
Spigaw 831144e737 chore: Add missing French translations for UI controls and settings (#1493)
Added missing strings for the French translation.

## Summary

Adding the missing French translated strings before the next PR.
No modification done to the preexisting strings, only added new ones.



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

No AI usage.
2026-03-25 21:03:24 -05:00
Mirus 42c33528f9 feat: Add Ukrainian translations for image-related strings (#1491)
## Summary

Prep for RC 1.2.0
https://github.com/crosspoint-reader/crosspoint-reader/pull/1483
2026-03-25 20:59:18 -05:00
Егор Мартынов 6969950cd7 feat: update Russian translation (#1489)
## Summary

Adds new Russian strings for recently merged features, yay! 😃 

---

### AI Usage

Did you use AI tools to help write this code? _**NO**_
2026-03-25 11:35:10 -05:00
jpirnay bc6f6daeb5 chore: Update German translation strings (#1495)
## Summary

* **What is the goal of this PR?** Updating German language file
* **What changes are included?**

## Additional Context

- None

---

### 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-03-25 11:34:53 -05:00
Jonasz Potoniec 8352e1f08f chore: Polish localization for STR_SHOW_HIDDEN_FILES (#1490)
## Summary

Polish localization for `STR_SHOW_HIDDEN_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? _**NO**_
2026-03-25 11:34:40 -05:00
Àngel dfc38cca4c chore: Catalan localization for STR_SHOW_HIDDEN_FILES (#1494)
## Summary

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

Adds the missing Catalan translation for STR_SHOW_HIDDEN_FILES in
lib/I18n/translations/catalan.yaml.

* **What changes are included?**

Only modified catalan.yaml file

## 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-03-25 11:34:17 -05:00
Stefan Blixten Karlsson 3856348ee6 fix: swedish translation (#1476)
## Summary

* **What is the goal of this PR?** 
  fix swedish translation
* **What changes are included?**
  lib\I18n\translations\swedish.yaml
## 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-03-25 11:33:11 -05:00
Baris AlbayrakandBarış Albayrak 0e228324e6 chore: Turkish localization for STR_SHOW_HIDDEN_FILES (#1487)
Adds the missing Turkish translation for STR_SHOW_HIDDEN_FILES in
lib/I18n/translations/turkish.yaml.

- Added: STR_SHOW_HIDDEN_FILES: "Gizli Dosyaları Göster"
- Scope: Turkish only (single-key micro-fix)

This follows up the missing-translations callout in #1483.

Co-authored-by: Barış Albayrak <barisa@pop-os.lan>
2026-03-25 08:29:03 +02:00
Dani Poveda 8e091609f7 chore: Spanish localization for STR_SHOW_HIDDEN_FILES (#1486)
## Summary

* Adding Spanish translation for string `STR_SHOW_HIDDEN_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? _**NO**_
2026-03-25 08:25:55 +02:00
jpirnay 710055f02c feat: Make directories stand out more in local file browser: "[dir]" instead of "dir" (#1339)
## Summary

* **What is the goal of this PR?** It's difficult to distinguish
directory names from normal file entries, so they are displayed now as
"[dir]" instead of "dir" for classic theme
* **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-03-24 19:47:42 -05:00
jpirnay 0245972132 chore: Add powershell script for clang-formatting (#1472)
## Summary

* **What is the goal of this PR?** Add a windows equivalent for the
linux clang-format-fix script
* **What changes are included?**

## Additional Context
```
.SYNOPSIS
    Runs clang-format -i on project *.cpp and *.h files.

.DESCRIPTION
    Formats all C/C++ source and header files in the repository, excluding
    generated, vendored, and build directories (open-x4-sdk, builtinFonts,
    hyphenation tries, uzlib, .pio, *.generated.h).

    The clang-format binary path is resolved once and cached in
    .local/clang-format-fix.local. On first run it checks a default path,
    then PATH, then common install locations. Edit the .local file to
    override manually.

.PARAMETER g
    Format only git-modified files (git diff --name-only HEAD) instead of
    the full tree.

.PARAMETER h
    Show this help text.
```
---

### 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-03-23 18:04:59 -05:00
Zach Nelson 0cbfaa007d fix: Overlapping battery percentage on image pages with anti-aliasing (#1452)
## Summary

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

When viewing a page with images and anti-aliasing enabled, the
`imagePageWithAA` path renders the page twice with fast refreshes (blank
image area, then restore). Both passes called `renderStatusBar()`, which
reads the battery percentage live. If the value changed between the two
renders (e.g. 88% -> 87%), the digits would overlap on screen.
Fix: Removed the redundant `renderStatusBar()` from the second BW
render. The status bar is already drawn and displayed in the first pass,
and only the image area needs restoration.

---

### 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-03-23 22:05:15 +00:00
jpirnay ceb6acc8d7 fix: Fix img layout issue / support CSS display:none for elements and images (#1443)
## Summary
- Add CSS `display: none` support to the EPUB rendering pipeline (fixes
#1431)
- Parse `display` property in stylesheets and inline styles, with full
cascade resolution (element, class, element.class, inline)
- Skip hidden elements and all their descendants in
`ChapterHtmlSlimParser`
- Separate display:none check for `<img>` tags (image code path is
independent of the general element handler)
- Flush pending text blocks before placing images to fix layout ordering
(text preceding an image now correctly renders above it)
- Bump CSS cache version to 4 to invalidate stale caches
- Add test EPUB (`test_display_none.epub`) covering class selectors,
element selectors, combined selectors, inline styles, nested hidden
content, hidden images, style priority/override, and realistic use cases
2026-03-23 13:51:02 -05:00
pablohcandzgredex 7d56810ee6 feat: integrated epub optimizer (#1224)
## Problem

Many e-ink readers have limited image decoder support natively.
EPUBs with images in other formats than **baseline JPEG** frequently
cause:

- **Broken images**: pages render as blank, corrupted noise, or never
load
- **Slow rendering**: unoptimized images cause severe delays on e-ink
hardware, up to 7 seconds per page turn, with cover images taking up to
59 seconds to render
- **Broken covers**: the book thumbnail never generates

Fixing this today requires external tools before uploading.

---

## What this PR does

Adds an **optional, on-demand EPUB optimizer** to the file upload flow.
When enabled,
it converts all images to baseline JPEG directly in the browser — no
server, no internet,
no external tools needed.

**Conversion is opt-in. The standard upload flow is unchanged.**

---

## Real-world impact

The optimizer was applied in batch to **61 EPUBs**:
- 60 standard EPUBs: 198 MB → 55 MB (**−72.2%**, 143 MB saved)
- Text-dominant books: 8–46% smaller (covers and inline images
converted)
  - Image-heavy / illustrated books: 65–93% smaller
- 1 Large manga volume (594 MB): 594 MB → 72 MB (**−87.8%**, 522 MB
saved)
- EPUB structural integrity fully maintained — zero new validation
issues introduced across all 61 books

*Size and integrity analysis:
[epub-comparator](https://github.com/pablohc/epub-comparator)*

From that set, **17 books were selected** as a representative sample
covering different content
types: image-heavy novels, pure manga, light novels with broken images,
and text-dominant books.
Each was benchmarked on two devices running in parallel, one on `master`
and one
on `PR#1224` — measuring render time across ~30 pages per book on
average.

### Rendering bugs fixed

| Book | Problem (original) | After optimization |
|------|--------------------|--------------------|
| Fairy Tale — Stephen King | Cover took **59.7 s** to render | 2.1 s
(−96%) |
| Cycle of the Werewolf — Stephen King | Cover took **23.3 s** to render
| 1.7 s (−93%) |
| Tomie: Complete Deluxe Ed. — Junji Ito | Cover took **18.3 s** to
render | 2.0 s (−89%) |
| Joel Dicker — El tigre (Ed. Ilustrada) | Cover took **14.5 s** to
render | 1.4 s (−90%) |
| Jackson, Holly — Asesinato para principiantes | Cover failed
completely (blank) | 2.0 s ✓ |
| Sentenced to Be a Hero — Yen Press | Cover failed, **8 images failed
to load** | All fixed ✓ |
| Flynn, Gillian — Perdida | Cover failed completely (blank) | 1.6 s ✓ |
| Chandler, Raymond — Asesino en la lluvia | Cover failed completely
(blank) | 2.0 s ✓ |

### Page render times — image-heavy EPUBs (avg per page)

| Book | Pages | Avg original | Avg optimized | Improvement | File size
|
|------|-------|-------------|---------------|-------------|-----------|
| Fairy Tale — Stephen King | 30 | 3,028 ms | 1,066 ms | **−64.8%** |
32.4 MB → 9.1 MB (−72%) |
| Cycle of the Werewolf — Stephen King | 33 | 3,026 ms | 1,558 ms |
**−48.5%** | 35.1 MB → 2.9 MB (−92%) |
| Joel Dicker — El tigre (Ed. Ilustrada) | 16 | 1,846 ms | 1,051 ms |
**−43.1%** | 5.3 MB → 0.4 MB (−93%) |
| Tomie: Complete Deluxe Ed. — Junji Ito | 30 | 4,817 ms | 2,802 ms |
**−41.8%** | 593.8 MB → 72.2 MB (−87.8%) |
| Sentenced to Be a Hero — Yen Press | 30 | 1,719 ms | 1,388 ms |
**−19.2%** | 15.2 MB → 1.6 MB (−90%) |

### Text-heavy EPUBs — no regression

| Book | Pages | Avg original | Avg optimized | Delta |
|------|-------|-------------|---------------|-------|
| Christie — Asesinato en el Orient Express | 30 | 1,672 ms | 1,646 ms |
−1.6% |
| Flynn — Perdida | 30 | 1,327 ms | 1,291 ms | −2.7% |
| Dicker — La verdad sobre el caso Harry Quebert | 30 | 1,132 ms | 1,084
ms | −4.2% |
| Hammett — El halcón maltés | 30 | 1,009 ms | 966 ms | −4.3% |
| Chandler — Asesino en la lluvia | 30 | 989 ms | 1,007 ms | +1.8% |

*Differences within ±5% — consistent with device measurement noise.*

*Render time benchmark:
[epub-optimization-benchmark](https://github.com/pablohc/epub-optimization-benchmark)*

---
## How to use it

**Single file:**
1. Click **Upload** (top of the page) — a modal opens. Use **Choose
files** to select one EPUB from your device.
2. Check **Optimize**.
- *(Optional)* Expand **Advanced Mode** — adjust quality, rotation, or
overlap; set individual images to H-Split / V-Split / Rotate.
3. Click **Optimize & Upload**.

**Batch (2+ files):**
1. Click **Upload** (top of the page) — a modal opens. Use **Choose
files** to select multiple EPUBs from your device.
2. Check **Optimize**.
   - *(Optional)* Expand **Advanced Mode** — adjust quality.
3. Click **Upload** — all files are converted and uploaded sequentially.

Upload a batch of files, without optimization:
<img width="810" height="671" alt="image"
src="https://github.com/user-attachments/assets/d892ae13-0b87-4ea4-b6b8-340d56efc763"
/>

Batch file upload, with standard optimization:
<img width="809" height="707" alt="image"
src="https://github.com/user-attachments/assets/d32dbc88-1208-4555-bfcf-330ab91d2174"
/>

Optimization Phase (1/2):
<img width="807" height="1055" alt="image"
src="https://github.com/user-attachments/assets/fd4cd5f9-e56e-4ca1-9777-6926b9baf2bb"
/>

Upload Phase (2/2):
<img width="805" height="1065" alt="image"
src="https://github.com/user-attachments/assets/483294f0-02f0-4569-ae11-c10b3581d747"
/>

Batch upload successfully confirmed:
<img width="812" height="1043" alt="image"
src="https://github.com/user-attachments/assets/80c135bf-05c3-4c80-8755-2a04c68235bc"
/>

---

## Options

**Always active when the converter is enabled:**
- Converts PNG, WebP, BMP, GIF → baseline JPEG
- Smart downscaling to 480×800 px max (preserves aspect ratio)
- True grayscale for e-ink (BT.709 luminance, always on)
- SVG cover fix + OPF/NCX compliance repairs

**Advanced Mode (opt-in) — single file:**
- JPEG quality presets: 30% / 45% / 60% / 75% / **85%** (default) / 95%
- Rotation direction for split images: CW (default) / CCW
- Min overlap when splitting: 5% (default) / 10% / 15%
- Auto-download conversion log toggle (detailed stats per image)
- Per-image picker: set Normal / H-Split / V-Split / Rotate per image
individually,
  with "Apply to all" for bulk assignment

**Advanced Mode (opt-in) — batch (2+ files):**
- JPEG quality presets: 30% / 45% / 60% / 75% / **85%** (default) / 95%
- Auto-download conversion log toggle (aggregated stats for all files)

---

## ⚠️ Known limitations

**KoReader hash-based sync will break** for converted files. The file
content changes,
so the hash no longer matches the original. Filename-based sync is
unaffected.

If you rely on KoReader hash sync, use the Calibre plugin or the web
tool instead.

---
## Build size impact

| Metric | master (53beeee) | PR #1224 (a2ba5db) | Delta |

|---------------|------------------|--------------------|----------------|
| Flash used | 5,557 KB | 5,616 KB | +59 KB (+1.1%) |
| Flash free | 843 KB | 784 KB | −59 KB |
| Flash usage | 86.8% | 87.7% | +0.9 pp |
| RAM used | 95,156 B | 95,156 B | no change |

> Both builds compiled with `gh_release` environment in release mode
(ESP32-C3, 6,400 KB Flash).
> The +59 KB increase is entirely due to `jszip.min.js` embedded as a
> gzipped static asset served from Flash. RAM usage is identical,
> confirming no runtime overhead — the library runs in the browser,
> not on the ESP32. ~784 KB of Flash remain available.

---

## Alternatives considered

| Approach | Friction |
|----------|---------|
| **This PR** — integrated in upload flow | Zero: convert + upload in
one step, offline, any browser |
| Calibre plugin (in parallel development) | Requires a computer with
Calibre installed, same network |
| Web converters | Requires extra upload / download / transfer steps |

---

## Credits

Based on the converter algorithm developed by @zgredex.
Co-authored-by: @zgredex

---

### AI Usage

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

---------

Co-authored-by: zgredex <zgredex@users.noreply.github.com>
2026-03-22 19:53:15 +00:00
Xuan-Son NguyenandZach Nelson 526c8a5e7a fix: use sleep routine from the original firmware (#1298)
## Summary

Fixes #1263

I spent half of my day(-off) reverse engineering the stock english
firmware V3.1.1, it's more or less like solving a sudoku with some known
pieces (like debug strings, known static addresses, known compiled
function, etc) and then the task is to guess the rest.

Long story short, this is the sleep routine that they use:

<img width="674" height="604" alt="image"
src="https://github.com/user-attachments/assets/6d53ce44-7bae-40c7-b4fb-24f898dbcc05"
/>

From the code above:
- They pull down GPIO13 (value = 0xd) before sleep
- They verify that power button is released by doing a delay loop of
50ms, similar to what we're doing
- `esp_sleep_config_gpio_isolate` is called but I'm not 100% sure why
- Pull up power button, note that it's likely redundant because power
button should already pulled up by `InputManager`
- `param1` and `param2` means enabling front/side buttons for wake up,
but it doesn't used in the code in reality. But I think it's physically
impossible, see the explanation below
- `param3` means "wake up from power button"
- `esp_sleep_start` is used; there is a logic to handle if it fails to
sleep, then retry recursively (no idea why!)

My observation is that they use GPIO13 so that it will be on HIGH state
when the chip is powered on, without any user space code to keep it on
that state. And once going to deep sleep, it goes into FLOATING by
default. That may explain why it need to be in LOW state before going to
sleep. (Nice trick btw)

Looking again at the circuit diagram provided
[here](https://github.com/sunwoods/Xteink-X4/blob/main/readme-img/sch.jpg)
(note: it's not official):

<img width="705" height="384" alt="image"
src="https://github.com/user-attachments/assets/b98d59fd-47ca-4d3d-a24a-94bf999e957b"
/>

It kinda make sense as the GPIO13 and VBUS (USB VCC) have the same role,
they are part of a simple "battery protection" cirtuit

Now, we may wonder, how the device wake up when there is no battery at
all?

<img width="440" height="323" alt="image"
src="https://github.com/user-attachments/assets/2981c411-239b-49a7-b9f7-9a75b6c1b6d3"
/>

It seems like power button is not just a simple switch between GPIO3 and
ground, but it also linked the POWER_CTRL, which leads to nowhere on the
diagram, but I suppose it connects the battery back for a short amount
of time, just enough for the MCU to wake up, and GPIO13 goes HIGH again.
It may also explain why power button becomes non-responsive for ~1
second after power on, as it's being pulled up by the current from
battery (remind: high = not pressed, low = pressed)

To test the theory above, I simply **comment out** the
`esp_deep_sleep_enable_gpio_wakeup`:
- On battery, power button works as nothing happen
- On USB, it doesn't wake up, I need to press RST

---

Important things about my analysis:
1. I had to name every function on the code above **manually**, but I'm
99% confident about it. The only function that I'm not sure is
`esp_wifi_bt_power_domain_off` ; Edit: it was indeed mislabeled, see
https://github.com/crosspoint-reader/crosspoint-reader/pull/1298#discussion_r2879670852
2. Some logic inside the stock firmware looks very strange, there is
almost no mention to "arduino" in the hardware, suggesting that they may
just call esp-idf functions directly, bypassing the arduino abstraction.

---

### 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: Zach Nelson <zach@zdnelson.com>
2026-03-21 13:35:38 -05:00
Adrian Wilkins-Caruana 0c9e8b3ece fix: Fix prewarm perf when a page contains many styles (#1451)
## Summary

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

Fix prewarm perf when a page contains many styles.

The prewarm page buffer was a single slot, so each `prewarmCache` call
for a new font style freed the previous style's glyphs. On pages with
multiple styles (regular + bold + italic), only the last style was
prewarmed. The others fell through to the hot-group compaction path at
~2-3ms per glyph.

This was most visible on rich formatting (e.g. this [Czech prayer
book](https://stahuj.kancional.cz/e-kniha/kancional.epub) with bold
headings, italic liturgical text, and regular body), where page renders
took 3-5 seconds instead of ~700ms.

Fix: use up to 4 page buffer slots (one per font style) so all styles
stay prewarmed simultaneously.

Fixes #1450.

---

### 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 diagnose and
brainstorm solutions.
2026-03-21 12:10:41 -05:00
Zach Nelson 53beeeed2b chore: Removed unused PlatformIO include directory placeholder (#1417)
## Summary

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

This change deletes include/README, which is a PlatformIO boilerplate
placeholder file explaining what header files are. The include/
directory isn't used by this project (headers live in lib/ and src/), so
this is just cleanup.

### 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-03-19 23:20:50 -05:00
LSTARandJake Kenneally 8dd365b4da feat: Implement silent pre-indexing for the next chapter in EpubReaderActivity (#979)
## Summary

* A simple tweak to pre-index the next chapter silently during normal
reading.
* Triggers silent pre-indexing of the next chapter when the penultimate
page of a chapter is rendered to reduce visible interruptions.
* Keeps existing indexing with popup when a reader jumps directly into
an unindexed chapter.

## Additional Context

* Reader input is temporarily blocked during silent indexing to avoid
navigation/index state conflicts.
* The penultimate page is used because readers typically spend longer
there than on the final page.
* This change optimizes linear reading flow while preserving reliable
indexing for non-linear navigation.

## Possible Improvements

* Add a setting for First Page Indexing vs Penultimate Page Pre-indexing
* Display an indexing icon in the status bar instead of using a popup
that overlaps book text.

Tested on device:

https://www.dropbox.com/scl/fi/29g5kjqgsi5e4hgujv38u/Silent-Indexing.MOV?rlkey=yemi4mosmev5vicaa7gpe49qw&dl=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? _**YES**_

---------

Co-authored-by: Jake Kenneally <jakekenneally@gmail.com>
2026-03-19 23:20:26 -05:00
Zach Nelson 9665dd7473 chore: Update SKILL.md to reflect generated i18n files are gitignored (#1423)
## Summary

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

Update SKILL.md to stop instructing contributors to commit `I18nKeys.h`
and `I18nStrings.h`. All three generated i18n files have been gitignored
since ff577540 and are regenerated at build 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? _**NO**_
2026-03-19 23:19:54 -05:00
jpirnay 71719e1d94 feat: battery charging indicator (mirroring PR #537) (#1427)
## Summary

* **What is the goal of this PR?** All praise goes to @didacta for his
PR #537. Just picked up the reviewer comments to contain the changes as
suggested (there was no response for more than 6 weeks, so I wanted to
reanimate this feature).

Just one addition: should recognize usb cable plug ins / retractions and
update the icon immediately

* **What changes are included?**

## Additional Context

see #537 

---

### 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-03-19 22:33:29 -05:00
Zach Nelson d6951f81b7 fix: Build with -fno-exceptions (#1412)
## Summary

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

Until today, I sincerely thought we were building without exception
support. The codebase is not set up with any exception handling
infrastructure. The SKILL.md file specifies no exceptions.

I just learned that we actually were building with exceptions enabled,
with `-fexceptions` coming from
~/.platformio/packages/framework-arduinoespressif32-libs/esp32c3/pioarduino-build.py.

This change removes the `-fexceptions` flag and adds `-fno-exceptions`.
The result is **53,670 bytes in flash savings**.

---

### 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-03-18 11:41:18 -05:00
Stefan Blixten Karlsson 99721c081b fix: Reduce flash usage by cleaning up I18n translations (#1401)
## Summary

* **What is the goal of this PR?** 
Removing no longer used i18n keys/string, to reduce (~28k) used flash
space.
To correct to swedish translations for `STR_FONT_SIZE` and
`STR_KOREADER_SYNC`.

* **What changes are included?**
   `lib\I18n\translations\*`

## 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-03-18 09:59:03 -05:00
jpirnay f9286709d1 feat: Show hidden directories in browser (#1288)
## Summary

* **What is the goal of this PR?** Add setting to display hidden files /
directories in filebrowser / web file browser
* **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-03-18 09:57:58 -05:00
Zach Nelson dc39480349 fix: Prevent line breaks on common English contractions (#1405) 2026-03-16 20:04:06 -04:00
jpirnay b5df6cb2b5 fix: jpeg resource cleanup (#1320)
## Summary

* **What is the goal of this PR?** Fix leak on decode error path in JPEG
converter
* **What changes are included?**
Unif resource cleanup

## 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? _**PARTIALLY**_
Identification of the issue by AI
2026-03-13 17:45:42 -05:00
fsocietyipaandOkzhetpes 16b73744c5 feat: Add Kazakh (kk) language support (#1377)
## Summary

* **What is the goal of this PR?** Implements Kazakh language
* **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

---------

Co-authored-by: Okzhetpes <okhzetpes.20@gmail.com>
2026-03-11 18:40:14 -05:00
jpirnay 79b54b3a75 fix: Init lastSleepImage (edge case) (#1360)
## Summary

* **What is the goal of this PR?** fix edge case for definition

## Additional Context

If loadFromFile() returns false (no state file exists — first boot, or
SD missing), lastSleepImage is never set and contains garbage.
[SleepActivity.cpp:83] then uses it in a while comparison to avoid
repeating the same image. The JSON path (doc["lastSleepImage"] |
(uint8_t)0) handles it, but only if the file exists.

---

### 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-03-11 18:37:13 -05:00
jpirnay 11ca208ec2 chore: change label while on settings tab actions (#1325)
## Summary

* **What is the goal of this PR?** The "Toggle" label while on the "tab"
actions of settings screen was misleading. Will show "Select" now ,
while "Toggle" remains in place for all 'real' settings

* **What changes are included?**

## Additional Context
<img width="240" alt="1"
src="https://github.com/user-attachments/assets/dc198716-0aad-4c75-96fe-52595625e69d"
/>
<img width="240" alt="2"
src="https://github.com/user-attachments/assets/85ce5368-801c-489d-aa94-51f126c3ddc8"
/>


---

### 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-03-11 18:36:34 -05:00
7a28f90dad fix: back button in settings returns to tab bar first (#1354)
## Summary
- Pressing Back while browsing settings within a category now jumps
focus to the category tab bar
- Pressing Back again from the tab bar exits settings to home
- Previously, Back always exited directly to home regardless of scroll
position

Closes #797

## Test plan
- [ ] Scroll deep into a settings category → press Back → tab bar is
focused
- [ ] Press Back again from tab bar → exits to home screen
- [ ] Use category switching (continuous hold) → still works as before
- [ ] Settings are saved on exit (not on tab-bar jump)

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

Co-authored-by: bkb <bkb@arcnode.xyz>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-11 18:35:48 -05:00
jpirnay 3dabd30287 fix: Add special handling for apostrophe hyphenation (#1318)
## Summary

* **What is the goal of this PR?** Fixing / extending the hyphenation
logic to deal with words containing an apostophe as raised in #1186
* **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? _**PARTIALLY**_ (as the
user provided a thorough analysis that I followed)
2026-03-11 18:35:23 -05:00
Adrian Wilkins-Caruana f1e9dc7f30 perf: font-compression improvements (#1056)
## Purpose

This PR includes some preparatory changes that are needed for an
upcoming performant CJK font feature. The changes have no impact on
render time and heap allocation for latin text. **Despite this, I think
these changes stand on their own as a better font
compression/decompression implementation.**

## Summary

- Font decompressor rewrite: Replaced the 4-slot LRU group cache with a
two-tier system — a page buffer (glyphs prewarmed before rendering
begins) and a hot-group fallback (last decompressed group retained for
non-prewarmed
  glyphs). 
- Byte-aligned compressed bitmap format: Glyph bitmaps within compressed
groups are now stored row-padded rather than tightly packed before
DEFLATE compression, improving compression ratios by making identical
pixel rows produce
identical byte patterns. Glyphs are compacted back to packed format on
demand at render time. Reduces flash size by 155 KB.
- Page prewarm system: Added `Page::collectText` and
`Page::getDominantStyle` to extract per-style glyph requirements before
rendering, and `GfxRenderer::prewarmFontCache` to pre-decompress only
the groups needed for the dominant style
   — eliminating mid-render decompression for the common case.
- UTF-8 robustness fixes: `utf8NextCodepoint` now validates continuation
bytes and returns a replacement glyph on malformed input;
`ChapterHtmlSlimParser` correctly preserves incomplete multi-byte
sequences across word-buffer flush
  boundaries rather than splitting them.

---

### 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**_ Architecture and
design was done by me, refined a bit by Claude. Code mostly by Claude,
but not entirely.
2026-03-11 21:05:46 +01:00
Dani Poveda b467ea7973 fix: Improve and add Spanish translations (#1338)
## Summary

* **What is the goal of this PR?** 
    - Improve and add the latest missing Spanish translations

* **What changes are included?**
    - Replaced `Der.` with `Dcha.`, as it's a more common abbreviation
- Replaced `Selec.` with `Selecc.` (same reason as in the previous case)
- Added missing strings `STR_IMAGES`, `STR_IMAGES_DISPLAY`,
`STR_IMAGES_PLACEHOLDER` and `STR_IMAGES_SUPPRESS`
    - Rewording certain translations to make them clearer

---

### 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-03-11 23:02:22 +03:00
Zach Nelson 32a5c1c358 fix: Fix inter-word spacing rounding error in text layout (#1311)
## Summary

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

### Problem

Inter-word gap widths were computed as two separately-snapped integers:

```cpp
gap = getSpaceWidth(fontId, style);                        // fp4::toPixel(spaceAdvance)
gap += getSpaceKernAdjust(fontId, leftCp, rightCp, style); // fp4::toPixel(kern1 + kern2)
```

Because `fp4::toPixel(a) + fp4::toPixel(b)` can differ from
`fp4::toPixel(a + b)` by +/-1 pixel when the fractional parts straddle a
rounding boundary, each inter-word space could be one pixel wider or
narrower than the correct value. This affected line-break width
decisions and word-position accumulation across the whole paragraph
layout pipeline.

### Fix

Replaces `getSpaceKernAdjust()` with `getSpaceAdvance(fontId, leftCp,
rightCp, style)`, which combines the space glyph advance and both
flanking kern values (`kern(leftCp, ' ')` + `kern(' ', rightCp)`) into a
single fixed-point sum before the snap:

```cpp
return fp4::toPixel(spaceAdvanceFP + kern(leftCp, ' ') + kern(' ', rightCp));
```

This is the same single-snap pattern already used by `getTextAdvanceX`
for word widths.

### Changes

- **`GfxRenderer`**: Replaces `getSpaceKernAdjust()` with
`getSpaceAdvance()`. `getSpaceWidth()` is retained for the
single-space-word case in `measureWordWidth` where no adjacent-word kern
context is available.
- **`ParsedText`**: All four call sites (`computeLineBreaks`,
`computeHyphenatedLineBreaks`, and both loops in `extractLine`) updated
to use `getSpaceAdvance()`. The now-redundant `spaceWidth`
pre-computation and parameter are removed from all three internal layout
functions.

---

### 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 for
correctness**_
2026-03-10 22:55:23 -05:00
Uri Tauber a95a63b753 fix: load access fault crash (#1370)
## Summary

Fixes a crash (load access fault) when opening EPUB chapters whose first
text block exceeds 750 words.

## Changes

* **Crash fix (`addLineToPage`)**: Added a null guard for `currentPage`.
If `makePages()` hasn't run yet (which can happen when the first block
triggers the "text block too long" split path), the page is now created
on demand.
* **Layout fix (`characterData`)**: The early-split path previously used
`viewportWidth`, ignoring CSS margins and padding. It now computes
`effectiveWidth` using `totalHorizontalInset()`, consistent with
`makePages()`.

## Additional Context

* Crash signature: `MCAUSE=0x5` (load access fault), `A0=0x0` (`Page*`
null), `MTVAL=0x4 / 0x8` (offsets into `Page::elements`).
* Confirmed in two user reports reported in #1328
* Tested on PR #1357 (not on `master`).

---

### 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-03-09 16:57:40 -05:00
jpirnay 4104fa8102 fix: Fix bootloop logging crash (#1357)
## Summary

* **What is the goal of this PR?** On a cold boot (or after a crash that
corrupts RTC RAM), logHead contains garbage. Then addToLogRingBuffer
does: ``strncpy(logMessages[logHead], message, MAX_ENTRY_LEN - 1); ``
With garbage logHead, this computes a completely invalid address. The %
MAX_LOG_LINES guard on line 16 only runs after the bad store, which is
too late. The fix is to clamp logHead before use.

## 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**_ (did use claude
for the magic hash value)
2026-03-09 21:53:38 +01:00
jpirnay e60ba7620d chore: micro-optimisation: early exit on fillUncompressedSizes (#1322)
## Summary

* **What is the goal of this PR?** Avoid repeated full central-directory
scans by using early stop when all requested targets are already
matched.
* **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? _** PARTIALLY **_
Identified by AI
2026-03-08 14:15:51 -05:00
Uri Tauber cd508d27d5 refactor: reader utils (#1329)
## Summary

Extract shared reader utilities (`ReaderUtils.h`) to reduce duplication
across `EpubReaderActivity`, `TxtReaderActivity`, and (upcoming)
`MarkdownReaderActivity`.

  Utilities extracted:

   - `applyOrientation()` — orientation switch logic
   - `detectPageTurn()` — page navigation input detection
   - `renderAntiAliased()` — grayscale anti-aliasing pass
   - `displayWithRefreshCycle()` — refresh mode cadence
   - `GO_HOME_MS` — back button timing constant

  ## Impact

Flash: 32 bytes saved (6006441 → 6006409 bytes). Minimal immediate gain,
but meaningful once markdown reader and future reader types share these
functions.

Code quality: Eliminates ~100 lines of duplicated logic spread across
multiple files. All readers now follow the same patterns for
orientation, input handling, and rendering.

  ## Rationale

This refactor is preparation for markdown support, which requires
identical input and rendering logic. Instead of copy-pasting these
patterns a third time, all readers now share a single, tested
implementation. Future reader types can reuse `ReaderUtils` without
duplication.

  ---

  ## AI Usage

Did you use AI tools to help write this code? YES Claude extracted the
code, under my guidance. Tested on my device and seems to work fine.
2026-03-08 11:45:54 -05:00
Jonasz Potoniec 170cc25774 chore: Polish localization for STR_DELETE (#1323) 2026-03-06 21:22:52 -05:00
Xuan-Son Nguyenandcoderabbitai[bot] c40e92e4d1 fix: dump crash log without usb plugged, bump release log to INFO (#1332)
## Summary

Follow-up
https://github.com/crosspoint-reader/crosspoint-reader/pull/1145

- Fix log not being record without USB connected
- Bump release log to INFO for more logging details

---

### 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-03-06 22:05:23 +01:00
Uri Tauber 4d22256745 feat: footnote anchor navigation (#1245)
## Summary: Enable footnote anchor navigation in EPUB reader

This PR extracts the core anchor-to-page mapping mechanism from PR #1143
(TOC fragment navigation) to provide immediate footnote navigation
support. By merging this focused subset first, users get a complete
footnote experience now while simplifying the eventual review and merge
of the full #1143 PR.

  ---

## What this extracts from PR #1143

PR #1143 implements comprehensive TOC fragment navigation for EPUBs with
multi-chapter spine files. This PR takes only the anchor resolution
infrastructure:

- Anchor-to-page mapping in section cache: During page layout,
ChapterHtmlSlimParser records which page each HTML id attribute lands
on, serializing the map into the .bin cache file.
- Anchor resolution in `EpubReaderActivity`: When navigating to a
footnote link with a fragment (e.g., `chapter2.xhtml#note1`), the reader
resolves the anchor to a page number and jumps directly to it.
- Section file format change: Bumped to version 15, adds anchor map
offset in header.

  ---

## Simplified scope vs. PR #1143

To minimize conflicts and complexity, this PR differs from #1143 in key
ways:

* **Anchors tracked**
  * **Origin:** Only TOC anchors (passed via `std::set`)
  * **This branch:** All `id` attributes

* **Page breaks**
  * **Origin**: Forces new page at TOC chapter boundaries
  * **This branch:** None — natural flow

* **TOC integration**
  * **Origin**: `tocBoundaries`, `getTocIndexForPage()`, chapter skip
  * **This branch:** None — just footnote links

* **Bug fix**
  * **This branch:** Fixed anchor page off-by-1/2 bug


The anchor recording bug (recording page number before `makePages()`
flushes previous block) was identified and fixed during this extraction.
The fix uses a deferred `pendingAnchorId` pattern that records the
anchor after page completion.

  ---

## Positioning for future merge

Changes are structured to minimize conflicts when #1143 eventually
merges:

- `ChapterHtmlSlimParser.cpp` `startElement()`: Both branches rewrite
the same if `(!idAttr.empty())` block. The merged version will combine
both approaches (TOC anchors get page breaks + immediate recording;
footnote anchors get deferred recording).
- `EpubReaderActivity.cpp` `render()`: The `pendingAnchor` resolution
block is positioned at the exact same insertion point where #1143 places
its `pendingTocIndex` block (line 596, right after `nextPageNumber`
assignment). During merge, both blocks will sit side-by-side.
   
  ---

## Why merge separately?

1. Immediate user value: Footnote navigation works now without waiting
for the full TOC overhaul
   2. Easier review: ~100 lines vs. 500+ lines in #1143 
3. Bug fix included: The page recording bug is fixed here and will carry
into #1143
4. Minimal conflicts: Structured for clean merge — both PRs touch the
same files but in complementary ways
---

### AI Usage

Did you use AI tools to help write this code? _**< YES >**_ Done by
Claude Opus 4.6
2026-03-06 21:10:45 +03:00
Xuan-Son Nguyenandcoderabbitai[bot] 18b36efbae feat: dump crash report to sdcard (#1145)
## Summary

This allow dumping crash message (i.e. assertion fail) and stack trace
to `crash_report.txt` file on sdcard. The stack trace can then be
decoded using https://esphome.github.io/esp-stacktrace-decoder/

Could be useful to debug things like
https://github.com/crosspoint-reader/crosspoint-reader/issues/1137 where
error doesn't always happen.

May also be useful to show a screen to tell what happen (show on next
boot after crash), similar to [flipper zero crash
message](https://www.reddit.com/r/flipperzero/comments/10f8m3f/anyone_who_can_tell_me_why_this_message_pops_up/)
, but this is better to be a dedicated PR (I'm missing the
`drawTextWrapped` function, too lazy to code it ; update: exactly what I
need in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1141)

To test this:
- Option 1: add an `assert(false)` somewhere in the code
- Option 2: try dereferencing a nullptr
- Option 3: try `throw` an exception

Example of a crash report:

```
CrossPoint version: 1.1.0-dev

Panic reason: abort() was called at PC 0x4214585b on core 0

Recent logs:
[196] [DBG] [GFX] Time = 2 ms from clearScreen to displayBuffer
[1831] [DBG] [RBS] Recent books loaded from file (7 entries)
[1832] [DBG] [ACT] Exiting activity: Boot
[1832] [DBG] [ACT] Entering activity: Home
[1891] [DBG] [GFX] Time = 54 ms from clearScreen to displayBuffer
[2521] [DBG] [GFX] Time = 46 ms from clearScreen to displayBuffer
[4839] [DBG] [PWR] Going to low-power mode
[10048] [INF] [MEM] Free: 134164 bytes, Total: 232372 bytes, Min Free: 133664 bytes
[20060] [INF] [MEM] Free: 134164 bytes, Total: 232372 bytes, Min Free: 133664 bytes
[30072] [INF] [MEM] Free: 134164 bytes, Total: 232372 bytes, Min Free: 133664 bytes
[34453] [DBG] [PWR] Restoring normal CPU frequency
[34485] [DBG] [GFX] Time = 30 ms from clearScreen to displayBuffer
[35182] [DBG] [GFX] Time = 31 ms from clearScreen to displayBuffer
[36675] [DBG] [GFX] Time = 30 ms from clearScreen to displayBuffer
[38800] [DBG] [GFX] Time = 30 ms from clearScreen to displayBuffer
[40079] [INF] [MEM] Free: 134164 bytes, Total: 232372 bytes, Min Free: 133664 bytes


Stack memory:
0x3FCB0650: 0x00000000 0x00000000 0x3FCB0668 0x4038DBB6 0x00000000 0x00000000 0x3FCA0030 0x3FC936D0 
0x3FCB0670: 0x3FCB067C 0x3FC936EC 0x3FCB0668 0x34313234 0x62353835 0x00000000 0x726F6261 0x20292874 
0x3FCB0690: 0x20736177 0x6C6C6163 0x61206465 0x43502074 0x34783020 0x35343132 0x20623538 0x63206E6F 
0x3FCB06B0: 0x2065726F 0x00000030 0x3FCA0000 0xB37A603F 0x00000001 0x3FCA7000 0x3FCABCDC 0x4214585E 
0x3FCB06D0: 0x3FCA7000 0x3FCA7000 0x3FCABCDC 0x421458AA 0x3FCABCDC 0x3FCA7000 0x3FCABCDC 0x421459CC 
0x3FCB06F0: 0x3FCA7000 0x3FCA7000 0x42145D5A 0x3C205624 0x40388560 0x3FCA7000 0x3FCABCFC 0x42079866 
0x3FCB0710: 0x3FCA7000 0x3FCA7000 0x00009C9A 0x4207B7F6 0x3FCA7000 0x42090000 0x001B7740 0x00000001 
0x3FCB0730: 0x3FCA7000 0x3FCA7000 0x00000001 0x600C0028 0x00000001 0x3FCA1000 0x00000000 0x00000000 
0x3FCB0750: 0x00000000 0x00000000 0x00000000 0xB37A603F 0x00000000 0x00000000 0x00000000 0x00000000 
0x3FCB0770: 0x00000000 0x00000000 0x00000000 0x00000000 0x00000000 0x42090000 0x3FCA7000 0x4208F9C4 
0x3FCB0790: 0x00000000 0x00000000 0x00000000 0x40388368 0x00000000 0x00000000 0x00000000 0x00000000 
0x3FCB07B0: 0x00000000 0x00000000 0x00000000 0x00000000 0x00000000 0xA5A5A5A5 0xA5A5A5A5 0xA5A5A5A5 
0x3FCB07D0: 0xA5A5A5A5 0xA5A5A5A5 0xA5A5A5A5 0xA5A5A5A5 0xBAAD5678 0xDA6D3601 0x5EB5B9C5 0x2602E480 
0x3FCB07F0: 0x2BCDD33F 0x15556D4A 0x1F2140A0 0x5D59BEE3 0x8E76449F 0x6FB2D0CE 0xF5F46FAC 0x0112946A 
0x3FCB0810: 0x3B0B32E0 0x7A52B537 0x46801DB4 0xDA85DF9F 0x37E83D20 0x12861028 0x47A702BB 0x287A3C8A 
0x3FCB0830: 0x03632209 0xD44C5489 0x5E258453 0xFDA77529 0xE6748E23 0xADCF1394 0x67AD6778 0x2C208663 
0x3FCB0850: 0xC7985786 0xD4AA3AB2 0x312E1760 0xEC7AEAAE 0x1857020E 0x48003E7E 0xD6CB8763 0x9B4A3F66 
0x3FCB0870: 0x4B79E9F6 0xCBF739F0 0x3794C641 0xD0DBA3CB 0x95B9BE15 0x581C9983 0xDE62EFB6 0x20C67C5B 
0x3FCB0890: 0x1E4A3DF3 0xFB317C74 0xC0D86103 0x1D79ED56 0x72FE0862 0x3D38B0C8 0xD27EB587 0x0E0A4C40 
0x3FCB08B0: 0xF643ADC0 0x56D114D7 0x703AF879 0xAC7F3075 0x89C78C23 0xEDA86814 0xF767B3E3 0x0528838F 
0x3FCB08D0: 0x50ED4662 0x11FD38E7 0x8A5A83BB 0x658159BD 0x781AF696 0x8A700F79 0x526DDE23 0xC8472505 
0x3FCB08F0: 0x21AACC02 0xCB89369E 0xB82E5BE2 0x4C6C9D7D 0x9E724D9B 0xDC1067F7 0x84478FBC 0x4E89C444 
0x3FCB0910: 0x973F4229 0x49F93DA8 0xE30200F6 0xD1B5C391 0x8363A89F 0x2409E74C 0x3AFF7B52 0xCBEC2349 
0x3FCB0930: 0xD38F6695 0xBC3EA980 0xF067EBB1 0x7F87D167 0x92B3823B 0x9F0617D7 0xA7537C57 0x12CAB3D4 
0x3FCB0950: 0xC82EEE37 0x84D4B4BC 0xE1E2261C 0x488F0ADA 0x96EAF2FF 0x0BC493A0 0xCE614467 0x3829053D 
0x3FCB0970: 0xA41156BE 0x2747B77D 0x64DEA90B 0xE704AB0A 0xE4B01006 0x8D51903C 0x56CD3CF2 0x07E0A8E8 
0x3FCB0990: 0xD1DE05CE 0x33368522 0xD1889988 0x3A3097F4 0xB0796D09 0xC78948AA 0x6DEFC56E 0xD5C2E1D9 
0x3FCB09B0: 0xFD6DD8FA 0xA957B675 0xC202D80D 0x733FF8F4 0xA1484913 0x0B9AFBA6 0x330C07EA 0x2C09AD4C 
0x3FCB09D0: 0x3B1E08F7 0x3FCAE7D0 0x00000170 0xABBA1234 0x0000015C 0x3FCB00E0 0x00009C93 0x3FCA13C4 
0x3FCB09F0: 0x3FCA13C4 0x3FCB09E4 0x3FCA13BC 0x00000018 0x00000000 0x00000000 0x3FCB09E4 0x00000000 
0x3FCB0A10: 0x00000001 0x3FCAE7E0 0x706F6F6C 0x6B736154 0x00000000 0x00000000 0x3FCB07D0 0x00000005 
0x3FCB0A30: 0x00000000 0x00000001 0x00000000 0x3FCAB444 0x4209AFF0 0x0017E38F 0x00000000 0x3FCA7BD0 

```

---

### 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-03-06 17:46:13 +01:00
jpirnay a35f372e1b fix: avoid zip filename overflow (#1321)
## Summary

* **What is the goal of this PR?** Potential stack buffer overflow from
untrusted ZIP entry name length
* **What changes are included?** If nameLen >= 256 , this writes past
the stack buffer. Risk: memory corruption/crash on malformed EPUB/ZIP.

## 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 **_ Issue
identified by AI
2026-03-05 21:25:17 -06:00
4ef433e373 feat: add turkish translation (#1192)
Description
  This Pull Request introduces Turkish language support to CrossPoint
  Reader firmware.


  Key Changes:
- Translation File: Added lib/I18n/translations/turkish.yaml with 315
translated
     string keys, covering all system UI elements.
- I18N Script Update: Modified scripts/gen_i18n.py to include the "TR"
     abbreviation mapping for Turkish.
- System Integration: Regenerated I18N C++ files to include the new
Language::TR
     enum and STRINGS_TR array.
- UI Availability: The language is now selectable in the Settings menu
and
     correctly handles Turkish-specific characters (ç, ğ, ı, ö, ş, ü).
- Documentation: Updated docs/i18n.md to include Turkish in the list of
     supported languages.

  Testing:
   - Verified the build locally with PlatformIO.
- Flashed the firmware to an Xteink X4 device and confirmed the Turkish
UI
     renders correctly.
---

### AI Usage

Did you use AI tools to help write this code?  Yes Gemini

---------

Co-authored-by: Baris Albayrak <baris@Bariss-MacBook-Pro.local>
Co-authored-by: Barış Albayrak <barisa@pop-os.lan>
2026-03-05 18:24:44 -06:00
Stefan Blixten Karlsson a5d7e03f54 fix: improve and add Swedish translations (#1317)
## Summary

* **What is the goal of this PR?**
  * Improve and add the latest missing Swedish translations.

* **What changes are included?**
* Added missing Swedish translations in
`lib\I18n\translations\swedish.yaml`
  
## Additional Context

* (none)

---

### 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-03-05 14:25:29 -06:00
ariel-lindemann 047b0029c9 chore: add missing translations for Romanian (#1265)
## Summary

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

added Romanian ranslations from recent commits.

---

### 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-03-05 10:19:17 -06:00
Zach Nelson c3f1dbfa09 perf: Avoid creating strings for file extension checks (#1303)
## Summary

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

This change avoids the pattern of creating a `std::string` using
`.substr` in order to compare against a file extension literal.
```c++
std::string path;
if (path.length() >= 4 && path.substr(path.length() - 4) == ".ext")
```

The `checkFileExtension` utility has moved from StringUtils to
FsHelpers, to be available to code in lib/. The signature now accepts a
`std::string_view` instead of `std::string`, which makes the single
implementation reusable for Arduino `String`.

Added utility functions for commonly repeated extensions.

These changes **save about 2 KB of flash (5,999,427 to 5,997,343)**.

---

### 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-03-05 10:12:22 -06:00
Zach Nelson ea88797c8e chore: Image settings Polish localization (#1299)
## Summary

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

Quick follow up to #1291, adding Polish translations suggested by
@th0m4sek

---

### 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-03-05 19:08:36 +03:00
pablohc 218201bd1f fix: Correct relative file paths in SKILL.md documentation (#1304)
## Summary

* **What is the goal of this PR?**
Update relative paths to correctly navigate from .skills/ directory to
project root by adding ../ prefix to file references.

* **What changes are included?**
.skills/SKILL.md

---

### 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-03-05 08:36:53 -06:00
Àngel 6ee05b08a1 chore: add missing Catalan strings (#1302)
## Summary

* **What is the goal of this PR?**
Add missing Catalan strings.

* **What changes are included?**
Changes on catalan.yaml file only.


### 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-03-05 09:28:25 -05:00
Zach Nelson a826569a0f refactor: Avoid rebuilding cache path strings (#1300)
## Summary

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

Avoid building cache path strings twice, once to check existence of the
file and a second time to delete the 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-03-04 20:48:02 -05:00
jpirnayandArthur Tazhitdinov 88594077aa fix: Extend missing / amend existing German translations (#1226)
## Summary

* **What is the goal of this PR?** Extend missing / amend existing
German translations
* **What changes are included?** German.yaml

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

Co-authored-by: Arthur Tazhitdinov <lisnake@gmail.com>
2026-03-03 11:02:04 -05:00
jpirnay ce0b439aa3 feat: User setting for image display (#1291)
## Summary

**What is the goal of this PR?** Add a user setting to decide image
support: display, show placeholder instead, supress fully

Fixes #1289

---

### 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-03-03 09:59:06 -06:00
Uri Tauber 019587bb77 fix: add Technically Unsupported section to SCOPE.md (#1295)
## Summary

**Goal of this PR**
Add an **"In-scope — technically not supported"** section to `SCOPE.md`.

This clarifies hardware/UX limitations (e.g., clock support) and is
intended to reduce recurring feature requests on topics already
discussed.

Based on discussions in #287 and #626.

---

### 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-03-03 09:57:57 -06:00
Dave Allie 4388bf8cc7 fix: Enable DESTRUCTOR_CLOSES_FILE flag (#1075)
## Summary

* Enable `DESTRUCTOR_CLOSES_FILE` flag
* We're never intending to not close files, so if we accidentally leave
them open as they're destructured, this will help close them.

## Additional Context

* As spotted in
https://github.com/crosspoint-reader/crosspoint-reader/pull/869, there
are cases where we were accidentally not closing files

Looks to use about 5K of flash.

```
RAM:   [===       ]  31.5% (used 103100 bytes from 327680 bytes)
Flash: [=======   ]  68.9% (used 4513220 bytes from 6553600 bytes)
```

```
RAM:   [===       ]  31.5% (used 103100 bytes from 327680 bytes)
Flash: [=======   ]  68.9% (used 4518498 bytes from 6553600 bytes)
```


---

### 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-03-03 08:41:02 -06:00
Mirus 6de8b7a666 chore: new Ukrainian localization strings (#1270)
## Summary

* **What changes are included?**
New Ukrainian localization strings

## Additional Context

auto turn functionality
https://github.com/crosspoint-reader/crosspoint-reader/pull/1219

---

### 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-03-03 00:01:47 -06:00
Xuan-Son Nguyen 307a6608f0 chore: remove rendundant xTaskCreate (#1264)
## Summary

Ref discussion:
https://github.com/crosspoint-reader/crosspoint-reader/pull/1222#discussion_r2865402110

Important note that this is a bug-for-bug fix. In reality, this branch
`WiFi.status() == WL_CONNECTED` is pretty much a dead code because the
entry point of these 2 activities don't use wifi.

It is better to refactor the management of network though, but it's
better to be a dedicated PR.

---

### 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-03-02 13:30:55 +01:00
Dani Povedaandcoderabbitai[bot] a350492571 fix: improve and add Spanish translations (#1254)
## Summary

* **What is the goal of this PR?** 
    - Improve and add the latest missing Spanish translations

* **What changes are included?**
- Add missing spaces and remove extra unneeded ones (spaces at the end
of certain strings and others, i.e. the one introduced in the string
`Smart Device`; actually, `SmartDevice` is the correct Calibre plugin
name)
    - Normalise the use of caps in certain strings
- Adapting the translation to the one found in related third-party
software (i.e. Spanish translation for the word `plugin` in Calibre is
`complemento`)
- Shortening some translations to make them smaller and fit better in
screen
- Rewording ambiguous translations (i.e. `Volver a inicio` could mean to
go back to Home, but also to go back to the first page of the current
book, so I changed it for a more specific action, `Volver al menú
Inicio`)

## Additional Context

* **Missing spaces caused a lack of clarity** 

    - My main motivation for this PR was the following:
        <details>

        <summary>Screenshots:</summary>
        
        In English:

![English.bmp](https://github.com/user-attachments/files/25650211/English.bmp)
        
        In Spanish:

![Spanish.bmp](https://github.com/user-attachments/files/25650225/Spanish.bmp)

        </details>

---

### 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-03-02 13:29:01 +01:00
jpirnay ef02737c89 feat: Prefer ".sleep" over "sleep" for custom image directory (#948)
## Summary

* Custom sleep screen images now load from /.sleep directory
(preferred), falling back to /sleep for backwards compatibility. The
dot-prefix keeps the directory hidden from the file browser.
* Rewrote User Guide section 3.6 to document all six sleep screen modes,
cover settings, and the updated custom image setup.

## Additional Context

* The sleep directoy entry while browsing files was distracting.

---

### 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-03-02 13:28:14 +01:00
jpirnay aff93f1dc0 fix: Hanging indent (negative text-indent) and em-unit sizing (#1229)
## Summary

* **What is the goal of this PR?** Fixing two independent CSS rendering
bugs combined to make hanging-indent list styles
(e.g. margin-left:3em; text-indent:-1em) render incorrectly:

* **What changes are included?**
  1. Negative text-indent was silently ignored

Three guards in ParsedText.cpp (computeLineBreaks,
computeHyphenatedLineBreaks,
extractLine) conditioned firstLineIndent on blockStyle.textIndent > 0,
so any
negative value collapsed to zero. Additionally, wordXpos was uint16_t,
which
cannot represent negative offsets — a cast of e.g. −18 would wrap to
65518 and
      render the word far off-screen.

   2. extraParagraphSpacing suppressed hanging indents

Even after removing the > 0 guard, the existing !extraParagraphSpacing
condition
would still suppress all text-indent when that setting is on (its
default). Positive
text-indent is a decorative paragraph indent that the user can
reasonably replace with
vertical spacing — negative text-indent is structural (it positions the
list marker)
      and must always apply.

   3. em unit was calibrated against line height, not font size

emSize was computed as getLineHeight() * lineCompression (the full line
advance).
CSS em units are defined relative to the font-size, which corresponds to
the
ascender height — not the line height. Using line height makes every
em-based
margin/indent ~20–30% wider than a browser would render it, and is
especially
noticeable for CSS that uses font-size: small (which we do not
implement).

## Additional Context

Test case
```
.lsl1 { margin-left: 3em; text-indent: -1em; }

<div class="lsl1">• First list item that wraps across lines</div>
<div class="lsl1">• Short item</div>
```
Before: all lines of all items started at 3 em from the left edge
(indent ignored).

After: the bullet marker hangs at 2 em; continuation lines align at 3
em.

<img width="240" alt="before"
src="https://github.com/user-attachments/assets/9dcbf3e0-fcd9-4af8-b451-a90ba4d2fb75"
/>
<img width="240" alt="after"
src="https://github.com/user-attachments/assets/1ffdcf56-a180-4267-9590-c60d7ac44707"
/>

---

### 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-03-02 12:02:09 +01:00
Arthur Tazhitdinov f0a549b680 refactor: rename MyLibrary to FileBrowser (#1260)
## Summary

* Renames MyLibrary component to FileBrowser, as it better reflects what
it is, in my opinion

## Additional Context

* Frees the Library name for possible future library component that can
cache metadata, provide other ways of browsing than filesystem
structure, etc
---

### AI Usage

Did you use AI tools to help write this code? _**< YES >**_
2026-03-02 11:00:53 +01:00
Zach Nelson 7dc518624c fix: Use fixed-point fractional x-advance and kerning for better text layout (#1168)
## Summary

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

Hopefully fixes #1182.

_Note: I think letterforms got a "heavier" appearance after #1098, which
makes this more noticeable. The current version of this PR reverts the
change to add `--force-autohint` for Bookerly, which to me seems to
bring the font back to a more aesthetic and consistent weight._

#### Problem

Character spacing was uneven in certain words. The word "drew" in
Bookerly was the clearest example: a visible gap between `d` and `r`,
while `e` and `w` appeared tightly condensed. The root cause was
twofold:

1. **Integer-only glyph advances.** `advanceX` was stored as a `uint8_t`
of whole pixels, sourced from FreeType's hinted `advance.x` (which
grid-fits to integers). A glyph whose true advance is 15.56px was stored
as 16px -- an error of +0.44px per character that compounds across a
line.

2. **Floor-rounded kerning.** Kern adjustments were converted with
`math.floor()`, which systematically over-tightened negative kerns. A
kern of -0.3px became -1px -- a 0.7px over-correction that visibly
closed gaps.

Combined, these produced the classic symptom: some pairs too wide,
others too tight, with the imbalance varying per word.

#### Solution: fixed-point accumulation with 1/16-pixel resolution, for
sub-pixel precision during text layout

All font metrics now use a "fixed-point 4" format -- 4 fractional bits
giving 1/16-pixel (0.0625px) resolution. This is implemented with plain
integer arithmetic (shifts and adds), requiring no floating-point on the
ESP32.

**How it works:**

A value like 15.56px is stored as the integer `249`:

```
249 = 15 * 16 + 9    (where 9/16 = 0.5625, closest to 0.56)
```

Two storage widths share the same 4 fractional bits:

| Field | Type | Format | Range | Use |
|-------|------|--------|-------|-----|
| `advanceX` | `uint16_t` | 12.4 | 0 -- 4095.9375 px | Glyph advance
width |
| `kernMatrix` | `int8_t` | 4.4 | -8.0 -- +7.9375 px | Kerning
adjustment |

Because both have 4 fractional bits, they add directly into a single
`int32_t` accumulator during layout. The accumulator is only snapped to
the nearest whole pixel at the moment each glyph is rendered:

```cpp
int32_t xFP = fp4::fromPixel(startX);     // pixel to 12.4: startX << 4

for each character:
    xFP += kernFP;                          // add 4.4 kern (sign-extends into int32_t)
    int xPx = fp4::toPixel(xFP);           // snap to nearest pixel: (xFP + 8) >> 4
    render glyph at xPx;
    xFP += glyph->advanceX;                // add 12.4 advance
```

Fractional remainders carry forward indefinitely. Rounding errors stay
below +/- 0.5px and never compound.

#### Concrete example: "drew" in Bookerly

**Before** (integer advances, floor-rounded kerning):

| Char | Advance | Kern | Cursor | Snap | Gap from prev |
|------|---------|------|--------|------|---------------|
| d | 16 px | -- | 33 | 33 | -- |
| r | 12 px | 0 | 49 | 49 | ~2px |
| e | 13 px | -1 | 60 | 60 | ~0px |
| w | 22 px | -1 | 72 | 72 | ~0px |

The d-to-r gap was visibly wider than the tightly packed `rew`.

**After** (12.4 advances, 4.4 kerning, fractional accumulation):

| Char | Advance (FP) | Kern (FP) | Accumulator | Snap | Ink start | Gap
from prev |

|------|-------------|-----------|-------------|------|-----------|---------------|
| d | 249 (15.56px) | -- | 528 | 33 | 34 | -- |
| r | 184 (11.50px) | 0 | 777 | 49 | 49 | 0px |
| e | 208 (13.00px) | -8 (-0.50px) | 953 | 60 | 61 | 1px |
| w | 356 (22.25px) | -4 (-0.25px) | 1157 | 72 | 72 | 0px |

Spacing is now `0, 1, 0` pixels -- nearly uniform. Verified on-device:
all 5 copies of "drew" in the test EPUB produce identical spacing,
confirming zero accumulator drift.

#### Changes

**Font conversion (`fontconvert.py`)**
- Use `linearHoriAdvance` (FreeType 16.16, unhinted) instead of
`advance.x` (26.6, grid-fitted to integers) for glyph advances
- Encode kern values as 4.4 fixed-point with `round()` instead of
`floor()`
- Add `fp4_from_ft16_16()` and `fp4_from_design_units()` helper
functions
- Add module-level documentation of fixed-point conventions

**Font data structures (`EpdFontData.h`)**
- `EpdGlyph::advanceX`: `uint8_t` to `uint16_t` (no memory cost due to
existing struct padding)
- Add `fp4` namespace with `constexpr` helpers: `fromPixel()`,
`toPixel()`, `toFloat()`
- Document fixed-point conventions

**Font API (`EpdFont.h/cpp`, `EpdFontFamily.h/cpp`)**
- `getKerning()` return type: `int8_t` to `int` (to avoid truncation of
the 4.4 value)

**Rendering (`GfxRenderer.cpp`)**
- `drawText()`: replace integer cursor with `int32_t` fixed-point
accumulator
- `drawTextRotated90CW()`: same accumulator treatment for vertical
layout
- `getTextAdvanceX()`, `getSpaceWidth()`, `getSpaceKernAdjust()`,
`getKerning()`: convert from fixed-point to pixel at API boundary

**Regenerated all built-in font headers** with new 12.4 advances and 4.4
kern values.

#### Memory impact

Zero additional RAM. The `advanceX` field grew from `uint8_t` to
`uint16_t`, but the `EpdGlyph` struct already had 1 byte of padding at
that position, so the struct size is unchanged. The fixed-point
accumulator is a single `int32_t` on the stack.

#### Test plan

- [ ] Verify "drew" spacing in Bookerly at small, medium, and large
sizes
- [ ] Verify uppercase kerning pairs: AVERY, WAVE, VALUE
- [ ] Verify ligature words: coffee, waffle, office
- [ ] Verify all built-in fonts render correctly at each size
- [ ] Verify rotated text (progress bar percentage) renders correctly
- [ ] Verify combining marks (accented characters) still position
correctly
- [ ] Spot-check a full-length book for any layout regressions

---

### 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.6
helped figure out a non-floating point approach for sub-pixel error
accumulation**_
2026-03-01 10:43:37 -06:00
Zach Nelson 620835a6a1 chore: add firmware size history script (#1235)
## Summary

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

- Adds `scripts/firmware_size_history.py`, a developer tool that builds
firmware at selected git commits and reports flash usage with deltas
between them.
- Supports two input modes: `--range START END` to walk every commit in
a range, or `--commits REF [REF ...]` to compare specific refs (which
can span branches).
- Defaults to a human-readable aligned table; pass `--csv` for
machine-readable output to stdout or `--csv FILE` to write to a 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, fully written by
AI**_
2026-03-01 10:28:32 -06:00
Zach Nelson 3cc8e272ca refactor: Use std binary search algorithms for font lookups (#1202)
## Summary

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

Rewrite of font routines to use std binary search algorithms instead of
custom repeated implementations: `lookupKernClass`,
`EpdFont::getLigature`, and `EpdFont::getGlyph`.

---

### 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-03-01 10:28:15 -06:00
Zach Nelson 80d1856330 perf: Removed unused ConfirmationActivity member (#1234)
## Summary

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

Small follow up to #909, removing an unused member variable and some
temporary debug logging.

---

### 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-03-01 10:04:52 -06:00
Lev Roland-Kalb 76681201bf fix: Hide unusable button hints when viewing empty directory (#1253)
## Summary

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

Increase accuracy of button hints and text description in the file
browser when viewing empty directory.
 
* **What changes are included?**

Adjusted button label hint rendering logic in file browser to hide the
"Open", "Up", and "Down" hints when the they are not available due to an
empty directory.

I also changed the NO_BOOKS_FOUND string to NO_FILES_FOUND and updated
translations. File browser shows more than just books so seeing "No
Books Found" really doesn't make sense.

## Additional Context

Very Simple change, here is what that looks like on my device.

<img width="1318" height="879" alt="Untitled (7)"
src="https://github.com/user-attachments/assets/6416c8c8-795d-41a5-9b9f-28d2c26666a0"
/>

---

### 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-03-01 13:10:25 +11:00
jpirnay 04242fa221 refactor: Simplify new setting introduction (#1086)
## Summary

* **What is the goal of this PR?** Eliminate the 3-file / 4-location
overhead for adding a new setting. Previously, every new setting
required manually editing JsonSettingsIO.cpp in two places (save +
load), duplicating knowledge already present in SettingsList.h. After
this PR, JsonSettingsIO.cpp never needs to be touched again for standard
settings.
* **What changes are included?**
* `SettingInfo` (in `SettingsActivity.h`) gains one new field: `bool
obfuscated` (base64 save/load for passwords), with a fluent builder
method `.withObfuscated()`. The previously proposed
`defaultValue`/`withDefault()` approach was dropped in favour of reading
the struct field's own initializer value as the fallback (see below).
* `SettingsList.h` entries are annotated with `.withObfuscated()` on the
OPDS password entry. The list is now returned as a `static const`
singleton (`const std::vector<SettingInfo>&`), so it is constructed
exactly once. A missing `key`/`category` on the
`statusBarProgressBarThickness` entry was also fixed — it was previously
skipped by the generic save loop, so changes were silently lost on
restart.
* `JsonSettingsIO::saveSettings` and `loadSettings` replace their ~90
lines of manual per-field code with a single generic loop over
`getSettingsList()`. The loop uses `info.key`,
`info.valuePtr`/`info.stringOffset`+`info.stringMaxLen` (for char-array
string fields), `info.enumValues.size()` (for enum clamping), and
`info.obfuscated`.
* **Default values**: instead of a duplicated `defaultValue` field in
`SettingInfo`, `loadSettings` reads `s.*(info.valuePtr)` *before*
overwriting it. Because `CrossPointSettings` is default-constructed
before `loadSettings` is called, this captures each field's struct
initializer value as the JSON-absent fallback. The single source of
truth for defaults is `CrossPointSettings.h`.
* One post-loop special case remains explicitly: the four `frontButton*`
remap fields (managed by the RemapFrontButtons sub-activity, not in
SettingsList) and `validateFrontButtonMapping()`.
* One pre-loop migration guard handles legacy settings files that
predate the status bar refactor: if `statusBarChapterPageCount` is
absent from the JSON, `applyLegacyStatusBarSettings()` is called first
so the generic loop picks up the migrated values as defaults and applies
its normal clamping.
* OPDS password backward-compat migration (plain `opdsPassword` →
obfuscated `opdsPassword_obf`) is preserved inside the generic
obfuscated-string path.

## Additional Context
Say we want to add a new `bookmarkStyle` enum setting with options
`DOT`, `LINE`, `NONE` and a default of `DOT`:

1. `src/CrossPointSettings.h` — add enum and member:
```cpp
enum BOOKMARK_STYLE { BOOKMARK_DOT = 0, BOOKMARK_LINE = 1, BOOKMARK_NONE = 2 };
uint8_t bookmarkStyle = BOOKMARK_DOT;
```

2. `lib/I18n/translations/english.yaml` — add display strings:
```yaml
STR_BOOKMARK_STYLE: "Bookmark Style"
STR_BOOKMARK_DOT: "Dot"
STR_BOOKMARK_LINE: "Line"
```
(Other language files will fall back to English if not translated. Run
`gen_i18n.py` to regenerate `I18nKeys.h`.)

3. `src/SettingsList.h` — add one entry in the appropriate category:
```cpp
SettingInfo::Enum(StrId::STR_BOOKMARK_STYLE, &CrossPointSettings::bookmarkStyle,
                  {StrId::STR_BOOKMARK_DOT, StrId::STR_BOOKMARK_LINE, StrId::STR_NONE_OPT},
                  "bookmarkStyle", StrId::STR_CAT_READER),
```

That's it — no default annotation needed anywhere, because
`bookmarkStyle = BOOKMARK_DOT` in the struct already provides the
fallback. The setting will automatically persist to JSON on save, load
with clamping on boot, appear in the device settings UI under the Reader
category, and be exposed via the web API — all with no further changes.

---

### 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-03-01 12:54:58 +11:00
martin brookandDave Allie 2b25f4d168 feat: replace picojpeg with JPEGDEC for JPEG image decoding (#1136)
## Summary

Replaces the picojpeg library with bitbank2/JPEGDEC for JPEG decoding in
the EPUB image pipeline. JPEGDEC provides built-in coarse scaling (1/2,
1/4, 1/8), 8-bit grayscale output, and streaming block-based decoding
via callbacks.

Includes a pre-build patch script for two JPEGDEC changes affecting
progressive JPEG support and EIGHT_BIT_GRAYSCALE mode.

Closes #912 

## Additional Context
# Example progressive jpeg 

<img
src="https://github.com/user-attachments/assets/e63bb4f8-f862-4aa0-a01f-d1ef43a4b27a"
width="400" height="800" />

Good performance increase from JPEGDEC over picojpeg cc @bitbank2 thanks

## Baseline JPEG Decode Performance: picojpeg vs JPEGDEC (float in
callback) vs JPEGDEC (fixed-point in callback)

Tested with `test_jpeg_images.epub` on device (ESP32-C3), first decode
(no cache).

| Image | Source | Output | picojpeg | JPEGDEC float | JPEGDEC
fixed-point | vs picojpeg | vs float |

|-------|--------|--------|----------|---------------|---------------------|-------------|----------|
| jpeg_format.jpg | 350x250 | 350x250 | 313 ms | 256 ms | **104 ms** |
**3.0x** | **2.5x** |
| grayscale_test.jpg | 400x600 | 400x600 | 768 ms | 661 ms | **246 ms**
| **3.1x** | **2.7x** |
| gradient_test.jpg | 400x500 | 400x500 | 707 ms | 597 ms | **247 ms** |
**2.9x** | **2.4x** |
| centering_test.jpg | 350x400 | 350x400 | 502 ms | 412 ms | **169 ms**
| **3.0x** | **2.4x** |
| scaling_test.jpg | 1200x1500 | 464x580 | 5487 ms | 1114 ms | **668
ms** | **8.2x** | **1.7x** |
| wide_scaling_test.jpg | 1807x736 | 464x188 | 4237 ms | 642 ms | **497
ms** | **8.5x** | **1.3x** |
| cache_test_1.jpg | 400x300 | 400x300 | 422 ms | 348 ms | **141 ms** |
**3.0x** | **2.5x** |
| cache_test_2.jpg | 400x300 | 400x300 | 424 ms | 349 ms | **142 ms** |
**3.0x** | **2.5x** |

  ### Summary

- **1:1 scale (fixed-point vs float)**: ~2.5x faster — eliminating
software float on the FPU-less ESP32-C3 is the dominant win
- **1:1 scale (fixed-point vs picojpeg)**: ~3.0x faster overall
- **Downscaled images (vs picojpeg)**: 8-9x faster — JPEGDEC's coarse
scaling + fixed-point draw callback
- **Downscaled images (fixed-point vs float)**: 1.3-1.7x — less dramatic
since JPEG library decode dominates over the draw callback for fewer
output pixels
- The fixed-point optimization alone (vs float JPEGDEC) saved **~60% of
render time** on 1:1 images, confirming that software float emulation
was the primary bottleneck in the draw callback
- See thread for discussions on quality of progressive images,
https://github.com/crosspoint-reader/crosspoint-reader/pull/1136#issuecomment-3952952315
- and the conclusion
https://github.com/crosspoint-reader/crosspoint-reader/pull/1136#issuecomment-3959379386
- Proposal to improve quality added at
https://github.com/crosspoint-reader/crosspoint-reader/discussions/1179
---

### 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: Dave Allie <dave@daveallie.com>
2026-03-01 12:24:58 +11:00
Xuan-Son Nguyenandcoderabbitai[bot] a57c62f0b4 fix: properly implement requestUpdateAndWait() (#1218)
## Summary

Properly implement `requestUpdateAndWait()` using freeRTOS direct task
notification.

FWIW, I think most of the current use cases of `requestUpdateAndWait()`
are redundant, better to be replaced by `requestUpdate(true)`. But just
keeping them in case we can find a proper use case for it in the future.

---

### 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**, it's trivial, so
I asked an AI to write the code

---------

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2026-03-01 12:12:57 +11:00
jpirnay 3da2cd3cf8 feat: Add git branch to version information on settings screen (#1225)
## Summary

* **What is the goal of this PR?** During my development I am frequently
jumping from branch to branch flashing test versions on my device. It
becomes sometimes quite difficult to figure out which version of the
software I am currently looking at.

* **What changes are included?**
- Dev builds now display the current git branch in the version string
shown on the Settings screen (e.g. 1.1.0-dev+feat-my-feature), making it
easier to identify which firmware is running on the device when
switching between branches frequently.
- Release, RC, and slim builds are unaffected — they continue to set
their version string statically in platformio.ini.
<img width="480" height="800" alt="after"
src="https://github.com/user-attachments/assets/d2ab3d69-ab6b-47a1-8eb7-1b40b1d3b106"
/>


## Additional Context

A new PlatformIO pre-build script (scripts/git_branch.py) runs
automatically before every dev build. It reads the base version from the
[crosspoint] section of platformio.ini, queries git rev-parse
--abbrev-ref HEAD for the current branch, and injects the combined
string as the CROSSPOINT_VERSION preprocessor define. In a detached HEAD
state it falls back to the short commit SHA. If git is unavailable it
warns and falls back to unknown.

The script can also be run directly with python scripts/git_branch.py
for validation without triggering a full build.

---

### 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-03-01 12:07:08 +11:00
Dave Allie 88c49b8bed Merge branch 'release/1.1.1' 2026-03-01 12:04:38 +11:00
th0m4sek f67e6c2831 feat: Add Polish strings for commits #1219,#1169,#1031 +tweaks (#1227)
## Summary

* **What is the goal of this PR?** Add missing strings and tweaks for
polish language
* **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-02-28 11:07:52 -06:00
5e95d9a36f feat: Long Click for File Deletion through File Browser (#909)
## Summary

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

Allow users to better manage their epub library by offloading unwanted
or finished books and other files. Resolves #893

* **What changes are included?**

Added Delete Book shortcut in the fil browser. Delete function
implements the new ConfirmationActivity to show file name and solicit
user interaction before either returning to the file browser on a press
of the back button, or proceeding to delete. Delete function then
deletes the file and returns user to the file browser menu at the
current directory. Video of it working on my machine attached here:


https://github.com/user-attachments/assets/329b0198-9e97-45ad-82aa-c39894351667


## Additional Context

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

Certainly potential risks associated with file deletion. Please let me
know if there are any concerns that need to be better addressed. I think
this is a very good feature to have to go along with the new screenshots
so you don't get stuck with a bunch of extra files on your device. Also
I did add this to the user guide.

---

### 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: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Co-authored-by: Егор Мартынов <martynovegorOF@yandex.ru>
Co-authored-by: Arthur Tazhitdinov <lisnake@gmail.com>
Co-authored-by: Zach Nelson <zach@zdnelson.com>
2026-02-28 10:58:10 -06:00
Arthur Tazhitdinov 45a228a645 fix: use HTTPClient::writeToStream for downloading files from OPDS (#1207)
## Summary

* Refactored `HttpDownloader::downloadToFile` to use `FileWriteStream`
and `HTTPClient::writeToStream`, removing manual chunked downloading
logic, which was error-prone.
* Fixes
https://github.com/crosspoint-reader/crosspoint-reader/issues/632

## Additional Context

* Tested downloading files from OPDS with a size up to 10 mb.

---

### 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-02-28 13:39:09 +03:00
Xuan-Son NguyenandZach Nelson 6ff5fcd9a7 fix: make file system operations thread-safe (HalFile) (#1212)
## Summary

Fix https://github.com/crosspoint-reader/crosspoint-reader/issues/1137

Introducing `HalFile`, a thin wrapper around `FsFile` that uses a global
mutex to protect file operations.

To test this PR, place the code below somewhere in the code base (I
placed it in `onGoToRecentBooks`)

```cpp
static auto testTask = [](void* param) {
  for (int i = 0; i < 10; i++) {
    String json = Storage.readFile("/.crosspoint/settings.json");
    LOG_DBG("TEST_TASK", "Read settings.json, bytes read: %u", json.length());
  }
  vTaskDelete(nullptr);
};
xTaskCreate(testTask, "test0", 8192, nullptr, 1, nullptr);
xTaskCreate(testTask, "test1", 8192, nullptr, 1, nullptr);
xTaskCreate(testTask, "test2", 8192, nullptr, 1, nullptr);
xTaskCreate(testTask, "test3", 8192, nullptr, 1, nullptr);
delay(1000);
```

It will reliably lead to crash on `master`, but will function correctly
with this PR.

A macro renaming trick is used to avoid changing too many downstream
code 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**, only to
help with tedious copy-paste tasks

---------

Co-authored-by: Zach Nelson <zach@zdnelson.com>
2026-02-28 11:15:27 +01:00
Zach Nelson 42b122b8fd docs: ActivityManager migration guide (#1222)
## Summary

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

Added overview and migration guide for ActivityManager changes in #1016.
Thanks for the suggestion, @drbourbon!

---

### 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, fully written by
Claude 4.6**_
2026-02-28 11:10:14 +01:00
Bas van der PloegandBas van der Ploeg 0e168aa22c fix: Dutch translation prefix correction (#1223)
## Summary

* **What is the goal of this PR?**
Fixed a small prefix translation (`STR_TO_PREFIX`)
* **What changes are included?**
Changed the translation from `naar ` to `met `

## Additional Context

* The English translation file doesn't make clear what the context of
`STR_TO_PREFIX` is, so the Dutch translation wasn't correct. This PR
fixes that.

---

### 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: Bas van der Ploeg <bas@MBP-M2-Max-3.localdomain>
2026-02-27 16:18:09 -06:00
Zach Nelson 050a3bd1b6 fix: ActivityManager tweaks (#1220)
## Summary

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

Small tweaks to #1016:
- Only Activity and ActivityManager can access activityResultHandler and
activityResult
- `[[maybe_unused]]` in RenderLock constructor
- Only ActivityManager and RenderLock can access renderingMutex
- Missing renderUpdate after failed wifi selection
- Standardize on activities calling finish instead of
activityManager.popActivity
- Hold RenderLock while mutating state in EpubReaderActivity result
handlers

---

### 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-02-27 22:48:24 +01:00
GenesiaW 3b4f2a1129 feat: Auto Page Turn for Epub Reader (#1219)
## Summary

* **What is the goal of this PR?** (e.g., Implements the new feature for
file uploading.)
- Implements auto page turn feature for epub reader in the reader
submenu

* **What changes are included?**
  - added auto page turn feature in epub reader in the submenu
  - currently there are 5 settings, `OFF, 1, 3, 6, 12` pages per minute

## Additional Context
* Add any other information that might be helpful for the reviewer
(e.g., performance implications, potential risks,
  specific areas to focus on).
  - Replacement PR for #723 
- when auto turn is enabled, space reserved for chapter title will be
used to indicate auto page turn being active
  - Back and Confirm button is used to disable it

---

### 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 (mainly code
reviews)**_
2026-02-27 22:42:41 +03:00
09cef70709 fix: clarity issue with ambiguous string SET (#1169)
## Summary

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

Fixes a clarity issue regarding the translation string `STR_SET`. The
issue lies in the fact that the english word can have different
meanings.

The only time the string is used is in the language selectio screen,
where it has the meaning of _selected_. (As in _The language has been
**set** to French_).

Another meaning can be _configured_. (As in _The KOReader username has
been __set__). This is the meaning many of the translations have taken.
The reason that the string is right above `STR_NOT_SET` (which is meant
as _not configured_).

With this PR I propose to explicitly use the term "_Selected_". There
are two good reasons for this:
+ it removes the confusion and the misleading translations
+ it is consistent with the button label `Select`, communicating the
link between the two (the row will be marked `Selected` if you press the
buttpn `Select`. Much clearer than now)

* **What changes are included?**

Removed the unused strings and added translations for the new string
`STR_SELECTED` for the languages I know.

tagging the translators for feedback:
fr: @Spigaw @CaptainFrito 
de: @DavidOrtmann
cs: @brbla 
pt: @yagofarias
it: @andreaturchet @fargolinux
ru: @madebykir @mrtnvgr 
es: @yeyeto2788 @Skrzakk @pablohc 
sv: @dawiik
ca: @angeldenom 
uj: @mirus-ua 
be: @dexif 

## Additional Context

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

the Issue was introduced in #1020. Previously, if a language was
selected it was marked with `[ON]` (`STR_ON_MARKER`). I considered
reverting it back to that, but the solution I described above seemed
superior.

---

### 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: Егор Мартынов <martynovegorOF@yandex.ru>
Co-authored-by: Mirus <mirusim@gmail.com>
2026-02-27 11:45:05 -06:00
Willy Hardy 4fb785af82 docs: add quick KOReader sync setup guide (#1181)
## Summary

* **What is the goal of this PR?**
Improve KOReader Sync documentation so new users can self-host and
configure sync quickly, with clear references in both README and
USER_GUIDE.

* **What changes are included?**
- Add a KOReader Sync feature mention in `README.md` and link to a
quick-setup section in the guide.
- Add `3.6.5 KOReader Sync Quick Setup` to `USER_GUIDE.md` with:
- Docker Compose-first setup instructions (plus Podman compose
alternative)
  - healthcheck verification
  - one-time user registration example using `curl`
- device-side setup steps (`Settings -> System -> KOReader Sync` and
`Authenticate`)
  - in-reader usage via `Sync Progress` in the reader menu
- Update reading mode navigation wording from "Chapter Menu" to "Reader
Menu" so it reflects current UI behavior.

Closes #1032.

## Additional Context

* This is documentation-only and does not change firmware behavior.
* The quick-setup flow is intentionally short and focused on getting
sync working quickly for typical home-network setups.

---

### 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-02-27 09:59:53 -06:00
Bas van der PloegandBas van der Ploeg 74c7205967 feat: add Dutch translation (#1204)
## Summary

* **Added Dutch translation** 
* **(Added dutch.yaml translation file)**

## 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: Bas van der Ploeg <bas@MBP-M2-Max-3.localdomain>
2026-02-27 09:10:18 -06:00
Victor Domingos 93d4a34c37 chore: Add Portuguese (Portugal) translator to the list (#1211)
## Summary

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

---

### AI Usage



Did you use AI tools to help write this code? _**<  NO >**_
2026-02-27 14:10:22 +03:00
Xuan-Son NguyenandZach Nelson c4fc4effbd refactor: implement ActivityManager (#1016)
## Summary

Ref comment:
https://github.com/crosspoint-reader/crosspoint-reader/pull/1010#pullrequestreview-3828854640

This PR introduces `ActivityManager`, which mirrors the same concept of
Activity in Android, where an activity represents a single screen of the
UI. The manager is responsible for launching activities, and ensuring
that only one activity is active at a time.

Main differences from Android's ActivityManager:
- No concept of Bundle or Intent extras
- No onPause/onResume, since we don't have a concept of background
activities
- onActivityResult is implemented via a callback instead of a separate
method, for simplicity

## Key changes

- Single `renderTask` shared across all activities
- No more sub-activity, we manage them using a stack; Results can be
passed via `startActivityForResult` and `setResult`
- Activity can call `finish()` to destroy themself, but the actual
deletion will be handled by `ActivityManager` to avoid `delete this`
pattern

As a bonus: the manager will automatically call `requestUpdate()` when
returning from another activity

## Example usage

**BEFORE**:

```cpp
// caller
    enterNewActivity(new WifiSelectionActivity(renderer, mappedInput,
                                               [this](const bool connected) { onWifiSelectionComplete(connected); }));

// subactivity
  onComplete(true); // will eventually call exitActivity(), which deletes the caller instance (dangerous behavior)
``` 

**AFTER**: (mirrors the `startActivityForResult` and `setResult` from
android)

```cpp
// caller
  startActivityForResult(new NetworkModeSelectionActivity(renderer, mappedInput),
                         [this](const ActivityResult& result) { onNetworkModeSelected(result.selectedNetworkMode); });

// subactivity
  ActivityResult result;
  result.isCancelled = false;
  result.selectedNetworkMode = mode;
  setResult(result);
  finish(); // signals to ActivityManager to go back to last activity AFTER this function returns
```

TODO:
- [x] Reconsider if the `Intent` is really necessary or it should be
removed (note: it's inspired by
[Intent](https://developer.android.com/guide/components/intents-common)
from Android API) ==> I decided to keep this pattern fr clarity
- [x] Verify if behavior is still correct (i.e. back from sub-activity)
- [x] Refactor the `ActivityWithSubactivity` to just simple `Activity`
--> We are using a stack for keeping track of sub-activity now
- [x] Use single task for rendering --> avoid allocating 8KB stack per
activity
- [x] Implement the idea of [Activity
result](https://developer.android.com/training/basics/intents/result)
--> Allow sub-activity like Wifi to report back the status (connected,
failed, etc)

---

### 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**, some
repetitive migrations are done by Claude, but I'm the one how ultimately
approve it

---------

Co-authored-by: Zach Nelson <zach@zdnelson.com>
2026-02-27 00:32:40 -06:00
pablohc 5b11e45a36 fix: prevent infinite render loop in Calibre Wireless after file transfer (#1070)
## Summary

* **What is the goal of this PR?**
Fix an infinite render loop bug in CalibreConnectActivity that caused
the e-ink display to refresh continuously every ~421ms after a file
transfer completed.

* **What changes are included?**
- Added lastProcessedCompleteAt member variable to track which server
completion timestamp has already been processed
- Modified the completion status update logic to only accept new values
from the server, preventing re-processing of old timestamps
- Added clarifying comments explaining the fix

## Problem Description
After receiving a file via Calibre Wireless, the activity displays
"Received: [filename]" for 6 seconds, then clears the message. However,
the web server's wsLastCompleteAt timestamp persists indefinitely and is
never cleared.

This created a race condition:

After 6 seconds, lastCompleteAt is set to 0 (timeout)
In the next loop iteration, status.lastCompleteAt (still has the old
timestamp) ≠ lastCompleteAt (0)
The code restores lastCompleteAt from the server value
Immediately, the 6-second timeout condition is met again
This creates an infinite cycle causing unnecessary e-ink refreshes

## Solution
The fix introduces lastProcessedCompleteAt to track which server
timestamp value has already been processed:

Only accept a new status.lastCompleteAt if it differs from
lastProcessedCompleteAt
Update lastProcessedCompleteAt when processing a new value
Do NOT reset lastProcessedCompleteAt when the 6-second timeout clears
lastCompleteAt
This prevents re-processing the same old server value after the timeout.

## Testing
Tested on device with multiple file transfer scenarios:

 File received message appears correctly after transfer
 Message clears after 6 seconds as expected
 No infinite render loop after timeout
 Multiple consecutive transfers work correctly
 Exiting and re-entering Calibre Wireless works as expected

## Performance Impact
Before: Infinite refreshes every ~421ms after timeout (high battery
drain, display wear)
After: 2-3 refreshes after timeout, then stops (normal behavior)


## Additional Context
This is a targeted fix that only affects the Calibre Wireless file
transfer screen. The root cause is the architectural difference between
the persistent web server state (wsLastCompleteAt) and the per-activity
display state (lastCompleteAt).

An alternative fix would be to clear wsLastCompleteAt in the web server
after some timeout, but that would affect all consumers of the web
server status. The chosen solution keeps the fix localized to
CalibreConnectActivity.

---

### AI Usage

Did you use AI tools to help write this code? _**< YES >**_
2026-02-26 23:34:11 -06:00
d05cb220bb feat: Polish translation tweaks (#1193)
## Summary

* **What is the goal of this PR?** some tweaks to Polish translation
* **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
>**_

---------

Co-authored-by: Zach Nelson <zach@zdnelson.com>
Co-authored-by: Arthur Tazhitdinov <lisnake@gmail.com>
2026-02-26 18:53:30 -06:00
Victor Domingos 125e091d13 fix: Small typo in i18n.md regarding C++ identifiers (#1210)
## Summary

fixes just a small typo in docs.


---

### AI Usage
Did you use AI tools to help write this code? _**< NO >**_
2026-02-26 23:00:34 +00:00
jpirnay 6b64a0a2d8 feat: aiagent context definition (#922)
## Summary

* As many of us have experienced, the use of AI coding agents (Claude,
Cursor, Copilot) in our daily workflows is no longer a hypothetical—it
is a reality. While these tools can significantly accelerate
development, they also pose a unique risk to a project like CrossPoint
Reader, where our hardware constraints (ESP32-C3 with ~380KB RAM) are
extremely tight.

* AI models often "hallucinate" APIs or suggest high-level C++ patterns
(like std::string or heavy heap usage) that are detrimental to our
memory-constrained environment.

* To address this, I am proposing the introduction of an AI Agent
Guidance File (.skills/SKILL.md recognized by many AI systems). This
file acts as a "Constitutional Document" for AI agents, forcing them to
adhere to our specific engineering rigors before they generate a single
line of code.

## 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? _**PARTIALLY**_ Asked
several Ai systems about their needs, added my own 5 cents of nuisances
I faced
2026-02-26 16:59:09 -06:00
Uri TauberandZach Nelson 1abe307f20 perf: Optimize HTML entities lookup to O(log(n)) (#1194)
## Summary

**What is the goal of this PR?** Replace the linear scan of
`lookupHtmlEntity` with a simple binary search to improve lookup
performance.

**What changes are included?**
`lib/Epub/Epub/Entities/htmlEntities.cpp`: 
 - Sorted the `ENTITY_LOOKUP` array.
 - Added a compile-time assertion to guarantee the array remains sorted.
 - Rewrote `lookupHtmlEntity` to use a binary search.

## Additional Context

Benchmarked on my x64 laptop (probably will be different on RISC-V)
```
=== Benchmark (53 entities x 10000 iterations) ===

Version           Total time   Avg per lookup
----------------------------------------------
linear          236.97 ms total     447.11 ns/lookup
binary search    22.09 ms total      41.68 ns/lookup

=== Summary ===

Binary search is 10.73x faster than linear scan.
```

This is a simplified alternative to #1180, focused on keeping the
implementation clean, and maintainable.

### AI Usage


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

---------

Co-authored-by: Zach Nelson <zach@zdnelson.com>
2026-02-26 12:55:31 -06:00
Mirus f7814cd139 chore: new Ukrainian translation lines (#1199)
## Summary

* **What is the goal of this PR?** 
Update translation after
https://github.com/crosspoint-reader/crosspoint-reader/pull/733

---

### 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-02-26 12:54:13 -06:00
madebyKir 3f98a87709 chore: Update russian.yaml (#1198)
## Summary

Translation added russian.yaml

## 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
>**_
NO
2026-02-26 09:15:34 -06:00
Uri TauberandArthur Tazhitdinov 30d8a8d011 feat: slim footnotes support (#1031)
## Summary
**What is the goal of this PR?** Implement support for footnotes in epub
files.
It is based on #553, but simplified — removed the parts which
complicated the code and burden the CPU/RAM. This version supports basic
footnotes and lets the user jump from location to location inside the
epub.

**What changes are included?**
- `FootnoteEntry` struct — A small POD struct (number[24], href[64])
shared between parser, page storage, and UI.
- Parser: `<a href>` detection (`ChapterHtmlSlimParser`) — During a
single parsing pass, internal epub links are detected and collected as
footnotes. The link text is underlined to hint navigability.
Bracket/whitespace normalization is applied to the display label (e.g.
[1] → 1).
- Footnote-to-page assignment (`ChapterHtmlSlimParser`, `Page`) —
Footnotes are attached to the exact page where their anchor word
appears, tracked via a cumulative word counter during layout, surviving
paragraph splits and the 750-word mid-paragraph safety flush.
- Page serialization (`Page`, `Section`) — Footnotes are
serialized/deserialized per page (max 16 per page). Section cache
version bumped to 14 to force a clean rebuild.
- Href → spine resolution (`Epub`) — `resolveHrefToSpineIndex()` maps an
href (e.g. `chapter2.xhtml#note1`) to its spine index by filename
matching.
- Footnotes menu + activity (`EpubReaderMenuActivity`,
`EpubReaderFootnotesActivity`) — A new "Footnotes" entry in the reader
menu lists all footnote links found on the current page. The user
scrolls and selects to navigate.
- Navigate & restore (`EpubReaderActivity`) — `navigateToHref()` saves
the current spine index and page number, then jumps to the target. The
Back button restores the saved position when the user is done reading
the footnote.

  **Additional Context**

**What was removed vs #553:** virtual spine items
(`addVirtualSpineItem`, `isVirtualSpineItem`), two-pass parsing,
`<aside>` content extraction to temp HTML files, `<p class="note">`
paragraph note extraction, `replaceHtmlEntities` (master already has
`lookupHtmlEntity`), `footnotePages` / `buildFilteredChapterList`,
`noterefCallback` / `Noteref` struct, and the stack size increase from 8
KB to 24 KB (not needed without two-pass parsing and virtual file I/O on
the render task).
 
**Performance:** Single-pass parsing. No new heap allocations in the hot
path — footnote text is collected into fixed stack buffers (char[24],
char[64]). Active runtime memory is ~2.8 KB worst-case (one page × 16
footnotes × 88 bytes, mirrored in `currentPageFootnotes`). Flash usage
is unchanged at 97.4%; RAM stays at 31%.
   
**Known limitations:** When clicking a footnote, it jumps to the start
of the HTML file instead of the specific anchor. This could be
problematic for books that don't have separate files for each footnote.
(no element-id-to-page mapping yet - will be another PR soon).

---

### AI Usage

Did you use AI tools to help write this code? _**< PARTIALLY>**_
Claude Opus 4.6 was used to do most of the migration, I checked manually
its work, and fixed some stuff, but I haven't review all the changes
yet, so feedback is welcomed.

---------

Co-authored-by: Arthur Tazhitdinov <lisnake@gmail.com>
2026-02-26 08:47:34 -06:00
ariel-lindemann 451774ddf8 fix: broken translations in status bar settings (#1188)
## Summary

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

The strings for `Show` and `Hide` were always showing in English,
regardless of which language was selected.

* **What changes are included?**

Replace the variables in the lambda by direct calls to the `tr` macro

## 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-02-26 11:25:34 +03:00
ariel-lindemann 8c27938979 fix: add missing romanian strings (#1187)
## Summary

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

Romanian translations for newly added strings.

* **What changes are included?**

Only the 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-02-26 11:22:30 +03:00
Nima SalamiandArthur Tazhitdinov 4a0ef05899 feat: add full Danish translation (#1146)
## Summary

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

* **What changes are included?**
Everything from
[`i18n.md`](https://github.com/crosspoint-reader/crosspoint-reader/blob/master/docs/i18n.md)

---

## Additional Context

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

No

---

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

Started translating myself, transitioned to having Claude Code do the
bulk of the translation. Read every translation myself and doubled
checked with a dictionary if I agreed with the translation made.

---------

Co-authored-by: Arthur Tazhitdinov <lisnake@gmail.com>
2026-02-26 11:21:03 +03:00
PerttuandArthur Tazhitdinov c99a673e5b feat: Add Finnish translations (#1133)
## Summary

* **What is the goal of this PR?** (e.g., Implements the new feature for
file uploading.)
Adds Finnish language support
* **What changes are included?**
Created new translation yaml file, ran the translation script to
generate the C++ code

---

### 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: Arthur Tazhitdinov <lisnake@gmail.com>
2026-02-25 17:03:35 +00:00
cae8517235 feat: Add Polish Language (#1155)
## Summary

* **What is the goal of this PR?** Implements Polish language
* **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
>**_

---------

Co-authored-by: Zach Nelson <zach@zdnelson.com>
Co-authored-by: Arthur Tazhitdinov <lisnake@gmail.com>
2026-02-25 17:01:19 +00:00
ariel-lindemann 7e214ea760 feat: sort languages in selection menu (#1071)
## Summary

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

Currently we are displaying the languages in the order they were added
(as in the `Language` enum). However, as new languages are coming in,
this will quickly be confusing to the users.

But we can't just change the ordering of the enum if we want to respect
bakwards compatibility.

So my proposal is to add a mapping of the alphabetical order of the
languages. I've made it so that it's generated by the `gen_i18n.py`
script, which will be used when a new language is added.


* **What changes are included?**

Added the array from the python script and changed
`LanguageSelectActivity` to use the indices from there. Also commited
the generated `I18nKeys.h`

## 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 was wondering if there is a better way to sort it. Currently, it's by
unicode value and Czech and Russian are last, which I don't know it it's
the most intuitive.

The current order is:
`Català, Deutsch, English, Español, Français, Português (Brasil),
Română, Svenska, Čeština, Русский`

---

### 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-02-25 19:44:17 +03:00
jpirnay b695a48af6 fix: WiFi lifecycle and hyphenation heap defragmentation for KOReader sync (#1151)
## Summary

* **What is the goal of this PR?** KOReader sync on a German-language
book would fail with an out-of-memory error when trying to open the
destination chapter after applying remote progress. The root cause was a
chain of two independent bugs that combined to exhaust the contiguous
heap needed by the EPUB inflate pipeline.
* **What changes are included?**
## Fix 1 — Hyphenation heap defragmentation (LiangHyphenation.cpp)
### What was happening
AugmentedWord, the internal struct used during Liang pattern matching,
held three std::vector<> members (bytes, charByteOffsets,
byteToCharIndex) plus a separate scores vector — a total of 4 heap
allocations per word during page layout. For a German-language section
with hundreds of words, thousands of small malloc/free cycles fragmented
the heap. Total free memory was adequate (~108 KB) but the largest
contiguous block shrank well below the 32 KB needed for the INFLATE ring
buffer used during EPUB decompression. The failure was invisible with
hyphenation disabled, where MaxAlloc stayed at ~77 KB; enabling German
hyphenation silently destroyed the contiguity the allocator needed.

### What changed
The three std::vector<> members of AugmentedWord and the scores vector
are replaced with fixed-size C arrays on the render-task stack:
```
uint8_t bytes[160]           // was std::vector<uint8_t>
size_t  charByteOffsets[70]  // was std::vector<size_t>
int32_t byteToCharIndex[160] // was std::vector<int32_t>
uint8_t scores[70]           // was std::vector<uint8_t>  (local in liangBreakIndexes)
```
Sizing is based on the longest known German word (~63 codepoints × 2
UTF-8 bytes + 2 sentinel dots = 128 bytes); MAX_WORD_BYTES=160 and
MAX_WORD_CHARS=70 give comfortable headroom. The same analysis holds for
all seven supported languages (en, fr, de, es, it, ru, uk) — every
accepted letter encodes to at most 2 UTF-8 bytes after case-folding.
Words exceeding the limits are silently skipped (no hyphenation
applied), which is correct behaviour. The struct lives on the 8 KB
render-task stack so no permanent DRAM is consumed.

Verification: after the fix, MaxAlloc reads 77,812 bytes with German
hyphenation enabled — identical to the figure previously achievable only
with hyphenation off.

## Fix 2 — WiFi lifecycle in KOReaderSyncActivity
(KOReaderSyncActivity.cpp)
### What was happening

onEnter() called WiFi.mode(WIFI_STA) unconditionally before delegating
to WifiSelectionActivity. WifiSelectionActivity manages WiFi mode
internally (it calls WiFi.mode(WIFI_STA) again at scan start and at
connection attempt). The pre-emptive call from KOReaderSyncActivity
interfered with the sub-activity's own state machine, causing
intermittent connection failures that were difficult to reproduce.

Additionally, WiFi was only shut down in onExit(). If the user chose
"Apply remote progress" the activity exited without turning WiFi off
first, leaving the radio on and its memory allocated while the EPUB was
being decompressed — unnecessarily consuming the contiguous heap
headroom that inflate needed.

### What changed

* WiFi.mode(WIFI_STA) removed from onEnter(). WifiSelectionActivity owns
WiFi mode; KOReaderSyncActivity should not touch it before the
sub-activity runs.
* A wifiOff() helper (SNTP stop + disconnect + WIFI_OFF with settling
delays) is extracted into the anonymous namespace and called at every
web-session exit point:
  - "Apply remote" path in loop() — before onSyncComplete()
  - performUpload() success path
  - performUpload() failure path
  - onExit() (safety net for all other exit paths)

## 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**_ and two days of
blood, sweat and heavy swearing...
2026-02-25 08:27:18 -06:00
divinitycove a6c5d9aa7c fix: Change "UI Font Size" to "Reader Font Size" (#1171)
## Summary

* **What is the goal of this PR?** (e.g., Implements the new feature for
file uploading.)
Update "UI Font Size" to "Reader Font Size", to match the rest of the
"Reader" settings and clarify that the setting doesn't change the UI
font.

* **What changes are included?**
Changes the `english.yaml` string and USER_GUIDE.md entry.

## 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
>**_
NO
2026-02-25 08:22:59 -06:00
James WhyteandArthur Tazhitdinov 2d49c7b7b4 feat: split status bar setting (#733)
## Summary

This PR aims to reduce the complexity of the status bar by splitting the
setting into 5:
- Chapter Page Count
- Book Progress %
- Progress Bar
- Chapter Title
- Battery Indicator

These are located within the new StausBarSettings activity, which also
shows a preview of the bar the user has created

<img width="513" height="806" alt="image"
src="https://github.com/user-attachments/assets/cdf852fb-15d8-4da2-a74f-fd69294d7b05"
/>


<img width="483" height="797" alt="image"
src="https://github.com/user-attachments/assets/66fc0c0d-ee51-4d31-b70d-e2bc043205d1"
/>


When updating from a previous version, the user's past settings are
honoured.

## Additional Context

The PR aims to remove any duplication of status bar code where possible,
and extracts the status bar rendering into a new component - StatusBar

It also adds a new (optional) padding option to the progress bar to
allow the status bar to be shifted upwards - this is only intended for
use in the settings.

---

### 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 - although did help to decode some C++ errors

---------

Co-authored-by: Arthur Tazhitdinov <lisnake@gmail.com>
2026-02-25 13:06:38 +03:00
Zach Nelson cb72916397 feat: Latin Extended-B European glyphs (#1167)
## Summary

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

Correction to #1157, which (embarrassingly) failed to actually include
the updated font header files. (Maybe we should generate these at build
time?)

Add Latin Extended-B glyphs for Croatian, Romanian, Pinyin, and European
diacritical variants. Fixes #921.

---

### 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, confirmed
codepoint ranges with Claude**_
2026-02-25 12:56:11 +03:00
iandchasseandcoderabbitai[bot] 35988ada55 feat: wrapped text in GfxRender, implemented in themes so far (#1141)
## Summary

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


* **What changes are included?**
Conrgegate the changes of #1074 , #1013 , and extended upon #911 by
@lkristensen
New function implemented in GfxRenderer.cpp
```C++
std::vector<std::string> GfxRenderer::wrappedText(const int fontId, const char* text, const int maxWidth,
                                                  const int maxLines, const EpdFontFamily::Style style) const
```
Applied logic to all uses in Lyra, Lyra Extended, and base theme
(continue reading card as pointed out by @znelson


## Additional Context





![IMG_8604](https://github.com/user-attachments/assets/49da71c9-a44f-4cde-b3bf-6773d71601b6)

![IMG_8605](https://github.com/user-attachments/assets/5eab4293-65c1-47fb-b422-8ab53a6b50a2)

![IMG_8606](https://github.com/user-attachments/assets/e0f98d19-0e3f-4294-83a1-e49264378dca)


---

### 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: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2026-02-25 10:24:35 +01:00
jpirnay f2fbdccd53 fix: Fix coverRendered flag (#1154)
## Summary

* **What is the goal of this PR?** During low memory situations (which I
encounterde a lot during my recent bugfixing activities) a cover was
considered rendered even if the buffer could not be stored.
* **What changes are included?** Proper assignment of flag logic

## 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-02-25 12:15:30 +03:00
ElizandEliz Kilic 128eb614a6 feat: Current page as QR (#1099)
## Summary

* **What is the goal of this PR?** Implements QR text of the current
page
* **What changes are included?**

## Additional Context

I saw this feature request at #982 
It made sense to me so I implemented. But if the team thinks it is not
necessary please let me know and we can close the PR.

| Page | Menu | QR |
|------|-------|----|
|
![IMG_6601.bmp](https://github.com/user-attachments/files/25473201/IMG_6601.bmp)
|
![IMG_6599.bmp](https://github.com/user-attachments/files/25473202/IMG_6599.bmp)
|
![IMG_6600.bmp](https://github.com/user-attachments/files/25473205/IMG_6600.bmp)
|


---

### AI Usage


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

---------

Co-authored-by: Eliz Kilic <elizk@google.com>
2026-02-25 12:12:31 +03:00
Zach Nelson 31396da064 fix: Update activity was missing "Back" button label (#1128)
## Summary

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

When update activity finds no update or fails, the "Back" button label
was missing. Fixes #1089.

---

### 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-02-24 22:21:46 -06:00
Zach Nelson 8ab2f22730 fix: Handle non-ASCII characters in sanitizeFilename (#1132)
## Summary

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

Probable fix for #1118. `sanitizeFilename` was only passing through
ASCII characters from filenames. It now maintains valid UTF-8
codepoints, including non-ASCII multibyte sequences. Truncation happens
at a maximum number of bytes, rather than characters, to prevent
filenames with many multibyte sequences from unexpectedly exceeding
FAT32 limits.

---

### 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 described #1118
to Claude and it suggested sanitizeFilename as the likely cause**_
2026-02-24 17:43:58 -06:00
Zach Nelson c0cd7c13a3 feat: Latin Extended-B European glyphs (#1157)
## Summary

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

Add Latin Extended-B glyphs for Croatian, Romanian, Pinyin, and European
diacritical variants. Fixes #921.

---

### 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, confirmed
codepoint ranges with Claude**_
2026-02-24 16:44:20 -06:00
Spigawandcoderabbitai[bot] 000289e429 fix: update french.yaml file to have a better French translation of the CFW (#1130)
Small edits of the French translation.

## Summary

* **What is the goal of this PR?** 
Small fixes of the French translation : fixes on missing/unclear rows,
usage of technical terms better suited for an e-reader GUI, shorter
sentences.

* **What changes are included?**
See above and in the .yaml files; only translations have changed, no
code edit.

## Additional Context

* Nothing else

---

### 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-02-25 01:20:20 +03:00
Zach Nelson ff577540a3 chore: Added generated lang headers to .gitignore (#1158)
## Summary

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

Following up on #1156: generated language header files should be
ignored.

---

### 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-02-24 21:48:07 +03:00
Zach Nelson 43efd80e14 chore: Removed generated language headers (#1156)
## Summary

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

I18nKeys.h and I18nStrings.h are generated by gen_i18n.py prior to each
build, so we do not need to maintain a checked-in copy of these 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? _**NO**_
2026-02-24 11:42:05 -06:00
danoobanddanoooob 5050992bd6 feat: Vietnamese glyphs support (#1147)
## Summary

* **What is the goal of this PR?**
Add Vietnamese glyphs support for the reader's built-in fonts, enabling
proper rendering of Vietnamese text in EPUB content.

* **What changes are included?**
- Added 3 new Unicode intervals to `fontconvert.py` covering Vietnamese
characters:
- **Latin Extended-B** (Vietnamese subset only): `U+01A0–U+01B0` — Ơ/ơ,
Ư/ư
- **Vietnamese Extended**: `U+1EA0–U+1EF9` — All precomposed Vietnamese
characters with tone marks (Ả, Ấ, Ầ, Ẩ, Ẫ, Ậ, Ắ, …, Ỹ)
- Re-generated all 54 built-in font header files (Bookerly, Noto Sans,
OpenDyslexic, Ubuntu across all sizes and styles) to include the new
Vietnamese glyphs.

## Additional Context

* **Scope**: This PR only covers the **reader** fonts. The outer UI
still uses the Ubuntu font which does not fully support Vietnamese — UI
and i18n will be addressed in a follow-up PR (per discussion in PR
#1124).
* **Memory impact**:

  | Metric | Before | After | Delta |
  |---|---|---|---|
| Flash Data (`.rodata`) | 2,971,028 B | 3,290,748 B | **+319,720 B
(+10.8%)** |
| Total image size | 4,663,235 B | 4,982,955 B | **+319,720 B (+6.9%)**
|
  | Flash usage | 69.1% | 74.0% | **+4.9 pp** |
  | RAM usage | 29.0% | 29.0% | **No change** |

* **Risk**: Low — this is a data-only change (font glyph tables in
`.rodata`). No logic changes, no RAM impact. Flash headroom remains
comfortable at 74%.

---

### AI Usage

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

AI was used to identify the minimal set of Unicode ranges needed for
Vietnamese support and to assist with the PR description.

---------

Co-authored-by: danoooob <danoooob@example.com>
2026-02-24 11:21:39 -06:00
jpirnay 7d97687a5b feat: Add maxAlloc to memory information (#1152)
## Summary

* **What is the goal of this PR?** During debugging of #1092 i
desperately needed to monitor the biggest allocatable block of memory on
the heap
* **What changes are included?** Added informaqtion to debug output,
amended monitor utility to pick it up

## 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-02-24 11:12:01 -06:00
Andrea TurchetandZach Nelson d6d0cc869e feat: Add full Italian translations (#1144)
## Summary

* **What is the goal of this PR?** Implements the Italian language
translation for CrossPoint Reader.
* **What changes are included?**
* Added [lib/I18n/translations/italian.yaml] with Italian translations
for all strings.
* Generated the necessary C++ files by running the [gen_i18n.py] script.
* Added myself to the [docs/translators.md] file under the Italian
section.

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

---------

Co-authored-by: Zach Nelson <zach@zdnelson.com>
2026-02-24 09:43:28 -06:00
Mirus 233deacfe1 fix: add new Ukrainian translation line for STR_SCREENSHOT_BUTTON (#1149)
## Summary

* **What is the goal of this PR?**
Add missing `STR_SCREENSHOT_BUTTON`

## Additional Context

After the screenshot feature was added, a new translation line was
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 **_
2026-02-24 18:25:15 +03:00
Zach NelsonandCursor 0eb8a9346b feat: Support for kerning and ligatures (#873)
## Summary

**What is the goal of this PR?**
Improved typesetting, including
[kerning](https://en.wikipedia.org/wiki/Kerning) and
[ligatures](https://en.wikipedia.org/wiki/Ligature_(writing)#Latin_alphabet).

**What changes are included?**
- The script to convert built-in fonts now adds kerning and ligature
information to the generated font headers.
- Epub page layout calculates proper kerning spaces and makes ligature
substitutions according to the selected font.


![3U1B1808](https://github.com/user-attachments/assets/1accb16f-2f1a-41e5-adca-89f1f1348494)

![3U1B1810](https://github.com/user-attachments/assets/2f6bd007-490e-420f-b774-3380b4add7ea)

![3U1B1815](https://github.com/user-attachments/assets/1986bb77-2db0-46e2-a5d6-8315dae9eb19)

## Additional Context

- I am not a typography expert. 
- The implementation has been reworked from the earlier version, so it
is no longer necessary to omit Open Dyslexic, and kerning data now
covers all fonts, styles, and codepoints for which we include bitmap
data.
- Claude Opus 4.6 helped with a lot of this.
- There's an included test epub document with lots of kerning and
ligature examples, shown in the photos.

**_After some time to mature, I think this change is in decent shape to
merge and get people testing._**

After opening this PR I came across #660, which overlaps in adding
ligature support.

---

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

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-24 11:31:43 +03:00
Jason2866 13592db50f perf: Update github actions for optimal performance with pioarduino (#1080)
## Summary

* **What is the goal of this PR?**
 speed increase gh workflows, optimized for pioarduino Platform 

* **What changes are included?**
  remove pip and pip cache
  install and use `uv`
use pioarduino core instead of Platformio core for optimal performance
with pioarduino Platform

## Additional Context

- signed off by the maintainer of pioarduino

---

### 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
>**_NO
2026-02-23 23:25:53 +03:00
martin brook f8a9f1f07a fix: image centering bleed (#1096)
## Summary

* Fixes #1026 
* Added a reproducer chapter to the epub generator for this case
* The fix is in endElement — when leaving a block/header element that
had an empty text block, reset the alignment to the user's default so it
doesn't bleed into the next sibling. This preserves accumulated margins
from parent elements while preventing stale alignment from carrying
across.

## Additional Context

### Before fix


![20260222_210029262](https://github.com/user-attachments/assets/263e4608-18cf-418b-871a-1c9a71822bdf)

![20260222_210040995](https://github.com/user-attachments/assets/9f0fdea1-5abf-4f1c-b35d-d35c8309456a)

![20260222_210052640](https://github.com/user-attachments/assets/b77dbadc-f347-400b-994a-17d0f5f073d8)

### After fix


![20260222_211037007](https://github.com/user-attachments/assets/294e15b3-ee40-4c21-8f5b-bd6b40d43d8d)

![20260222_211045139](https://github.com/user-attachments/assets/74107cf9-08a2-4737-be7f-ed0b5648ca6f)

---

### 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-02-23 23:17:37 +03:00
Adrian Wilkins-Caruana 052f497b9e fix: force auto-hinting for Bookerly to fix inconsistent stem widths (#1098)
## Summary

Bookerly's native TrueType hinting is effectively a no-op at the sizes
used here, causing FreeType to place stems at inconsistent sub-pixel
positions. This results in the 'k' stem (8-bit fringe: 0x38=56) falling
just below the 2-bit quantization threshold while 'l' and 'h' stems
(fringes: 0x4C=76, 0x40=64) land above it --- making 'k' visibly
narrower (2.00px vs 2.33px effective width).

FreeType's auto-hinter snaps all stems to consistent grid positions,
normalizing effective stem width to 2.67px across all glyphs.

Adds --force-autohint flag to fontconvert.py and applies it to Bookerly
only. NotoSans, OpenDyslexic, and Ubuntu fonts are unaffected.

Here is an example of before/after. Take notice of the vertical stems on
characters like `l`, `k`, `n`, `i`, etc. The font is Bookerly 12pt
regular:

**BEFORE**:

![before](https://github.com/user-attachments/assets/65b2acab-ad95-489e-885e-e3a0163cc252)

**AFTER**:


![after](https://github.com/user-attachments/assets/d09a8b5d-40af-4a7d-b622-e1b2cabcce85)

Claude generated this script to quantitatively determine that this
change makes the vertical stems on a variety of characters more
consistent for Bookerly _only_.

<details>
  <summary>Python script</summary>
    
  ```python
#!/usr/bin/env python3
"""Compare stem consistency across all font families with and without
auto-hinting.

Run from repo root:
    python3 compare_all_fonts.py
"""

import freetype

DPI = 150
CHARS = ["k", "l", "h", "i", "b", "d"]
SIZES = [12, 14, 16, 18]

FONTS = {
"Bookerly":
"lib/EpdFont/builtinFonts/source/Bookerly/Bookerly-Regular.ttf",
"NotoSans":
"lib/EpdFont/builtinFonts/source/NotoSans/NotoSans-Regular.ttf",
"OpenDyslexic":
"lib/EpdFont/builtinFonts/source/OpenDyslexic/OpenDyslexic-Regular.otf",
"Ubuntu": "lib/EpdFont/builtinFonts/source/Ubuntu/Ubuntu-Regular.ttf",
}

MODES = {
    "default": freetype.FT_LOAD_RENDER,
"autohint": freetype.FT_LOAD_RENDER | freetype.FT_LOAD_FORCE_AUTOHINT,
}


def q4to2(v):
    if v >= 12:
        return 3
    elif v >= 8:
        return 2
    elif v >= 4:
        return 1
    else:
        return 0


def get_stem_eff(face, char, flags):
    gi = face.get_char_index(ord(char))
    if gi == 0:
        return None
    face.load_glyph(gi, flags)
    bm = face.glyph.bitmap
    w, h = bm.width, bm.rows
    if w == 0 or h == 0:
        return None

    p2 = []
    for y in range(h):
        row = []
        for x in range(w):
            row.append(q4to2(bm.buffer[y * bm.pitch + x] >> 4))
        p2.append(row)

    # Measure leftmost stem in stable middle rows
    mid_start, mid_end = h // 4, h - h // 4
    widths = []
    for y in range(mid_start, mid_end):
        first = next((x for x in range(w) if p2[y][x] > 0), -1)
        if first < 0:
            continue
        last = first
        for x in range(first, w):
            if p2[y][x] > 0:
                last = x
            else:
                break
        eff = sum(p2[y][x] for x in range(first, last + 1)) / 3.0
        widths.append(eff)
    return round(sum(widths) / len(widths), 2) if widths else None


def main():
    for font_name, font_path in FONTS.items():
        try:
            freetype.Face(font_path)
        except Exception:
            print(f"\n  {font_name}: SKIPPED (file not found)")
            continue

        print(f"\n{'=' * 80}")
        print(f"  {font_name}")
        print(f"{'=' * 80}")

        for size in SIZES:
            print(f"\n  {size}pt:")
            print(f"  {'':6s}", end="")
            for c in CHARS:
                print(f"  '{c}'  ", end="")
            print("  | spread")

            for mode_name, flags in MODES.items():
                face = freetype.Face(font_path)
                face.set_char_size(size << 6, size << 6, DPI, DPI)
                vals = []
                print(f"  {mode_name:6s}", end="")
                for c in CHARS:
                    v = get_stem_eff(face, c, flags)
                    vals.append(v)
                    print(f"  {v:5.2f}" if v else "    N/A", end="")

                valid = [v for v in vals if v is not None]
spread = max(valid) - min(valid) if len(valid) >= 2 else 0
                marker = " <-- inconsistent" if spread > 0.5 else ""
                print(f"  | {spread:.2f}{marker}")


if __name__ == "__main__":
    main()

  ```
  
</details>

Here are the results. The table compares how the font-generation
`autohint` flag affects the range of widths of various characters. Lower
`spread` mean that glyph stroke widths should appear more consistent.
```
    Spread = max stem width - min stem width across glyphs (lower = more consistent):                                                          
                                                                                                                                               
    ┌──────────────┬──────┬─────────┬──────────┬──────────┐                                                                                    
    │     Font     │ Size │ Default │ Autohint │  Winner  │                                                                                    
    ├──────────────┼──────┼─────────┼──────────┼──────────┤                                                                                    
    │ Bookerly     │ 12pt │ 1.49    │ 1.12     │ autohint │                                                                                    
    ├──────────────┼──────┼─────────┼──────────┼──────────┤                                                                                    
    │              │ 14pt │ 1.39    │ 1.13     │ autohint │                                                                                    
    ├──────────────┼──────┼─────────┼──────────┼──────────┤                                                                                    
    │              │ 16pt │ 1.38    │ 1.16     │ autohint │                                                                                    
    ├──────────────┼──────┼─────────┼──────────┼──────────┤                                                                                    
    │              │ 18pt │ 1.90    │ 1.58     │ autohint │                                                                                    
    ├──────────────┼──────┼─────────┼──────────┼──────────┤                                                                                    
    │ NotoSans     │ 12pt │ 1.16    │ 0.94     │ mixed    │                                                                                    
    ├──────────────┼──────┼─────────┼──────────┼──────────┤                                                                                    
    │              │ 14pt │ 0.83    │ 1.14     │ default  │                                                                                    
    ├──────────────┼──────┼─────────┼──────────┼──────────┤                                                                                    
    │              │ 16pt │ 1.41    │ 1.51     │ default  │                                                                                    
    ├──────────────┼──────┼─────────┼──────────┼──────────┤                                                                                    
    │              │ 18pt │ 1.74    │ 1.63     │ mixed    │                                                                                    
    ├──────────────┼──────┼─────────┼──────────┼──────────┤                                                                                    
    │ OpenDyslexic │ 12pt │ 2.22    │ 1.44     │ autohint │                                                                                    
    ├──────────────┼──────┼─────────┼──────────┼──────────┤                                                                                    
    │              │ 14pt │ 2.57    │ 3.29     │ default  │                                                                                    
    ├──────────────┼──────┼─────────┼──────────┼──────────┤                                                                                    
    │              │ 16pt │ 3.13    │ 2.60     │ autohint │                                                                                    
    ├──────────────┼──────┼─────────┼──────────┼──────────┤                                                                                    
    │              │ 18pt │ 3.21    │ 3.23     │ ~tied    │                                                                                    
    ├──────────────┼──────┼─────────┼──────────┼──────────┤                                                                                    
    │ Ubuntu       │ 12pt │ 1.25    │ 1.31     │ default  │                                                                                    
    ├──────────────┼──────┼─────────┼──────────┼──────────┤                                                                                    
    │              │ 14pt │ 1.41    │ 1.64     │ default  │                                                                                    
    ├──────────────┼──────┼─────────┼──────────┼──────────┤                                                                                    
    │              │ 16pt │ 2.21    │ 1.71     │ autohint │                                                                                    
    ├──────────────┼──────┼─────────┼──────────┼──────────┤                                                                                    
    │              │ 18pt │ 1.80    │ 1.71     │ autohint │                                                                                    
    └──────────────┴──────┴─────────┴──────────┴──────────┘                                                                                    
```


---

### 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? I used AI to make sure I'm
not doing something stupid, since I'm not a typography expert. I made
the changes though.
2026-02-23 22:13:08 +03:00
Dexif bfdf0a4f78 feat: set WiFi hostname to CrossPoint-Reader-XXXXXXXXXXXX (#1107)
## Summary

Replace the default esp32-XXXXXXXXXXXX hostname with
CrossPoint-Reader-AABBCCDDEEFF (full MAC address) so the device is
easily identifiable on the router's client list.
2026-02-23 22:00:16 +03:00
Xuan-Son Nguyen ae94e97fb8 fix: sdfat warning about redefinition of macro (#1135)
## Summary

Fix redefinition of `FILE_*` macro.

Note that there will still be 2 warning:

```
.pio/libdeps/default/WebSockets/src/WebSocketsClient.cpp: In member function 'void WebSocketsClient::clientDisconnect(WSclient_t*, const char*)':
.pio/libdeps/default/WebSockets/src/WebSocketsClient.cpp:573:31: warning: 'virtual void NetworkClient::flush()' is deprecated: Use clear() instead. [-Wdeprecated-declarations]
  573 |             client->tcp->flush();
      |             ~~~~~~~~~~~~~~~~~~^~
```

--> I assume the upstream library need to fix it

And:

```
src/activities/Activity.cpp:8:1: warning: 'noreturn' function does return
    8 | }
      | ^
```

Will be fixed in #1016 

---

### 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-02-23 12:52:25 -06:00
pepastach 04e72a9ede fix: Unify inconsistent Wi-Fi/WiFi in Czech translation (#1138)
## Summary

* **What is the goal of this PR?**
Fix inconsistent WiFi strings in Czech translation. 

* **What changes are included?**
Only a few `Wi-Fi` strings changed to `WiFi` to maintain consistency.

---

### 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-02-23 12:51:34 -06:00
ariel-lindemann 52ca658634 fix: added romanian translation to new strings (#1105)
## Summary

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

Added Romanian translations for newly addded strings

* **What changes are included?**

Just the translations in the localisation file.

## 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-02-23 11:31:14 -06:00
divinitycoveandZach Nelson 75bb1d8582 docs: USER_GUIDE.md update for 1.1.0 (#1108)
## Summary

* **What is the goal of this PR?** (e.g., Implements the new feature for
file uploading.)
- This PR updates the USER_GUIDE.md to match recent changes, in
particular 1.1.0, alongside some minor copyediting.
* **What changes are included?**

  - Renamed titles for screens to match current UI.
- Sorted Settings section by order in UI, added subheadings for Settings
pages, and added all current settings.
  - Updated System Navigation behaviour description to match #726.

## Additional Context

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

This is an admittedly quick edit I did to get USER_GUIDE.md up to
scratch alongside the release of 1.1.0. I'm a writer, not a programmer,
so there are some things that will probably need improvement.

- ~Recent Books needs to be added, something I could add to this PR if
needed.~
- ~Remap Front Buttons might need to be updated.~

These have been added in later commits, Remap Front Buttons might still
need more detail.

- The UI Theme, Embedded Style, Hyphenation, WiFi Networks, KOReader
Sync, and Clear Reading Cache settings might need better (or more
technically specific) descriptions.

Two questions I have:
- ~Should the new Settings subheadings be added to the Table of
Contents?~ Added in later commits.
- The Manual of Style/formatting for USER_GUIDE.md, especially in the
Settings section, is somewhat inconsistent. Let me know if any of my
edits don't fit with this.

---

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

---------

Co-authored-by: Zach Nelson <zach@zdnelson.com>
2026-02-23 11:14:11 -06:00
Xuan-Son Nguyen 7761ced0ed fix: acquire power lock before sleeping (#1125)
## Summary

Ref: https://github.com/crosspoint-reader/crosspoint-reader/issues/1110

Power lock is automatically acquired on `render()`. However, instead of
using `render()`, sleep activity render everything right inside
`onEnter()`, so no power lock was acquired.

After https://github.com/crosspoint-reader/crosspoint-reader/pull/1016 ,
the power lock will also be acquired on activity transition.

---

### 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-02-23 20:07:55 +03:00
Dexif 2b52bc658c feat: add Belarusian translation (#1120)
Add full Belarusian localization.
2026-02-23 10:52:49 -06:00
Florian Hülsmann 57250b97e4 fix: Consider extra quotation styles when hyphenating quoted words (#1077)
## Summary

* **What is the goal of this PR?** Address expected hyphenation issue
from
https://github.com/crosspoint-reader/crosspoint-reader/issues/998#issuecomment-3940533510
* Closes #998 
* **What changes are included?** Add `„` (U+201E, _Double Low-9
Quotation Mark_), `‚` (U+201A, _Single Low-9 Quotation Mark_) and `‹`
(U+2039, _Single Left-pointing Angle Quotation Mark_) exceptions, other
quote types were handled correctly.

**Before**
<img width="480" height="155" alt="hyph3"
src="https://github.com/user-attachments/assets/e06b4071-2c8c-4814-965d-96fbe302a450"
/>
**After**
<img width="480" height="154" alt="hyph3-fix"
src="https://github.com/user-attachments/assets/4f7f5406-e200-451c-8bee-3f410cc84bbe"
/>


## 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-02-23 17:25:22 +03:00
Zach Nelson 13fc8b94b0 refactor: Simplify REPLACEMENT_GLYPH fallback (#1119)
## Summary

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

Consolidated repeated logic to fall back to REPLACEMENT_GLYPH.

---

### 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-02-23 13:32:50 +01:00
Zach Nelson 410c70ab89 perf: UITheme::getMetrics const and const-ref usage (#1094)
## Summary

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

Small cleanup to make getTheme and getMetrics methods on UITheme const.
They return const refs, so updated call sites to use `const auto&`.

Realistically this won't make much performance difference, but it better
conveys the nature of theme metrics being shared const state.

---

### 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-02-23 13:29:18 +01:00
CaptainFrito a6aead660a feat: Themed language screen (#1020)
## Summary

Added theme support to the language screen

![IMG_8139
Medium](https://github.com/user-attachments/assets/08fe06b4-d8ed-41fe-9584-8b90488eebae)


## Additional Context

---

### AI Usage

Did you use AI tools to help write this code? _**NO**_
2026-02-23 15:16:46 +03:00
ElizandEliz Kilic 9e4ef008cc feat: Download links for web server (#1039)
## Summary

* **What is the goal of this PR?** Add a link to files to download on
web server

## Additional Context

* There was already an api to download files, i just added a link to it
for files.

---

### AI Usage

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

Addresses #621

---------

Co-authored-by: Eliz Kilic <elizk@google.com>
2026-02-23 11:16:40 +03:00
Dexif 6dc993852b fix: add missing keyboard metrics to Lyra3CoversTheme (#1101)
Fix for keyboard. Issue #1100
2026-02-22 21:12:22 -05:00
Dave Allie 63002d464b Merge branch 'release/1.1.0-rc' 2026-02-23 08:47:29 +11:00
Xuan-Son Nguyen 75ff7b25ab fix: double free WebDAVHandler (#1093)
## Summary

Ref:
https://github.com/crosspoint-reader/crosspoint-reader/pull/1047#discussion_r2838439305

To reproduce:
1. Open file transfer
2. Join a network
3. Once it's connected, press (hold) back

---

### 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-02-22 22:20:41 +01:00
Mirus 4ccafe5cfa feat: add Ukrainian translation (#1065)
## Summary

* **What is the goal of this PR?**
A Ukrainian translation for the GUI

* **What changes are included?**
Everything according to
https://github.com/crosspoint-reader/crosspoint-reader/blob/master/docs/i18n.md

## Additional Context

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

Nope

---

### 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 >**_ as a
consistency validation
2026-02-22 20:34:44 +03:00
Dave Allie ecb5b1b4e5 chore: Remove miniz and modularise inflation logic (#1073)
## Summary

* Remove miniz and move completely to uzlib
* Move uzlib interfacing to InflateReader to better modularise inflation
code

---

### 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 helped with
the extraction and refactor
2026-02-22 21:38:03 +11:00
Dave Allie f28623dacd chore: Resolve several build warnings (#1076)
## Summary

* Resolve several build warnings

---

### 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-02-22 20:56:13 +11:00
DexifandDave Allie a610568f8c feat: upgrade platform and support webdav (#1047)
## Summary
- Upgrade platform from espressif32 6.12.0 (Arduino Core 2.0.17) to
pioarduino 55.03.37 (Arduino Core 3.3.7, ESP-IDF 5.5.2)
- Add WebDAV Class 1 server (RFC 4918) - SD card can be mounted as a
network drive
- I also slightly fixed the SDK and also made a [pull request
](https://github.com/open-x4-epaper/community-sdk/pull/21)

First PR #1030 (was closed because the implementation was based on an
old version of the libraries)
Issue #439

---------

Co-authored-by: Dave Allie <dave@daveallie.com>
2026-02-22 20:31:33 +11:00
jpirnayandDave Allie d9f114b652 feat: Allow a local configuration file for custom compiles (#879)
## Summary

* platformio.ini is a repository based config for platformio and cannot
be modified without constant nagging of git to include it into you
commits
* PlatformIO allows you to split your configuration into multiple files
using the extra_configs option in the [platformio] block. This
effectively merges other .ini files into your main one. This will be
silently ignored if such a file does not exist

## Additional Context

* Modifiy platformio.ini and add a .gitignore entry to ignore your local
config
* eg my own ``platformio.local.ini``:
```
[env:custom]
extends = base
build_flags =
  ${base.build_flags}
  -DCROSSPOINT_VERSION=\"${crosspoint.version}-custom\"
  ; inclusion of additional fonts is disabled in custom builds to save space
  -DOMIT_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? _NO_

---------

Co-authored-by: Dave Allie <dave@daveallie.com>
2026-02-22 18:32:03 +11:00
3cb60aa231 fix: Close leaked file descriptors in SleepActivity and web server (#869)
## Summary

- **SleepActivity.cpp**: Add missing `file.close()` calls in 3 code
paths that open BMP files for sleep screen rendering but never close
them before returning. Affects random custom sleep images, the
`/sleep.bmp` fallback, and book cover sleep screens.
- **CrossPointWebServer.cpp**: Add missing `dir.close()` in the delete
handler when `Storage.open()` returns a valid `FsFile` that is not a
directory.

## Context

SdFat is configured with `DESTRUCTOR_CLOSES_FILE=0`, which means
`FsFile` objects are **not** automatically closed when they go out of
scope. Every opened file must be explicitly closed.

The SleepActivity leaks are particularly impactful because they occur on
every sleep cycle. While ESP32 deep sleep clears RAM on wake, these
leaks can still affect the current session if sleep screen rendering is
triggered multiple times (e.g., cover preview, or if deep sleep fails to
engage).

The web server leak in `handleDelete()` is a minor edge case (directory
path that opens successfully but `isDirectory()` returns false), but
it's still worth fixing for correctness.

## Test plan

- [x] Verify sleep screen still renders correctly (custom BMP, fallback,
cover modes)
- [x] Verify folder deletion still works via the web UI
- [ ] Monitor free heap before/after sleep screen rendering to confirm
no leak

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

Co-authored-by: Jan Bažant <janbazant@Jan--Mac-mini.local>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Dave Allie <dave@daveallie.com>
2026-02-22 18:15:02 +11:00
786b438ea2 feat: enhance file deletion functionality with multi-select (#682)
## Summary

* **What is the goal of this PR?** Enhances the file manager with
multi-select deletion functionality and improved UI formatting.
* **What changes are included?**
	* Added multi-select capability for file deletion in the web interface
	* Fixed formatting issues in file table for folder rows
* Updated [.gitignore] to exclude additional build artifacts and cache
files
	* Refactored CrossPointWebServer.cpp to support batch file deletion
* Enhanced FilesPage.html with improved UI for file selection and
deletion

## Additional Context

* The file deletion endpoint now handles multiple files in a single
request, improving efficiency when removing multiple files
* Changes are focused on the web file manager component only

---

### 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: Jessica Harrison <jessica.harrison@entelect.co.za>
Co-authored-by: Dave Allie <dave@daveallie.com>
2026-02-22 18:04:01 +11:00
jpirnayandDave Allie 6e4d0e534d feat: Migrate binary settings to json (#920)
## Summary

* This PR introduces a migration from binary file storage to JSON-based
storage for application settings, state, and various credential stores.
This improves readability, maintainability, and allows for easier manual
configuration editing.
* Benefits:
  - Settings files are now JSON and can be easily read/edited manually
  - Easier to inspect application state and settings during development
  - JSON structure is more flexible for future changes
* Drawback: around 15k of additional flash usage
* Compatibility: Seamless migration preserves existing user data

## Additional Context
1. New JSON I/O Infrastructure files:
- JsonSettingsIO: Core JSON serialization/deserialization logic using
ArduinoJson library
- ObfuscationUtils: XOR-based password obfuscation for sensitive data
2. Migrated Components (now use JSON storage with automatic binary
migration):
     - CrossPointSettings (settings.json): Main application settings
- CrossPointState (state.json): Application state (open book, sleep
mode, etc.)
- WifiCredentialStore (wifi.json): WiFi network credentials (Password
Obfuscation: Sensitive data like WiFi passwords, uses XOR encryption
with fixed keys. Note: This is obfuscation, not cryptographic security -
passwords can be recovered with the key)
- KOReaderCredentialStore (koreader.json): KOReader sync credentials
     - RecentBooksStore (recent.json): Recently opened books list
3. Migration Logic
     - Forward Compatibility: New installations use JSON format
- Backward Compatibility: Existing binary files are automatically
migrated to JSON on first load
- Backup Safety: Original binary files are renamed with .bak extension
after successful migration
- Fallback Handling: If JSON parsing fails, system falls back to binary
loading
4. Infrastructure Updates
     - HalStorage: Added rename() method for backup operations

---

### 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: Dave Allie <dave@daveallie.com>
2026-02-22 17:18:25 +11:00
Bilal f62529ad91 docs: Add lightweight contributor onboarding documentation (#894)
### Summary

This PR introduces a lightweight contributor onboarding docs section
under `docs/contributing/` and improves local formatting ergonomics for
first-time contributors.

The goal is to make CrossPoint easier to contribute to for software
developers who are new to embedded systems (like me), while keeping
onboarding modular and aligned with existing project docs.

### What changed

- Added contributor docs hub: `docs/contributing/README.md`
- Added focused onboarding pages:
  - `docs/contributing/getting-started.md`
  - `docs/contributing/architecture.md`
  - `docs/contributing/development-workflow.md`
  - `docs/contributing/testing-debugging.md`
- Linked contributor docs from `README.md` for discoverability
- Expanded architecture documentation with Mermaid diagrams
- Improved `bin/clang-format-fix`:
  - prefers `clang-format-21` when available
- validates formatter version and fails fast with a clear message if too
old
  - handles missing positional arg safely
- Updated docs to explain common `clang-format` setup/version issues and
install paths (including fallback steps when `clang-format-21` is
unavailable in default apt sources)

### Why

- There was no dedicated contributor onboarding path; first-time
contributors had to infer workflow from multiple files.
- New contributors (especially from non-embedded backgrounds) need a
clear mental model of architecture, runtime flow, and debugging process.
- Local formatting setup caused avoidable friction due to clang-format
version mismatch (`.clang-format` expects newer keys used in CI).
- The updates make contribution setup more predictable, reduce
onboarding confusion, and align local checks with CI expectations.

### Additional context

- No firmware behavior/runtime logic was changed; this PR focuses on
contributor experience and tooling clarity.

---

### AI Usage

> Did you use AI tools to help write this code?

Yes, I used AI tools to assist with generating the documentation. I then
manually reviewed, tested, and refined the code to ensure it works
correctly. please feel free to point out any discrepancies or areas for
improvement.
2026-02-22 16:50:08 +11:00
Carlos Bonadeoandcarlosbonadeo 88537769f6 style: Phase 1 - Simple light dark themes (#1006)
## Summary

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

Implement automatic dark theme on server files.

Instead of a big change proposed in
https://github.com/crosspoint-reader/crosspoint-reader/pull/837, this PR
introduces a simple implementation of light/dark themes.

* **What changes are included?**

- Choose `#6e9a82` as accent color (taken from
![logo](https://avatars.githubusercontent.com/u/254441081?s=48&v=4))
- Implement a very basic media query for dark themes (`@media
(prefers-color-scheme: dark)`)
- Update style using CSS variables 

## Additional Context

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

We can think of it as a incremental enhancement, this is the first phase
of a series of PRs (hopefully).

Next steps/Phases:
1. Light/Dark themes (this PR)
2. Load external CSS file to avoid duplication
3. HTML enhancement (for example, use dialog element instead of divs)
4. Use SVG instead of emojis
5. Use Vite + Typescript to improve DX and have better minification

---

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

---------

Co-authored-by: carlosbonadeo <carlosbonadeo@skyscanner.net>
2026-02-22 16:45:19 +11:00
Zach NelsonandKuanysh Bekkulov 3696794591 perf: Replace std::list with std::vector in text layout (#1038)
## Summary

_Revision to @blindbat's #802. Description comes from the original PR._

- Replace `std::list` with `std::vector` for word storage in `TextBlock`
and `ParsedText`
- Use index-based access (`words[i]`) instead of iterator advancement
(`std::advance(it, n)`)
- Remove the separate `continuesVec` copy that was built from
`wordContinues` for O(1) access — now unnecessary since
`std::vector<bool>` already provides O(1) indexing

## Why

`std::list` allocates each node individually on the heap with 16 bytes
of prev/next pointer overhead per node. For text layout with many small
words, this means:
- Scattered heap allocations instead of contiguous memory
- Poor cache locality during iteration (each node can be anywhere in
memory)
- Per-node malloc/free overhead during construction and destruction

`std::vector` stores elements contiguously, giving better cache
performance during the tight rendering and layout loops. The
`extractLine` function also benefits: list splice was O(1) but required
maintaining three parallel iterators, while vector range construction
with move iterators is simpler and still efficient for the small
line-sized chunks involved.

## Files changed

- `lib/Epub/Epub/blocks/TextBlock.h` / `.cpp`
- `lib/Epub/Epub/ParsedText.h` / `.cpp`

## AI Usage

YES

## Test plan

- [ ] Open an EPUB with mixed formatting (bold, italic, underline) —
verify text renders correctly
- [ ] Open a book with justified text — verify word spacing is correct
- [ ] Open a book with hyphenation enabled — verify words break
correctly at hyphens
- [ ] Navigate through pages rapidly — verify no rendering glitches or
crashes
- [ ] Open a book with long paragraphs — verify text layout matches
pre-change behavior

---------

Co-authored-by: Kuanysh Bekkulov <kbekkulov@gmail.com>
2026-02-22 15:28:56 +11:00
c1fad16e10 feat: Take screenshots (#759)
## Summary

* **What is the goal of this PR?** Implements a take-screenshot feature
* **What changes are included?**

- Quick press Power button and Down button at the same time to take a
screenshot
- Screenshots are saved in `screenshots` folder

## Additional Context

- Currently it does not use the device orientation.

---

Example screenshots:


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

[screenshot-6771.bmp](https://github.com/user-attachments/files/25157071/screenshot-6771.bmp)


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

[screenshot-14158.bmp](https://github.com/user-attachments/files/25157073/screenshot-14158.bmp)


### AI Usage

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

---------

Co-authored-by: Eliz Kilic <elizk@google.com>
Co-authored-by: Xuan Son Nguyen <son@huggingface.co>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Arthur Tazhitdinov <lisnake@gmail.com>
2026-02-22 15:22:32 +11:00
jpirnay 5f5561b684 fix: Fix hyphenation and rendering of decomposed characters (#1037)
## Summary

* This PR fixes decomposed diacritic handling end-to-end:
- Hyphenation: normalize common Latin base+combining sequences to
precomposed codepoints before Liang pattern matching, so decomposed
words hyphenate correctly
- Rendering: correct combining-mark placement logic so non-spacing marks
are attached to the preceding base glyph in normal and rotated text
rendering paths, with corresponding text-bounds consistency updates.
- Hyphenation around non breaking space variants have been fixed (and
extended)
- Hyphenation of terms that already included of hyphens were fixed to
include Liang pattern application (eg "US-Satellitensystem" was
*exclusively* broken at the existing hyphen)

## Additional Context

* Before
<img width="800" height="480" alt="2"
src="https://github.com/user-attachments/assets/b9c515c4-ab75-45cc-8b52-f4d86bce519d"
/>


* After
<img width="480" height="800" alt="fix1"
src="https://github.com/user-attachments/assets/4999f6a8-f51c-4c0a-b144-f153f77ddb57"
/>
<img width="800" height="480" alt="fix2"
src="https://github.com/user-attachments/assets/7355126b-80c7-441f-b390-4e0897ee3fb6"
/>

* Note 1: the hyphenation fix is not a 100% bullet proof implementation.
It adds composition of *common* base+combining sequences (e.g. O +
U+0308 -> Ö) during codepoint collection. A complete solution would
require implementing proper Unicode normalization (at least NFC,
possibly NFKC in specific cases) before hyphenation and rendering,
instead of hand-mapping a few combining marks. That was beyond the scope
of this fix.

* Note 2: the render fix should be universal and not limited to the
constraints outlined above: it properly x-centers the compund glyph over
the previous one, and it uses at least 1pt of visual distance in y.

Before:
<img width="478" height="167" alt="Image"
src="https://github.com/user-attachments/assets/f8db60d5-35b1-4477-96d0-5003b4e4a2a1"
/>

After: 
<img width="479" height="180" alt="Image"
src="https://github.com/user-attachments/assets/1b48ef97-3a77-475a-8522-23f4aca8e904"
/>

* This should resolve the issues described in #998 
---

### 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-02-22 13:11:07 +11:00
DestinySpeaker 10a2678584 fix: Fixed book title in home screen (#1013)
## Summary

* **What is the goal of this PR?** (e.g., Implements the new feature for
file uploading.)
* The goal is to fix the title of books in the Home Screen. 

Before

![IMG_8867](https://github.com/user-attachments/assets/6cc9ca22-b95b-4863-872d-ef427c42f833)

After:

![IMG_8868](https://github.com/user-attachments/assets/585031b1-2348-444c-8f32-073fed3b6582)

* **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, Cursor
2026-02-22 12:55:59 +11:00
pablohc e32d41a37e fix: improve Spanish translations (#1054)
## Summary

* **What is the goal of this PR?**
* improve Spanish translations

* **What changes are included?**

- Fix typos and accents (Librería, conexión, etc.)
- Translate untranslated strings (BOOTING, SLEEPING, etc.)
- Improve consistency and conciseness
- Fix question mark placement (¿...?)
- Standardize terminology (Punto de Acceso, Suspensión, etc.)

---

### 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-02-21 22:30:42 +11:00
Lev Roland-KalbandCopilot 7717ae2683 feat: Added BmpViewer activity for viewing .bmp images in file browser (#887)
## Summary

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

Implements new feature for viewing .bmp files directly from the "Browse
Files" menu.

* **What changes are included?**

You can now view .bmp files when browsing. You can click the select
button to open the file, and then click back to close it and continue
browsing in the same location. Once open a file will display on the
screen with no additional options to interact outside of exiting with
the back button.

The attached video shows this feature in action:


https://github.com/user-attachments/assets/9659b6da-abf7-4458-b158-e11c248c8bef

## Additional Context

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

The changes implemented in #884 are also present here as this feature is
actually what led to me noticing this issue. I figured I would add that
PR as a separate request in case that one could be more easily merged
given this feature is significantly more complicated and will likely be
subject to more intense review.

---

### 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-02-21 12:27:25 +03:00
Àngel f02c9784ec feat: add Catalan strings (#1049)
## Summary

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

Add support for Catalan language user interface.
* **What changes are included?**

A new i18n file catalan.yml.

## 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-02-21 12:26:50 +03:00
Luke Stein 693dba4c94 fix: Shorten "Forget Wifi" button labels to fit on button (#1045) 2026-02-20 20:05:03 -05:00
ariel-lindemann 9c55c15a72 feat: added Romanian strings (#987)
## Summary

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

To add upport for a romanian language user interface.

* **What changes are included?**

A new i18n file `romanian.yml`

## 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-02-20 20:27:43 +03:00
Arnau 6ba9658f15 fix: typo in USER_GUIDE.md (#1036)
## Summary

Just fixed a typo `Xtink` -> `Xteink`

---

### 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-02-20 17:22:17 +03:00
Dave Allie 22b96ec22a fix: Destroy CSS Cache file when invalid (#1018)
## Summary

* Destroy CSS Cache file when invalid

## Additional Context

* Fixes issue where it would attempt to rebuild every book open

---

### 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-02-20 17:04:50 +11:00
pablohc c9faf2a8c0 fix: continue reading card classic theme (#990)
## Summary

* **What is the goal of this PR?** 
* **What changes are included?**

- Adapt card width to cover image aspect ratio in Classic theme
- Increase homeTopPadding from 20px to 40px to avoid overlap with
battery icon
- Card width now calculated from BMP dimensions instead of fixed 240px
- Maximum card width limited to 90% of screen width
- Falls back to original behavior (half screen width) when no cover
available

## Additional Context

* Solve conflicts in PR #683 

Before:
<img width="1052" height="1014" alt="image"
src="https://github.com/user-attachments/assets/6c857913-d697-4e9e-9695-443c0a4c0804"
/>

PR:

![Screenshot_2026-02-19-14-22-36-68_99c04817c0de5652397fc8b56c3b3817](https://github.com/user-attachments/assets/81505728-d42e-41bd-bd77-44848e05b1eb)


---

### 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-02-20 16:35:58 +11:00
Dave Allie 356fe9a31e fix: Strip unused CSS rules (#1014)
## Summary

* In a sample book I loaded, it had 900+ CSS rules, and took up 180kB of
memory loading the cache in
* Looking at the rules, a lot of them were completely useless as we only
ever apply look for 3 kinds of CSS rules:
    * `tag`
    * `tag.class1`
    * `.class1`
* Stripping out CSS rules with descendant, nested, attribute matching,
sibling matching, pseudo element selection (as we never actually read
these from the cache) reduced the rule count down to 200

## Additional Context

* I've left in `.class1.class2` rules for now, even though we
technically can never match on them as they're likely to be addressed
soonest out of the all the CSS expansion
* Because we don't ever delete the CSS cache, users will need to delete
the book cache through the menu in order to get this new logic
* A new PR should be done up to address this - tracked here
https://github.com/crosspoint-reader/crosspoint-reader/issues/1015

---

### 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-02-20 16:35:49 +11:00
DestinySpeaker 5da23eed82 fix: Fixed Image Sizing When No Width is Set (#1002)
## Summary

* **What is the goal of this PR?** (e.g., Implements the new feature for
file uploading.)
When no width is set for an image, the image currently automatically
sets to the width of the page. However, with this fix, the parser will
use the height and aspect ratio of the image to properly set a height
for it. See below example:


Before:

![IMG_8862](https://github.com/user-attachments/assets/64b3b92f-1165-45ca-8bdb-8e69613d9725)

After:

![IMG_8863](https://github.com/user-attachments/assets/5cb99b12-d150-4b37-ae4c-c8a20eb9f3a0)


* **What changes are included?✱
Changes to the CSS parser

## 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, Cursor
2026-02-20 16:34:28 +11:00
ariel-lindemann 388fbf206a docs: image support marked as completed (#1008)
## Summary

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

Epub image support was added in #556. The goal of this PR is to document
that in the readme.

* **What changes are included?**

Only the checkmark in the readme.

## 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-02-20 13:13:25 +11:00
martin brook 2a38bfd8af fix: use double FAST_REFRESH to prevent washout on large grey images (#957)
## Summary

Fixes https://github.com/crosspoint-reader/crosspoint-reader/issues/1011

Use double FAST_REFRESH for image pages to prevent grayscale washout,
HALF_REFRESH sets e-ink particles too firmly for the grayscale LUT to
adjust, causing washed-out images (especially large, light-gray ones).
Replace HALF_REFRESH with @pablohc's double FAST_REFRESH technique:
blank only the image bounding box area, then re-render with images. This
clears ghosting while keeping particles loosely set for grayscale.

## 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? _**<  PARTIALLY  >**_
2026-02-20 12:05:15 +11:00
Lev Roland-Kalb d7f89e6c0d fix: re-implementing Cover Outlines for the new Lyra Themes (#1017)
## Summary

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

Improve legibility of Cover Icons on the home page and elsewhere. Fixes
#898
Re implements the changes made in #907 that were overwritten by the new
lyra themes

* **What changes are included?**
Cover outline is now shown even when cover is found to prevent issues
with low contrast covers blending into the background. Photo is attached
below:

<img width="1137" height="758" alt="Untitled (4)"
src="https://github.com/user-attachments/assets/21ae6c94-4b43-4a0c-bec7-a6e4c642ffad"
/>



## Additional Context

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

Re implements the changes made in #907 that were overwritten by the new
lyra themes

---

### 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-02-20 11:56:19 +11:00
jpirnay 07d715e32d perf: Improve font drawing performance (#978)
## Summary

* ``renderChar`` checked ``is2Bit`` on every pixel inside the inner
loop, even though the value is constant for the lifetime of a single
glyph
* Moved the branch above both loops so each path (2-bit antialiased /
1-bit monochrome) runs without a per-pixel conditional
* Eliminates redundant work in the two inner loops that render font
glyphs to the frame buffer, targeting ``renderChar`` and
``drawTextRotated90CW`` in ``GfxRenderer.cpp``

## Additional Context
* Measured on device using a dedicated framebuffer benchmark (no display
refresh). 100 repetitions of "The quick brown fox jumps".

| Test            | Before          | After           | Change  |
|-----------------|-----------------|-----------------|---------|
| drawText UI12   | 1,337 µs/call | 1,024 µs/call | −23%|
| drawText Bookerly14 | 2.174 µs / call | 1,847 µs/call | −15% |

---

### 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 did
the analysis and wrote the benchmarks
2026-02-20 11:12:05 +11:00
Uri Tauber 63b2643534 fix: Fix dangling pointer (#1010)
## Summary

* **What is the goal of this PR?** Small fix for bug I found.

## Additional Context


1. `RecentBooksActivity::loop()` calls
`onSelectBook(recentBooks[selectorIndex].path)` - passing a
**reference** to the path
  2. `onSelectBook` is `onGoToReader` which first calls `exitActivity()`
3. `exitActivity()` triggers `RecentBooksActivity::onExit()` which call
`recentBooks.clear()`
4. The string reference `initialEpubPath` is now a **dangling
reference** - the underlying string has been destroyed
5. When the reference is then used in `new ReaderActivity(...)`, it
reads garbage memory
6. The same issue occurs in `HomeActivity` at line 200 with the same
pattern

The fix is to make a copy of the string in `onGoToReader` before calling
`exitActivity()`, so the path data persists even after the activity
clears its data structures.

---

### AI Usage

Did you use AI tools to help write this code? _**< YES >**_ Claude found
the bug, after I shared with it a serial log.
2026-02-20 11:08:37 +11:00
Vincent Politzer cabbfcfd7e fix: Use HalPowerManager for battery percentage (#1005)
## Summary

The introduction of `HalGPIO` moved the `BatteryMonitor battery` object
into the member function `HalGPIO::getBatteryPercentage()`.

Then, with the introduction of `HalPowerManager`, this function was
moved to `HalPowerManager::getBatteryPercentage()`.

However, the original `BatteryMonitor battery` object is still utilized
by themes for displaying the battery percentage.

This PR replaces these deprecated uses of `BatteryMonitor battery` with
the new `HalPowerManager::getBatteryPercentage()` function.

---

### 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-02-20 00:05:35 +01:00
Arthur Tazhitdinov d461d93e76 fix: Increase PNGdec buffer size to support wide images (#995)
## Summary

* Increased `PNG_MAX_BUFFERED_PIXELS` from 6402 to 16416 in
`platformio.ini` to support up to 2048px wide RGBA images
* adds a check to abort decoding and log an error if the required PNG
scanline buffer exceeds the configured `PNG_MAX_BUFFERED_PIXELS`,
preventing possible buffer overruns.
* fixes
https://github.com/crosspoint-reader/crosspoint-reader/issues/993

---

### 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-02-20 03:51:38 +11:00
Uri Tauber ca89e41636 fix: Crash (Load access fault) when indexing chapters containing characters unsupported by bold/italic font variants (#997)
## Summary

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

I flashed the last revision before commit f1740dbe, and chapter indexing
worked without any crashes.
After applying f1740dbe, the same chapter consistently triggered a
device reboot during indexing.

The affected chapter contains inline equation images surrounded by
styled (bold/italic) text that includes special math/symbol characters.

## Additional Context

Prior to f1740dbe, both `getTextAdvanceX()` and `getSpaceWidth()` always
measured text using `EpdFontFamily::REGULAR`, regardless of the actual
style.

Commit f1740dbe improved correctness by passing the active style so
spacing is calculated using the actual bold/italic font variant.

However, bold and italic variants have narrower Unicode coverage than
the regular font. When a character exists in the regular font but not in
the selected styled variant, `pdFont::getGlyph()` returns `nullptr`.

The updated measurement functions did not check for this and immediately
dereferenced the pointer:

`width += font.getGlyph(cp, style)->advanceX;   // nullptr->advanceX`


Because `advanceX` is located at byte offset 2 within `EpdGlyph`,
dereferencing a null pointer caused the CPU to attempt a load from
address `0x00000002`, resulting in a RISC-V:
Load access fault
MCAUSE = 5
MTVAL = 2


## Fix

Added null-safety checks to both `getTextAdvanceX()` and
`getSpaceWidth()`, following the same pattern used in the rendering
path:

If the glyph is missing in the selected style → fall back to the
replacement glyph.

If the replacement glyph is also unavailable → treat the character as
zero-width.

This preserves the improved style-correct spacing while preventing
crashes.
No behavioral changes occur for characters that are supported by the
selected font variant.

---

### AI Usage

Did you use AI tools to help write this code? _**< YES >**_
I encounter this bug while testing 1.1.0 RC. 
I pasted the serial log to Claude, which identify the bug and fixed it.
I can confirm now the chapter in question is indexed and loaded
correctly.
2026-02-20 03:44:46 +11:00
432 changed files with 250567 additions and 155906 deletions
+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
View File
@@ -0,0 +1 @@
../../.skills/SKILL.md
+14 -2
View File
@@ -44,8 +44,14 @@ jobs:
with:
python-version: '3.14'
- name: Install uv
uses: astral-sh/setup-uv@v7
with:
version: "latest"
enable-cache: false
- name: Install PlatformIO Core
run: pip install --upgrade platformio
run: uv pip install --system -U https://github.com/pioarduino/platformio-core/archive/refs/tags/v6.1.19.zip
- name: Run cppcheck
run: pio check --fail-on-defect low --fail-on-defect medium --fail-on-defect high
@@ -61,8 +67,14 @@ jobs:
with:
python-version: '3.14'
- name: Install uv
uses: astral-sh/setup-uv@v7
with:
version: "latest"
enable-cache: false
- name: Install PlatformIO Core
run: pip install --upgrade platformio
run: uv pip install --system -U https://github.com/pioarduino/platformio-core/archive/refs/tags/v6.1.19.zip
- name: Build CrossPoint
run: |
+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 -8
View File
@@ -12,19 +12,18 @@ jobs:
with:
submodules: recursive
- uses: actions/cache@v5
with:
path: |
~/.cache/pip
~/.platformio/.cache
key: ${{ runner.os }}-pio
- uses: actions/setup-python@v6
with:
python-version: '3.14'
- name: Install uv
uses: astral-sh/setup-uv@v7
with:
version: "latest"
enable-cache: false
- name: Install PlatformIO Core
run: pip install --upgrade platformio
run: uv pip install --system -U https://github.com/pioarduino/platformio-core/archive/refs/tags/v6.1.19.zip
- name: Build CrossPoint
run: pio run -e gh_release
+7 -8
View File
@@ -12,19 +12,18 @@ jobs:
with:
submodules: recursive
- uses: actions/cache@v5
with:
path: |
~/.cache/pip
~/.platformio/.cache
key: ${{ runner.os }}-pio
- uses: actions/setup-python@v6
with:
python-version: '3.14'
- name: Install uv
uses: astral-sh/setup-uv@v7
with:
version: "latest"
enable-cache: false
- name: Install PlatformIO Core
run: pip install --upgrade platformio
run: uv pip install --system -U https://github.com/pioarduino/platformio-core/archive/refs/tags/v6.1.19.zip
- name: Extract env
run: |
+9
View File
@@ -3,6 +3,8 @@
.DS_Store
.vscode
lib/EpdFont/fontsrc
lib/I18n/I18nKeys.h
lib/I18n/I18nStrings.h
lib/I18n/I18nStrings.cpp
*.generated.h
.vs
@@ -10,3 +12,10 @@ build
**/__pycache__/
/compile_commands.json
/.cache
.history/
/.venv
*.local*
*.cpfont
lib/EpdFont/scripts/downloaded_fonts/
lib/EpdFont/scripts/instanced_fonts/
lib/EpdFont/scripts/output/
+1 -1
View File
@@ -1,3 +1,3 @@
[submodule "open-x4-sdk"]
path = open-x4-sdk
url = https://github.com/open-x4-epaper/community-sdk.git
url = https://github.com/crosspoint-reader/community-sdk.git
+881
View File
@@ -0,0 +1,881 @@
# CrossPoint Reader Development Guide
Project: Open-source e-reader firmware for Xteink X4 (ESP32-C3)
Mission: Provide a lightweight, high-performance reading experience focused on EPUB rendering on constrained hardware.
## AI Agent Identity and Cognitive Rules
* 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.
* 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).
---
## Development Environment Awareness
**CRITICAL**: Detect the host platform at session start to choose appropriate tools and commands.
### Platform Detection
```bash
# Detect platform (run once per session)
uname -s
# Returns: MINGW64_NT-* (Windows Git Bash), Linux, Darwin (macOS)
```
**Detection Required**: Run `uname -s` at session start to determine platform
### Platform-Specific Behaviors
- **Windows (Git Bash)**: Unix commands, `C:\` paths in Windows but `/` in bash, limited glob (use `find`+`xargs`)
- **Linux/WSL**: Full bash, Unix paths, native glob support
**Cross-Platform Code Formatting**:
```bash
find src -name "*.cpp" -o -name "*.h" | xargs clang-format -i
```
---
## Platform and Hardware Constraints
### Hardware Specs
* MCU: ESP32-C3 (Single-core RISC-V @ 160MHz)
* RAM: ~380KB usable (VERY LIMITED - primary project constraint)
* **NO PSRAM**: ESP32-C3 has no PSRAM capability (unlike ESP32-S3)
* **Single Buffer Mode**: Only ONE 48KB framebuffer (not double-buffered)
* Flash: 16MB (Instruction storage and static data)
* Display: 800x480 E-Ink (Slow refresh, monochrome, 1-2s full update)
* Framebuffer: 48,000 bytes (800 × 480 ÷ 8)
* Storage: SD Card (Used for books and aggressive caching)
### The Resource Protocol
1. Stack Safety: Limit local function variables to < 256 bytes. The ESP32-C3 default stack is small; use std::unique_ptr or static pools for larger buffers.
2. Heap Fragmentation: Avoid repeated new/delete in loops. Allocate buffers once during onEnter() and reuse them.
3. Flash Persistence: Large constant data (UI strings, lookup tables) MUST be marked static const to stay in Flash (Instruction Bus), freeing DRAM.
4. String Policy: Prohibit std::string and Arduino String in hot paths. Use std::string_view for read-only access and snprintf with fixed char[] buffers for construction.
5. UI Strings: All user-facing text must use the `tr()` macro (e.g., `tr(STR_LOADING)`) for i18n support. Never hardcode UI strings directly. For the avoidance of doubt, logging messages (LOG_DBG/LOG_ERR) can be hardcoded, but user-facing text must use `tr()`.
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.
---
## Project Architecture
### Build System: PlatformIO
**PlatformIO is BOTH a VS Code extension AND a CLI tool**:
1. **VS Code Extension** (Recommended):
* Extension ID: `platformio.platformio-ide` (see `.vscode/extensions.json`)
* Provides: Toolbar buttons, IntelliSense, integrated build/upload/monitor
* Configuration: `.vscode/c_cpp_properties.json`, `.vscode/tasks.json`
* Usage: Click Build (✓), Upload (→), or Monitor (🔌) buttons
2. **CLI Tool** (`pio` command):
* **Installation**: Python package (typically `pip install platformio`)
* **Windows Location**: `C:\Users\<user>\AppData\Local\Programs\Python\Python3xx\Scripts\pio.exe`
* **Verify**: `which pio` (Git Bash) or `where.exe pio` (cmd)
* **Usage**: `pio run`, `pio run -t upload`, etc.
**Configuration Files**:
* `platformio.ini`: Main build configuration (committed to git)
* `platformio.local.ini`: Local overrides (gitignored, create if needed)
* `partitions.csv`: ESP32 flash partition layout
### Build Environment
* **Standard**: C++20 (`-std=c++2a`). No Exceptions, No RTTI.
* **Logging**: ALWAYS use `LOG_INF`, `LOG_DBG`, or `LOG_ERR` from `Logging.h`. Raw Serial output is deprecated.
* **Environments** (in `platformio.ini`):
* `default`: Development (LOG_LEVEL=2, serial enabled)
* `gh_release`: Production (LOG_LEVEL=0)
* `gh_release_rc`: Release candidate (LOG_LEVEL=1)
* `slim`: Minimal build (no serial logging)
### Critical Build Flags
These flags in `platformio.ini` fundamentally affect firmware behavior:
```cpp
-DEINK_DISPLAY_SINGLE_BUFFER_MODE=1 // Single framebuffer (saves 48KB RAM!)
-DARDUINO_USB_MODE=1 // Enable USB CDC
-DARDUINO_USB_CDC_ON_BOOT=1 // Serial available immediately at boot
-DXML_CONTEXT_BYTES=1024 // XML parser memory limit (EPUB parsing)
-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()`)
- Must call `renderer.restoreBwBuffer()` to free temporary buffers
- See [lib/GfxRenderer/GfxRenderer.cpp:439-440](../lib/GfxRenderer/GfxRenderer.cpp) for malloc usage
### Directory Structure
* lib/: Internal libraries (Epub engine, GfxRenderer, UITheme, I18n)
* 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)
* .crosspoint/: SD-based binary cache for EPUB metadata and pre-rendered layout sections
### Hardware Abstraction Layer (HAL)
**CRITICAL**: Always use HAL classes, NOT SDK classes directly.
| HAL Class | Wraps SDK Class | Purpose | Singleton Macro |
|-----------|----------------|---------|-----------------|
| `HalDisplay` | `EInkDisplay` | E-ink display control | *(none)* |
| `HalGPIO` | `InputManager` | Button input handling | *(none)* |
| `HalStorage` | `SDCardManager` | SD card file I/O | `Storage` |
**Location**: [lib/hal/](../lib/hal/)
**Why HAL?**
- Provides consistent error logging per module
- Abstracts SDK implementation details
- Centralizes resource management
**Example - HalStorage**:
```cpp
#include <HalStorage.h>
// Use Storage singleton (defined via macro)
FsFile file;
if (Storage.openFileForRead("MODULE", "/path/to/file.bin", file)) {
// Read from file
// No file.close() needed — DESTRUCTOR_CLOSES_FILE=1 handles it at scope exit
}
```
**Usage**: See example above. Uses `FsFile` (SdFat), NOT Arduino `File`. Do NOT add `file.close()` for local variables (see DESTRUCTOR_CLOSES_FILE above).
---
## Coding Standards
### Naming Conventions
* Classes: PascalCase (e.g., EpubReaderActivity)
* Methods/Variables: camelCase (e.g., renderPage())
* Constants: UPPER_SNAKE_CASE (e.g., MAX_BUFFER_SIZE)
* Private Members: memberVariable (no prefix)
* File Names: Match Class names (e.g., EpubReaderActivity.cpp)
### Header Guards
* Use #pragma once for all header files.
### 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. 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
#### `std::string_view` and Null Termination
`string_view` is *not* null-terminated. Passing `.data()` to any C-style API (`drawText`, `snprintf`, `strcmp`, SdFat file paths) is undefined behaviour when the view is a substring or a view of a non-null-terminated buffer.
**Rule**: `string_view` is safe only when passing to C++ APIs that accept `string_view`. For any C API boundary, convert explicitly:
```cpp
// WRONG - undefined behaviour if view is a substring:
renderer.drawText(font, x, y, myView.data(), true);
// CORRECT - guaranteed null-terminated:
renderer.drawText(font, x, y, std::string(myView).c_str(), true);
// CORRECT - for short strings, use a stack buffer:
char buf[64];
snprintf(buf, sizeof(buf), "%.*s", (int)myView.size(), myView.data());
```
#### `IRAM_ATTR` and Flash Cache Safety
All code runs from flash via the instruction cache. During SPI flash operations (OTA write, SPIFFS commit, NVS update) the cache is briefly suspended. Any code that can execute during this window — ISRs in particular — must reside in IRAM or it will crash silently.
```cpp
// ISR handler: must be in IRAM
void IRAM_ATTR gpioISR() { ... }
// Data accessed from IRAM_ATTR code: must be in DRAM, never a flash const
static DRAM_ATTR uint32_t isrEventFlags = 0;
```
**Rules**:
- All ISR handlers: `IRAM_ATTR`
- Data read by `IRAM_ATTR` code: `DRAM_ATTR` (a flash-resident `static const` will fault)
- Normal task code does **not** need `IRAM_ATTR`
#### ISR vs Task Shared State
`xSemaphoreTake()` (mutex) **cannot** be called from ISR context — it will crash. Use the correct primitive for each communication direction:
| Direction | Correct primitive |
|---|---|
| ISR → task (data) | `xQueueSendFromISR()` + `portYIELD_FROM_ISR()` |
| ISR → task (signal) | `xSemaphoreGiveFromISR()` + `portYIELD_FROM_ISR()` |
| Task → task | `xSemaphoreTake()` / mutex |
| Simple flag (single writer ISR) | `volatile bool` + `portENTER_CRITICAL_ISR()` |
#### RISC-V Alignment
ESP32-C3 faults on unaligned multi-byte loads. Never cast a `uint8_t*` buffer to a wider pointer type and dereference it directly. Use `memcpy` for any unaligned read:
```cpp
// WRONG — faults if buf is not 4-byte aligned:
uint32_t val = *reinterpret_cast<const uint32_t*>(buf);
// CORRECT:
uint32_t val;
memcpy(&val, buf, sizeof(val));
```
This applies to all cache deserialization code and any raw buffer-to-struct casting. `__attribute__((packed))` structs have the same hazard when accessed via member reference.
#### Template and `std::function` Bloat
Each template instantiation generates a separate binary copy. `std::function<void()>` adds ~24 KB per unique signature and heap-allocates its closure. Avoid both in library code and any path called from the render loop:
```cpp
// Avoid — heap-allocating, large binary footprint:
std::function<void()> callback;
// Prefer — zero overhead:
void (*callback)() = nullptr;
// For member function + context (common activity callback pattern):
struct Callback { void* ctx; void (*fn)(void*); };
```
When a template is necessary, limit instantiations: use explicit template instantiation in a `.cpp` file to prevent the compiler from generating duplicates across translation units.
---
### Error Handling Philosophy
**Source**: [src/main.cpp:132-143](../src/main.cpp), [lib/GfxRenderer/GfxRenderer.cpp:10](../lib/GfxRenderer/GfxRenderer.cpp)
**Pattern Hierarchy**:
1. **LOG_ERR + return false** (90%): `LOG_ERR("MOD", "Failed: %s", reason); return false;`
2. **LOG_ERR + fallback**: `LOG_ERR("MOD", "Unavailable"); useDefault();`
3. **assert(false)**: Only for fatal "impossible" states (framebuffer missing)
4. **ESP.restart()**: Only for recovery (OTA complete)
**Rules**: NO exceptions, NO abort(), ALWAYS log before error return
### Acceptable malloc/free Patterns
**Source**: [src/activities/home/HomeActivity.cpp:166](../src/activities/home/HomeActivity.cpp), [lib/GfxRenderer/GfxRenderer.cpp:439-440](../lib/GfxRenderer/GfxRenderer.cpp)
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**:
```cpp
// Allocate
auto* buffer = static_cast<uint8_t*>(malloc(bufferSize));
if (!buffer) {
LOG_ERR("MODULE", "malloc failed: %d bytes", bufferSize);
return false; // Handle allocation failure
}
// Use buffer
processData(buffer, bufferSize);
// Free immediately after use
free(buffer);
buffer = nullptr;
```
**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
**Examples in codebase**:
- 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)
---
## UI and Orientation Guidelines
### Orientation-Aware Logic
* No Hardcoding: Never assume 800 or 480. Use renderer.getScreenWidth() and renderer.getScreenHeight().
* Viewable Area: Use renderer.getOrientedViewableTRBL() to stay within physical bezel margins.
### Logical Button Mapping
**Source**: [src/MappedInputManager.cpp:20-55](../src/MappedInputManager.cpp)
Constraint: Physical button positions are fixed on hardware, but their logical functions change based on user settings and screen orientation.
**Button Categories**:
1. **Physical Fixed** (Up/Down side buttons):
- `Button::Up` → Always `HalGPIO::BTN_UP`
- `Button::Down` → Always `HalGPIO::BTN_DOWN`
2. **User Remappable** (Front buttons):
- `Button::Back` → Maps to `SETTINGS.frontButtonBack` (hardware index)
- `Button::Confirm` → Maps to `SETTINGS.frontButtonConfirm`
- `Button::Left` → Maps to `SETTINGS.frontButtonLeft`
- `Button::Right` → Maps to `SETTINGS.frontButtonRight`
3. **Reader-Specific** (Page navigation with optional swap):
- `Button::PageBack` → Uses side button (swappable via `SETTINGS.sideButtonLayout`)
- `Button::PageForward` → Uses side button (swappable)
**Implementation**:
- Activities use **logical buttons** (e.g., `Button::Confirm`)
- `MappedInputManager` translates to **physical hardware buttons**
- User can remap front buttons in settings
- Orientation changes handled separately by renderer coordinate transforms
**Rule**: Always use `MappedInputManager::Button::*` enums, never raw `HalGPIO::BTN_*` indices (except in ButtonRemapActivity).
### UITheme (The GUI Macro)
* Rule: All UI rendering must go through the GUI macro (UITheme).
* Do not hardcode fonts, colors, or positioning. This ensures orientation-aware layout consistency.
---
## Common Patterns
### Singleton Access
**Available Singletons**:
```cpp
#define SETTINGS CrossPointSettings::getInstance() // User settings
#define APP_STATE CrossPointState::getInstance() // Runtime state
#define GUI UITheme::getInstance() // Current theme
#define Storage HalStorage::getInstance() // SD card I/O
#define I18N I18n::getInstance() // Internationalization
```
### Activity Lifecycle and Memory Management
**Source**: [src/main.cpp:132-143](../src/main.cpp)
**CRITICAL**: Activities are **heap-allocated** and **deleted on exit**.
```cpp
// main.cpp navigation pattern
void exitActivity() {
if (currentActivity) {
currentActivity->onExit();
delete currentActivity; // Activity deleted here!
currentActivity = nullptr;
}
}
void enterNewActivity(Activity* activity) {
currentActivity = activity; // Heap-allocated activity
currentActivity->onEnter();
}
```
**Memory Implications**:
- 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
- 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 member FsFiles */ Activity::onExit(); }
```
**Critical**: Free resources in reverse order. Delete tasks BEFORE activity destruction.
### FreeRTOS Task Guidelines
**Source**: [src/activities/util/KeyboardEntryActivity.cpp:45-50](../src/activities/util/KeyboardEntryActivity.cpp)
**Pattern**: See Activity Lifecycle above. `xTaskCreate(&taskTrampoline, "Name", stackSize, this, 1, &handle)`
**Stack Sizing** (in BYTES, not words):
- **2048**: Simple rendering (most activities)
- **4096**: Network, EPUB parsing
- Monitor: `uxTaskGetStackHighWaterMark()` if crashes
**Rules**: Always `vTaskDelete()` in `onExit()` before destruction. Use mutex if shared state.
### Global Font Loading
**Source**: [src/main.cpp:40-115](../src/main.cpp)
**All fonts are loaded as global static objects** at firmware startup:
- 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
**Compilation Flag**:
```cpp
#ifndef OMIT_FONTS
// Most fonts loaded here
#endif
```
**Implications**:
- Fonts stored in **Flash** (marked as `static const` in `lib/EpdFont/builtinFonts/`)
- Font rendering data cached in **DRAM** when first used
- `OMIT_FONTS` can reduce binary size for minimal builds
- Font IDs defined in [src/fontIds.h](../src/fontIds.h)
**Usage**:
```cpp
#include "fontIds.h"
renderer.insertFont(FONT_UI_MEDIUM, ui12FontFamily);
renderer.drawText(FONT_UI_MEDIUM, x, y, "Hello", true);
```
---
## Testing and Debugging
### Build Commands
**Via CLI**:
```bash
# Build firmware (default environment)
pio run
# Build and upload to device
pio run -t upload
# Build specific environment
pio run -e gh_release
# Clean build artifacts
pio run -t clean
# Upload filesystem data (if using SPIFFS/LittleFS)
pio run -t uploadfs
```
**Via VS Code**:
* Use PlatformIO toolbar: Build (✓), Upload (→), Clean (🗑️)
* Or Command Palette: `PlatformIO: Build`, `PlatformIO: Upload`, etc.
### Monitoring and Debugging
```bash
# Enhanced monitor with color/logging (recommended)
python3 scripts/debugging_monitor.py
# Standard PlatformIO monitor
pio device monitor
# Combined upload + monitor
pio run -t upload && pio device monitor
```
**Via VS Code**: Click Monitor (🔌) button in PlatformIO toolbar
### Code Quality
```bash
# Static analysis (cppcheck)
pio check
# Format code (clang-format) - Windows Git Bash
find src -name "*.cpp" -o -name "*.h" | xargs clang-format -i
# Format code (clang-format) - Linux
clang-format -i src/**/*.cpp src/**/*.h
```
### Debugging Crashes
**Common Crash Causes**:
1. **Out of Memory** (Most common):
```cpp
LOG_DBG("MEM", "Free heap: %d bytes", ESP.getFreeHeap());
```
- Monitor heap usage throughout activity lifecycle
- Check if large allocations (>10KB) occur before crash
- Verify buffers are freed in `onExit()`
2. **Stack Overflow**:
```cpp
LOG_DBG("TASK", "Stack high water: %d", uxTaskGetStackHighWaterMark(taskHandle));
```
- Occurs during deep recursion or large local variables
- Increase task stack size in `xTaskCreate()` (2048 → 4096)
- Move large buffers to heap with malloc
3. **Use-After-Free**:
- Activity deleted but task still running
- Always `vTaskDelete()` in `onExit()` BEFORE activity destruction
- Set pointers to `nullptr` after `free()`
4. **Corrupt Cache Files**:
- Delete `.crosspoint/` directory on SD card
- Forces clean re-parse of all EPUBs
- Check file format versions in [docs/file-formats.md](../docs/file-formats.md)
5. **Watchdog Timeout**:
- Loop/task blocked for >5 seconds
- Add `vTaskDelay(1)` in tight loops
- Check for blocking I/O operations
**Verification Steps**:
1. Check serial output for stack traces
2. Monitor heap with `ESP.getFreeHeap()` before/after operations
3. Verify task deletion with task list (`vTaskList()`)
4. Test with `LOG_LEVEL=2` (debug logging enabled)
---
## Git Workflow and Repository Awareness
### Repository Detection Protocol
**CRITICAL**: ALWAYS verify repository context before git operations. This could be:
- A **fork** with `origin` pointing to personal repo, `upstream` to main repo
- A **direct clone** with `origin` pointing to main repo
- Multiple collaborator remotes
**Verification Commands** (run at session start):
```bash
# Check current branch
git branch --show-current
# Check all remotes
git remote -v
# Identify main branch name (could be 'main' or 'master')
git symbolic-ref refs/remotes/origin/HEAD 2>/dev/null | sed 's@^refs/remotes/origin/@@'
# Check working tree status
git status --short
```
**Example Output** (forked repository):
```text
origin https://github.com/<your-username>/crosspoint-reader.git (fetch/push)
upstream https://github.com/crosspoint-reader/crosspoint-reader.git (fetch/push)
```
### Git Operation Rules
1. **Never assume branch names**:
```bash
# Bad: git push origin main
# Good: git push origin $(git branch --show-current)
```
2. **Never assume remote names or write permissions**:
- **Forked repos**: Push to `origin` (your fork), submit PR to `upstream`
- **Direct contributors**: May push feature branches to `upstream`
- **Always ask**: "Should I push to origin or create a PR?"
3. **Check for upstream changes before starting work**:
```bash
# Sync fork with upstream (if applicable)
git fetch upstream
git merge upstream/main # or upstream/master
```
4. **Use explicit remote and branch names**:
```bash
# Check remotes first
git remote -v
# Use explicit syntax
git push <remote> <branch>
```
### Branch Naming Convention
**For feature/fix branches**:
```text
feature/<short-description> # New features
fix/<issue-number>-<description> # Bug fixes
refactor/<component-name> # Code refactoring
docs/<topic> # Documentation updates
```
**Examples**:
- `feature/sd-download-progress`
- `fix/123-orientation-crash`
- `refactor/hal-storage`
### Commit Message Format
**Pattern**:
```text
<type>: <short summary (50 chars max)>
<optional detailed description>
```
**Types**: `feat`, `fix`, `refactor`, `docs`, `test`, `chore`, `perf`
**Example**:
```text
feat: add real-time SD download progress bar
Implements progress tracking for book downloads using
UITheme progress bar component with heap-safe updates.
Tested in all 4 orientations with 5MB+ files.
```
### When to Commit
**DO commit when**:
- User explicitly requests: "commit these changes"
- Feature is complete and tested on device
- Bug fix is verified working
- Refactoring preserves all functionality
- All tests pass (`pio run` succeeds)
**DO NOT commit when**:
- Changes are untested on actual hardware
- Build fails or has warnings
- Experimenting or debugging in progress
- User hasn't explicitly requested commit
- Files excluded by `.gitignore` would be included — always run `git status` and cross-check against `.gitignore` before staging (e.g., `*.generated.h`, `.pio/`, `compile_commands.json`, `platformio.local.ini`)
**Rule**: **If uncertain, ASK before committing.**
---
## Generated Files and Build Artifacts
### Files Generated by Build Scripts
**NEVER manually edit these files** - they are regenerated automatically:
1. **HTML Headers** (generated by `scripts/build_html.py`):
- `src/network/html/*.generated.h`
- **Source**: HTML templates in `data/html/` directory
- **Triggered**: During PlatformIO `pre:` build step
- **To modify**: Edit source HTML in `data/html/`, not generated headers
2. **I18n Headers** (generated by `scripts/gen_i18n.py`):
- `lib/I18n/I18nKeys.h`, `lib/I18n/I18nStrings.h`, `lib/I18n/I18nStrings.cpp`
- **Source**: YAML translation files in `lib/I18n/translations/` (one per language)
- **To modify**: Edit source YAML files, then run `python scripts/gen_i18n.py lib/I18n/translations lib/I18n/`
- **Commit**: Source YAML files only. All three generated files (`I18nKeys.h`, `I18nStrings.h`, `I18nStrings.cpp`) are in `.gitignore` and regenerated at build time.
3. **Build Artifacts** (in `.gitignore`):
- `.pio/` - PlatformIO build output
- `build/` - Compiled binaries
- `*.generated.h` - Any auto-generated headers
- `compile_commands.json` - LSP/IDE metadata
### Modifying Generated Content Workflow
**To change HTML pages**:
1. Edit source: `data/html/<pagename>.html`
2. Build: `pio run` (auto-triggers `scripts/build_html.py`)
3. Generated headers update: `src/network/html/<pagename>Html.generated.h`
4. **Commit ONLY** source HTML, NOT generated `.generated.h` files
**To add/modify translations (i18n)**:
1. Edit or add YAML file: `lib/I18n/translations/<language>.yaml`
- Each file must contain: `_language_name`, `_language_code`, `_order`, and `STR_*` keys
- English (`english.yaml`) is the reference; missing keys in other languages fall back to English
2. Run generator: `python scripts/gen_i18n.py lib/I18n/translations lib/I18n/`
3. Generated files update: `I18nKeys.h`, `I18nStrings.h`, `I18nStrings.cpp`
4. **Commit** source YAML files only. All three generated files are in `.gitignore` and regenerated at build time.
**To use translated strings in code**:
```cpp
#include <I18n.h>
// Use tr() macro with StrId enum (defined in generated I18nKeys.h)
renderer.drawText(FONT_UI, x, y, tr(STR_LOADING), true);
```
**To add custom fonts**:
1. Place source fonts in `lib/EpdFont/fontsrc/` (gitignored)
2. Run conversion script (see `lib/EpdFont/README`)
3. Update global font objects in `src/main.cpp:40-115`
4. Add font ID constant to `src/fontIds.h`
---
## Local Development Configuration
### platformio.local.ini (Personal Overrides)
**Purpose**: Personal development settings that should NEVER be committed.
**Use Cases**:
- Serial port configuration (varies by machine)
- Debug flags for specific testing
- Local build optimizations
- Developer-specific paths
**Example** `platformio.local.ini`:
```ini
# platformio.local.ini (gitignored)
[env:default]
upload_port = COM7 # Windows: COMx, Linux: /dev/ttyUSBx
monitor_port = COM7
build_flags =
${base.build_flags}
-DMY_DEBUG_FLAG=1 # Personal debug flags
-DTEST_FEATURE_ENABLED=1
```
**Configuration Hierarchy**:
1. `platformio.ini` - **Committed**, shared project settings
2. `platformio.local.ini` - **Gitignored**, personal overrides
3. Local file extends/overrides base config
**Rules**:
- **NEVER commit** `platformio.local.ini`
- **NEVER put** personal info (serial ports, credentials) in main `platformio.ini`
- Use `${base.build_flags}` to extend (not replace) base flags
---
## Testing and Verification Workflow
### Testing Checklist
**AI agent scope** (what you CAN verify):
1. ✅ **Build**: `pio run -t clean && pio run` (0 errors/warnings)
2. ✅ **Quality**: `pio check` + `find src -name "*.cpp" -o -name "*.h" | xargs clang-format -i`
3. ✅ **Format**: Commit messages (`feat:`/`fix:`), no `.gitignore`-excluded files staged (e.g., `*.generated.h`, `.pio/`, `platformio.local.ini`)
4. ✅ **CI**: Fix GitHub Actions failures before review
5. ✅ **Code review**: Ensure orientation-aware logic is correct in all 4 modes by inspecting switch/case coverage
**Human tester scope** (flag these for the user):
6. 🔲 **Device**: Test on hardware
7. 🔲 **Orientations**: Verify all 4 modes (Portrait/Inverted/Landscape CW/CCW)
8. 🔲 **Heap**: `ESP.getFreeHeap()` > 50KB, no leaks
9. 🔲 **Cache**: If EPUB modified, delete `.crosspoint/` and verify re-parse
### CI/CD Pipeline Awareness
**GitHub Actions** run automatically on pull requests:
| Workflow | File | Purpose |
|----------|------|---------|
| Build Check | `.github/workflows/ci.yml` | Verifies code compiles |
| Format Check | `.github/workflows/pr-formatting-check.yml` | Validates clang-format |
| Release Build | `.github/workflows/release.yml` | Production releases |
| RC Build | `.github/workflows/release_candidate.yml` | Release candidates |
**Rules**:
- **Fix CI failures BEFORE** requesting review
- CI runs on: Push to PR, PR updates
- Format check fails → Run clang-format locally
- Build check fails → Fix compile errors
---
## Serial Monitoring and Live Debugging
### Serial Monitor Options
1. **Enhanced**: `python3 scripts/debugging_monitor.py` (color-coded, recommended)
2. **Standard**: `pio device monitor` (basic, no colors)
3. **VS Code**: Monitor (🔌) button (IDE-integrated)
### Live Debugging Patterns
**Heap**: `LOG_DBG("MEM", "Free: %d", ESP.getFreeHeap());` (every 5s in loop)
**Stack**: `uxTaskGetStackHighWaterMark(nullptr)` (< 512 bytes → increase stack)
**Flush**: `logSerial.flush();` (force output before crash)
**Port Detection**: Windows: `mode` | Linux: `ls /dev/ttyUSB* /dev/ttyACM*` or `dmesg | grep tty`
---
## Cache Management and Invalidation
### Cache Structure on SD Card
**Location**: `.crosspoint/` directory on SD card root
**Structure**: `.crosspoint/epub_<hash>/{book.bin, progress.bin, cover.bmp, sections/*.bin}`
**Hash**: `std::hash<std::string>{}(filepath)` → Moving/renaming file = new hash = lost progress
### Cache Invalidation Rules
**Cache is automatically invalidated when**:
1. **File format version changes** (see `docs/file-formats.md`)
- `book.bin` version number incremented
- `section.bin` version number incremented
2. **Render settings change**:
- Font family or size (`SETTINGS.fontFamily`, `SETTINGS.fontSize`)
- Line spacing (`SETTINGS.lineSpacing`)
- Paragraph spacing (`SETTINGS.extraParagraphSpacing`)
- Screen margins (`SETTINGS.screenMargin`)
3. **Viewport dimensions change**:
- Screen orientation change
- Display resolution change
4. **Book file modified**:
- Moved, renamed, or content changed (new hash)
**Manual Cache Clear** (safe operations):
```bash
# Delete ALL caches (forces full regeneration)
rm -rf /path/to/sd/.crosspoint/
# Delete specific book cache
rm -rf /path/to/sd/.crosspoint/epub_<hash>/
# Keep progress, delete only rendered sections
rm -rf /path/to/sd/.crosspoint/epub_<hash>/sections/
```
**When to Clear Cache**:
- EPUB parsing errors after code changes to `lib/Epub/`
- Corrupt rendering (missing text, wrong layout)
- Testing cache generation logic
- After modifying:
- `lib/Epub/Epub/Section.cpp`
- `lib/Epub/Epub/BookMetadataCache.cpp`
- Render settings in `CrossPointSettings`
### Cache File Format Versioning
**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)
**Version Increment Rules**:
1. **ALWAYS increment version** BEFORE changing binary structure
2. Version mismatch → Cache auto-invalidated and regenerated
3. Document format changes in `docs/file-formats.md`
**Example** (incrementing section format version):
```cpp
// lib/Epub/Epub/Section.cpp
static constexpr uint8_t SECTION_FILE_VERSION = 13; // Was 12, now 13
// Add new field to structure
struct PageLine {
// ... existing fields ...
uint16_t newField; // New field added
};
```
---
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.
Symlink
+1
View File
@@ -0,0 +1 @@
.skills/SKILL.md
+177 -97
View File
@@ -1,102 +1,169 @@
# CrossPoint Reader
Firmware for the **Xteink X4** e-paper display reader (unaffiliated with Xteink).
Built using **PlatformIO** and targeting the **ESP32-C3** microcontroller.
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.
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.
**Now running on:** ESP32C3-based Xteink [X4](https://www.xteink.com/products/xteink-x4) and [X3](https://www.xteink.com/products/xteink-x3).
![](./docs/images/cover.jpg)
![CrossPoint Reader running on Xteink device](./docs/images/cover.jpg)
## Motivation
## What can CrossPoint do?
E-paper devices are fantastic for reading, but most commercially available readers are closed systems with limited
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.
- **Reader engine**: EPUB 2/3 rendering with embedded-style option, image handling, hyphenation, kerning, chapter navigation, footnotes, go-to-percent, auto page turn, orientation control, focus reading, KOReader progress sync and more.
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**.
- **Various formats**: native handling for `.epub`, `.xtc/.xtch`, `.txt`, and `.bmp`.
This project is **not affiliated with Xteink**; it's built as a community project.
- **Screenshots.**
## Features & Usage
- **Custom fonts**: install your favorite fonts on the SD card.
- [x] EPUB parsing and rendering (EPUB 2 and EPUB 3)
- [ ] 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] Configurable font, layout, and display options
- [ ] User provided fonts
- [ ] Full UTF support
- [x] Screen rotation
- **Tilt page turn (X3 only)**.
Multi-language support: Read EPUBs in various languages, including English, Spanish, French, German, Italian, Portuguese, Russian, Ukrainian, Polish, Swedish, Norwegian, [and more](./USER_GUIDE.md#supported-languages).
- **Library workflow**: folder browser, hidden-file toggle, long-press delete, recent books, SD-cache management.
See [the user guide](./USER_GUIDE.md) for instructions on operating CrossPoint.
- **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 WiFi), both with QR helpers
- Calibre wireless connect flow
- OPDS browser with saved servers (up to 8), search, pagination, and direct download
- OTA update checks and installs from GitHub releases
For more details about the scope of the project, see the [SCOPE.md](SCOPE.md) document.
- **Customization**: multiple themes (Classic, Lyra, Lyra Extended, RoundedRaff), sleep screen modes, front/side button remapping, status bar controls, power-button behavior, refresh cadence, and more.
## Installing
- **Localization**: 22 UI languages and counting.
### Web (latest firmware)
### Coming soon:
1. Connect your Xteink X4 to your computer via USB-C and wake/unlock the device
2. Go to https://xteink.dve.al/ and click "Flash CrossPoint firmware"
- RTL support — Arabic, Hebrew, and Farsi.
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.
- Bookmarks.
### Web (specific firmware version)
- Dictionary lookup — inline word lookup without leaving the reader.
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
- More themes.
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.
- Much more! stay tuned.
---
## USB-locked devices (Xteink Unlocker)
Some Xteink units purchased from third-party stores (e.g. AliExpress) ship with USB flashing locked from the factory.
If your device is locked, you will need to use the **Xteink Unlocker** tool available at
https://crosspointreader.com/#unlock-tool before you can flash CrossPoint.
**You do not need this tool if you bought your device directly from xteink.com.** Those units are not locked.
**Not sure if your device is locked?** Power it on, connect the USB-C cable, and try flashing via the web flasher first (see
[Install firmware](#install-firmware) below). If the browser's serial device picker does not show your device, try a different
USB port or browser before assuming the device is locked. Only reach for the unlocker if the device still doesn't appear.
> ### ⚠️ WARNING: READ THIS BEFORE USING THE UNLOCKER ⚠️
>
> **The only officially supported firmwares in the unlock tool are CrossPoint and CrossInk.**
>
> Flashing any other firmware on a USB-locked device may **permanently brick the device** or leave it **permanently
> stuck on that firmware with no recovery path**. Once USB flashing is re-locked, your only way back is via OTA, and if
> the firmware you flashed doesn't support OTA, **there is no way out**.
>
> **The Papyrix fork has removed OTA update support from its code.** If you flash Papyrix onto a
> USB-locked unit, you will have **zero update or recovery path** and will be stuck on it forever. **Do not flash
> Papyrix (or any other unsupported firmware) on a locked device.**
## Install firmware
### Web installer (recommended)
1. Connect your device to your computer via USB-C and wake/unlock the device
2. Go to https://crosspointreader.com/#flash-tools, select device (X3 or X4), and choose an official CrossPoint release.
### Web installer (specific version)
1. Connect your device to your computer via USB-C and wake/unlock the device
2. Download a `firmware.bin` from [Releases](https://github.com/crosspoint-reader/crosspoint-reader/releases), local build, or continuous integration artifact.
3. Go to https://crosspointreader.com/#flash-tools, select device (X3 or X4), click "Custom .bin" and upload a `firmware.bin`.
### Revert to Official Firmware
To revert to the official firmware, you can also flash the latest official firmware using https://crosspointreader.com/#flash-tools.
### Command line
1. Install [`esptool`](https://github.com/espressif/esptool):
```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
---
## Documentation
- [User Guide](./USER_GUIDE.md)
- [Web server usage](./docs/webserver.md)
- [Web server endpoints](./docs/webserver-endpoints.md)
- [Project scope](./SCOPE.md)
- [Contributing docs](./docs/contributing/README.md)
---
## Development quick start
### Prerequisites
* **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
### Build / flash / monitor
Connect your Xteink X4 to your computer via USB-C and run the following command.
```sh
```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.
@@ -106,7 +173,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.
@@ -115,61 +184,72 @@ 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.
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_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_<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
│ └── sections/ # per-chapter layout cache
│ ├── 0.bin
│ ├── 1.bin
│ └── ...
└── 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.
Removing `/.crosspoint` clears all cached metadata and forces a full regeneration on next open. Note: the cache isn't cleared automatically when you delete a book, and moving a file to a new path resets its reading progress.
For more details on the internal file structures, see the [file formats document](./docs/file-formats.md).
---
## Contributing
Contributions are very welcome!
Contributions are welcome. If you're new to the codebase, start with the [contributing docs](./docs/contributing/README.md). For things to work on, check the [ideas discussion board](https://github.com/crosspoint-reader/crosspoint-reader/discussions/categories/ideas) — leave a comment before starting so we don't duplicate effort.
If you'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
Everyone here is a volunteer, so please be respectful and patient. For governance and community expectations, see [GOVERNANCE.md](./GOVERNANCE.md).
---
CrossPoint Reader is **not affiliated with Xteink or any manufacturer of the X4 hardware**.
## Community forks
Huge shoutout to [**diy-esp32-epub-reader** by atomic14](https://github.com/atomic14/diy-esp32-epub-reader), which was a project I took a lot of inspiration from as I
was making CrossPoint.
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.
- [crosspoint-reader (jpirnay)](https://github.com/jpirnay/crosspoint-reader) — Faster integration of functionality. Tracks upstream PRs and integrates the good ones ahead of the official merge.
- [crosspoint-reader-cjk](https://github.com/aBER0724/crosspoint-reader-cjk) — Purpose-built for Chinese, Japanese, and Korean reading.
- [inx](https://github.com/obijuankenobiii/inx) — Completely reimagines the user interface with tabbed navigation.
- ~~[PlusPoint](https://github.com/ngxson/pluspoint-reader) — custom JS apps support.~~ (Unmaintained)
- [crosspoint-reader-papers3](https://github.com/juicecultus/crosspoint-reader-papers3) — Crosspoint port for M5Stack Paper S3.
**Note:** Many of these features will make their way into CrossPoint over time. We maintain a slower pace to ensure rock-solid stability and squash bugs before they reach your device.
Want to build your own device? Be sure to check out the [de-link](https://github.com/iandchasse/de-link) project.
---
CrossPoint Reader is **not affiliated with Xteink or any device manufacturer**.
Huge shoutout to [diy-esp32-epub-reader](https://github.com/atomic14/diy-esp32-epub-reader), which inspired this project.
+12
View File
@@ -27,6 +27,12 @@ usability over "swiss-army-knife" functionality.
* **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.
* **Clock Display (device dependent):**
| Device | Scope |
| -- | -- |
| X3 | The X3 uses a dedicated DS3231 RTC, which maintains accurate time across sleep cycles and can be treated as a reliable wall clock. |
| X4 | The X4 relies on the ESP32-C3's internal RTC, which drifts significantly during deep sleep. NTP sync could correct this, with an appropriate user experience around connecting to the internet on wake or on demand. This causes some tension with the **Active Connectivity** section below, so please open a discussion about this UX if it's a feature you would find useful. |
### Out-of-Scope
@@ -39,6 +45,12 @@ usability over "swiss-army-knife" functionality.
* **Complex Annotation:** No typed out notes. These features are better suited for devices with better input
capabilities and more powerful chips.
### In-scope — Technically Unsupported
*These features align with CrossPoint's goals but are impractical on the current hardware or produce poor UX.*
* **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.
## 3. Idea Evaluation
While I appreciate the desire to add new and exciting features to CrossPoint Reader, CrossPoint Reader is designed to be
+275 -70
View File
@@ -10,12 +10,20 @@ Welcome to the **CrossPoint** firmware. This guide outlines the hardware control
- [First Launch](#first-launch)
- [3. Screens](#3-screens)
- [3.1 Home Screen](#31-home-screen)
- [3.2 Book Selection](#32-book-selection)
- [3.3 Reading Mode](#33-reading-mode)
- [3.4 File Upload Screen](#34-file-upload-screen)
- [3.4.1 Calibre Wireless Transfers](#341-calibre-wireless-transfers)
- [3.5 Settings](#35-settings)
- [3.6 Sleep Screen](#36-sleep-screen)
- [3.2 Reading Mode](#32-reading-mode)
- [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.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 OPDS Servers (Multiple Libraries)](#365-opds-servers-multiple-libraries)
- [3.6.6 Web Settings (WiFi + OPDS)](#366-web-settings-wifi--opds)
- [3.6.7 KOReader Sync Quick Setup](#367-koreader-sync-quick-setup)
- [3.7 Sleep Screen](#37-sleep-screen)
- [4. Reading Mode](#4-reading-mode)
- [Page Turning](#page-turning)
- [Chapter Navigation](#chapter-navigation)
@@ -28,7 +36,7 @@ Welcome to the **CrossPoint** firmware. This guide outlines the hardware control
## 1. Hardware Overview
The device utilises the standard buttons on the Xtink X4 (in the same layout as the manufacturer firmware, by default):
The device utilises the standard buttons on the Xteink X4 (in the same layout as the manufacturer firmware, by default):
### Button Layout
| Location | Buttons |
@@ -36,7 +44,12 @@ The device utilises the standard buttons on the Xtink X4 (in the same layout as
| **Bottom Edge** | **Back**, **Confirm**, **Left**, **Right** |
| **Right Side** | **Power**, **Volume Up**, **Volume Down**, **Reset** |
Button layout can be customized in **[Settings](#35-settings)**.
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**.
---
@@ -45,9 +58,9 @@ Button layout can be customized in **[Settings](#35-settings)**.
### Power On / Off
To turn the device on or off, **press and hold the Power button for approximately half a second**.
In **[Settings](#35-settings)** you can configure the power button to turn the device off with a short press instead of a long one.
In the **[Controls Settings](#363-controls)** you can configure the power button to turn the device off with a short press instead of a long one.
To reboot the device (for example if it's frozen, or after a firmware update), press and release the Reset button, and then quickly press and hold the Power button for a few seconds.
To reboot the device (for example after a firmware update or if it's frozen), press and release the Reset button, and then quickly press and hold the Power button for a few seconds.
### First Launch
@@ -62,29 +75,34 @@ Upon turning the device on for the first time, you will be placed on the **[Home
### 3.1 Home Screen
The Home Screen is the main entry point to the firmware. From here you can navigate to **[Reading Mode](#4-reading-mode)** with the most recently read book, **[Book Selection](#32-book-selection)**, **[Settings](#35-settings)**, or the **[File Upload](#34-file-upload-screen)** screen.
The Home screen is the main entry point to the firmware. From here you can navigate to **[Reading Mode](#4-reading-mode)** with the most recently read book, the **[Browse Files](#33-browse-files-screen)** screen, the **[Recent Books](#34-recent-books-screen)** screen, the **[File Transfer](#35-file-transfer-screen)** screen, or **[Settings](#36-settings)**.
### 3.2 Book Selection
The Book Selection acts as a folder and file browser.
* **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.
### 3.3 Reading Mode
### 3.2 Reading Mode
See [Reading Mode](#4-reading-mode) below for more information.
### 3.4 File Upload Screen
### 3.3 Browse Files Screen
The File Upload 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 Browse Files screen acts as a file and folder browser.
* **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.
### 3.4 Recent Books Screen
The Recent Books screen lists the most recently opened books in a chronological view, displaying title and author.
### 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.
See the [webserver docs](./docs/webserver.md) for more information on how to connect to the web server and upload files.
> [!TIP]
> Advanced users can also manage files programmatically or via the command line using `curl`. See the [webserver docs](./docs/webserver.md) for details.
### 3.4.1 Calibre Wireless Transfers
### 3.5.1 Calibre Wireless Transfers
CrossPoint supports sending books from Calibre using the CrossPoint Reader device plugin.
@@ -96,23 +114,26 @@ CrossPoint supports sending books from Calibre using the CrossPoint Reader devic
3. Make sure your computer is on the same WiFi network.
4. In Calibre, click "Send to device" to transfer books.
### 3.5 Settings
### 3.6 Settings
The Settings screen allows you to configure the device's behavior. There are a few settings you can adjust:
#### 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](#36-sleep-screen) below for more information
- "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, fallbacks to "Custom" behavior
- "Cover + Custom" - The book cover image, falls back to "Custom" behavior
- **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 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
- "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
- "Inverted" - The image will be inverted as in white & black and will be displayed without grayscale conversion
- **Status Bar**: Configure the status bar displayed while reading:
- "None" - No status bar
- "No Progress" - Show status bar without reading progress
@@ -120,61 +141,245 @@ The Settings screen allows you to configure the device's behavior. There are a f
- "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 pecentage display in the status bar; the battery icon will still be shown:
- "Never" - Always show battery percentage (default)
- **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
- **Extra Paragraph Spacing**: If enabled, vertical space will be added between paragraphs in the book. If disabled, paragraphs will not have vertical space between them, but will have first-line indentation.
- **Text Anti-Aliasing**: Whether to show smooth grey edges (anti-aliasing) on text in reading mode. Note this slows down page turns slightly.
- **Short Power Button Click**: Controls the effect of a short click of the power button:
- "Ignore" - Require a long press to turn off the device
- "Sleep" - A short press powers the device off
- "Page Turn" - A short press in reading mode turns to the next page; a long press turns the device off
- **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)**
- **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
#### 3.6.2 Reader
- **Reader Font Family**: Choose the font used for reading:
- "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
- **Front Button Layout**: Configure the order of the bottom edge buttons:
- Back, Confirm, Left, Right (default)
- Left, Right, Back, Confirm
- Left, Back, Confirm, Right
- Back, Confirm, Right, Left
- **Side Button Layout (reader)**: Swap the order of the up and down volume buttons from Previous/Next to Next/Previous. This change is only in effect when reading.
- **Long-press Chapter Skip**: Set whether long-pressing page turn buttons skip to the next/previous chapter.
- **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
- **Text Anti-Aliasing**: Whether to show smooth grey edges (anti-aliasing) on text in reading mode. Note this slows down page turns slightly.
#### 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.
- **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
- Swap the order of the up and down volume buttons from Previous/Next to Next/Previous. This change is only in effect when reading.
- **Reader Font Family**: Choose the font used for reading:
- "Bookerly" (default) - Amazon's reading 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", "Large", or "X Large".
- **Reader Line Spacing**: Adjust the spacing between lines; options are "Tight", "Normal", or "Wide".
- **Reader Screen Margin**: Controls the screen margins in reader 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".
- **Time to Sleep**: Set the duration of inactivity before the device automatically goes to sleep.
- **Refresh Frequency**: Set how often the screen does a full refresh while reading to reduce ghosting.
- **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
- **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.
- **Check for updates**: Check for firmware updates over WiFi.
- **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
### 3.6 Sleep Screen
#### 3.6.4 System
You can customize the sleep screen by placing custom images in specific locations on the SD card:
- **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.
- **Single Image:** Place a file named `sleep.bmp` in the root directory.
- **Multiple Images:** Create a `sleep` directory in the root of the SD card and place any number of `.bmp` images inside. If images are found in this directory, they will take priority over the `sleep.bmp` file, and one will be randomly selected each time the device sleeps.
- **WiFi Networks**: Connect to WiFi networks for file transfers and firmware updates.
- **KOReader Sync**: Options for setting up KOReader for syncing book progress.
- **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.
- **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 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 WiFi network management, see [Web Settings (WiFi + OPDS)](#366-web-settings-wifi--opds).
#### 3.6.6 Web Settings (WiFi + OPDS)
While in **File Transfer** mode, the web settings page includes management cards for both **WiFi Networks** and **OPDS Servers**.
1. On device: open **File Transfer** and connect to WiFi.
1. In a browser, open `http://<device-ip>/settings` or `http://crosspoint.local`.
1. In **WiFi Networks**, add, edit, or delete saved network entries (SSID + optional password).
1. 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 device-side WiFi 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`)
1. Register a user once (only if needed):
```bash
USERNAME="user"
PASSWORD="pass"
PASSWORD_MD5="$(printf '%s' "$PASSWORD" | openssl md5 | awk '{print $2}')"
curl -i "https://sync.koreader.rocks/users/create" \
-H "Accept: application/vnd.koreader.v1+json" \
-H "Content-Type: application/json" \
--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)
1. Start a sync server:
```bash
mkdir -p kosync-quickstart
cd kosync-quickstart
cat > compose.yaml <<'YAML'
services:
kosync:
image: koreader/kosync:latest
ports:
- "7200:7200"
- "17200:17200"
volumes:
- ./data/redis:/var/lib/redis
environment:
- ENABLE_USER_REGISTRATION=true
restart: unless-stopped
YAML
# Docker
docker compose up -d
# Podman (alternative)
podman compose up -d
```
> [!NOTE]
> You'll need to set the **Sleep Screen** setting to **Custom** in order to use these images.
> `ENABLE_USER_REGISTRATION=true` is convenient for first setup. After creating your users, set it to `false` (or remove it) to avoid unexpected registrations.
2. Verify the server:
```bash
curl -H "Accept: application/vnd.koreader.v1+json" "http://<server-ip>:17200/healthcheck"
# Expected: {"state":"OK"}
```
3. Register a user once.
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.
> Create unique sync-only credentials and do not reuse main account passwords.
> Prefer `https://<server-ip>:7200` whenever traffic leaves a fully trusted LAN or when using untrusted networks.
> Use `curl -k` only for self-signed certificate testing.
```bash
USERNAME="user"
PASSWORD="pass"
PASSWORD_MD5="$(printf '%s' "$PASSWORD" | openssl md5 | awk '{print $2}')"
curl -i "http://<server-ip>:17200/users/create" \
-H "Accept: application/vnd.koreader.v1+json" \
-H "Content-Type: application/json" \
--data "{\"username\":\"$USERNAME\",\"password\":\"$PASSWORD_MD5\"}"
```
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.
### 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. |
#### Cover settings
When using **Cover** or **Cover + Custom**, two additional settings apply:
- **Sleep Screen Cover Mode**: **Fit** (scale to fit, white borders) or **Crop** (scale and crop to fill the screen).
- **Sleep Screen Cover Filter**: **None** (grayscale), **Contrast** (black & white), or **Inverted** (inverted black & white).
#### Custom images
To use custom sleep images, set the sleep screen mode to **Custom** or **Cover + Custom**, then place images on the SD card:
- **Multiple Images (recommended):** Create a `.sleep` directory in the root of the SD card and place any number of `.bmp` images inside. One will be randomly selected each time the device sleeps. (A directory named `sleep` is also accepted as a fallback.)
- **Single Image:** Place a file named `sleep.bmp` in the root directory. This is used as a fallback if no valid images are found in the `.sleep`/`sleep` directory.
> [!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.
---
@@ -188,7 +393,7 @@ Once you have opened a book, the button layout changes to facilitate reading.
| **Previous Page** | Press **Left** _or_ **Volume Up** |
| **Next Page** | Press **Right** _or_ **Volume Down** |
The role of the volume (side) buttons can be swapped in **[Settings](#35-settings)**.
The role of the volume (side) buttons can be swapped in the **[Controls Settings](#363-controls)**.
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.
@@ -196,13 +401,13 @@ If the **Short Power Button Click** setting is set to "Page Turn", you can also
* **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 **[Settings](#35-settings)** to help avoid changing chapters by mistake.
This feature can be disabled in the **[Controls Settings](#363-controls)** to help avoid changing chapters by mistake.
### System Navigation
* **Return to Book Selection:** Press **Back** to close the book and return to the **[Book Selection](#32-book-selection)** screen.
* **Return to Home:** Press and **hold** the **Back** button to close the book and return to the **[Home](#31-home-screen)** screen.
* **Chapter Menu:** Press **Confirm** to open the **[Table of Contents/Chapter Selection](#5-chapter-selection-screen)**.
* **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.
### Supported Languages
+30 -8
View File
@@ -1,17 +1,34 @@
#!/usr/bin/env bash
# Check if clang-format is availible
command -v clang-format >/dev/null 2>&1 || {
printf "'clang-format' not found in current environment\n"
printf "install 'clang', 'clang-tools', or 'clang-format' depending on your distro/os and tooling requirements\n"
exit 1
}
# Check if clang-format is available and pick the preferred binary.
if command -v clang-format-21 >/dev/null 2>&1; then
CLANG_FORMAT_BIN="clang-format-21"
elif command -v clang-format >/dev/null 2>&1; then
CLANG_FORMAT_BIN="clang-format"
else
printf "'clang-format' not found in current environment\n"
printf "Install clang-format-21 (recommended), clang, clang-tools, or clang-format depending on your distro/os and tooling requirements\n"
exit 1
fi
set -euo pipefail
GIT_LS_FILES_FLAGS=""
if [[ "$1" == "-g" ]]; then
# -g scopes formatting to tracked files currently modified in git status.
if [[ "${1:-}" == "-g" ]]; then
GIT_LS_FILES_FLAGS="--modified"
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."
echo "Install clang-format-21 and rerun ./bin/clang-format-fix"
exit 1
fi
# --- Main Logic ---
@@ -22,9 +39,14 @@ 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/' \
| xargs -r clang-format -style=file -i
| xargs -r "${CLANG_FORMAT_BIN}" -style=file -i
# Restore strict pipeline failure handling for the rest of the script.
set -o pipefail
+155
View File
@@ -0,0 +1,155 @@
<#
.SYNOPSIS
Runs clang-format -i on project *.cpp and *.h files.
.DESCRIPTION
Formats all C/C++ source and header files in the repository, excluding
generated, vendored, and build directories (open-x4-sdk, builtinFonts,
hyphenation tries, uzlib, .pio, *.generated.h).
The clang-format binary path is resolved once and cached in
bin/clang-format-fix.local. On first run it checks a default path,
then PATH, then common install locations. Edit the .local file to
override manually.
.PARAMETER g
Format only git-modified files (git diff --name-only HEAD) instead of
the full tree.
.PARAMETER h
Show this help text.
.EXAMPLE
.\clang-format-fix.ps1
Format all files.
.EXAMPLE
.\clang-format-fix.ps1 -g
Format only git-modified files.
#>
param(
[switch]$g,
[switch]$h
)
if ($h) {
Get-Help $PSCommandPath -Detailed
return
}
$repoRoot = (Resolve-Path "$PSScriptRoot\..").Path
$configFile = Join-Path $PSScriptRoot 'clang-format-fix.local'
$defaultPath = 'C:\Program Files\LLVM\bin\clang-format.exe'
$candidatePaths = @(
'C:\Program Files\LLVM\bin\clang-format.exe'
'C:\Program Files (x86)\LLVM\bin\clang-format.exe'
'C:\msys64\ucrt64\bin\clang-format.exe'
'C:\msys64\mingw64\bin\clang-format.exe'
"$env:LOCALAPPDATA\LLVM\bin\clang-format.exe"
)
function Find-ClangFormat {
# Try PATH first
$inPath = Get-Command clang-format -ErrorAction SilentlyContinue
if ($inPath) { return $inPath.Source }
# Try candidate paths
foreach ($p in $candidatePaths) {
if (Test-Path $p) { return $p }
}
return $null
}
function Resolve-ClangFormat {
# 1. Read from config if present
if (Test-Path $configFile) {
$saved = (Get-Content $configFile -Raw).Trim()
if ($saved -and (Test-Path $saved)) { return $saved }
Write-Host "Configured path no longer valid: $saved"
}
# 2. Check default
if (Test-Path $defaultPath) {
$defaultPath | Set-Content $configFile
Write-Host "Saved clang-format path to $configFile"
return $defaultPath
}
# 3. Search PATH and candidate locations
$found = Find-ClangFormat
if ($found) {
$found | Set-Content $configFile
Write-Host "Found clang-format at $found - saved to $configFile"
return $found
}
Write-Error "clang-format not found. Install LLVM or add clang-format to PATH."
exit 1
}
$clangFormat = Resolve-ClangFormat
$exclude = @(
'open-x4-sdk'
'lib\EpdFont\builtinFonts'
'lib\Epub\Epub\hyphenation\generated'
'lib\uzlib'
'.pio'
'.venv'
)
function Test-Excluded($fullPath) {
foreach ($ex in $exclude) {
if ($fullPath -like "*\$ex\*") { return $true }
}
if ($fullPath -like '*.generated.h') { return $true }
return $false
}
if ($g) {
# Only git-modified *.cpp / *.h files
# Covers both staged and unstaged changes
$files = @(git -C $repoRoot diff --name-only HEAD) +
@(git -C $repoRoot diff --name-only --cached) |
Sort-Object -Unique |
Where-Object { $_ -match '\.(cpp|h)$' } |
ForEach-Object { Get-Item (Join-Path $repoRoot $_) -ErrorAction SilentlyContinue } |
Where-Object { $_ -and -not (Test-Excluded $_.FullName) }
} else {
$files = Get-ChildItem -Path $repoRoot -Recurse -Include *.cpp, *.h -File |
Where-Object { -not (Test-Excluded $_.FullName) }
}
$files = @($files)
if ($files.Count -eq 0) {
Write-Host 'No files to format.'
return
}
Write-Host "Formatting $($files.Count) files..."
$i = 0
$changed = 0
$failures = 0
foreach ($f in $files) {
$i++
$rel = $f.FullName.Substring($repoRoot.Length + 1)
$hashBefore = (Get-FileHash $f.FullName -Algorithm MD5).Hash
& $clangFormat -i $f.FullName
if ($LASTEXITCODE -ne 0) {
$failures++
Write-Host " [$i/$($files.Count)] $rel (FAILED, exit code $LASTEXITCODE)"
continue
}
$hashAfter = (Get-FileHash $f.FullName -Algorithm MD5).Hash
if ($hashBefore -ne $hashAfter) {
$changed++
Write-Host " [$i/$($files.Count)] $rel (changed)"
} else {
Write-Host " [$i/$($files.Count)] $rel"
}
}
Write-Host "Done. $changed/$($files.Count) files changed, $failures failed."
if ($failures -gt 0) { exit 1 }
+472
View File
@@ -0,0 +1,472 @@
# Activity & ActivityManager Migration Guide
This document explains the refactoring from the original per-activity render task model to the centralized `ActivityManager` introduced in [PR #1016](https://github.com/crosspoint-reader/crosspoint-reader/pull/1016). It covers the architectural differences, what changed for activity authors, and the FreeRTOS task and locking model that underpins the system.
## Overview of Changes
| Aspect | Old Model | New Model |
|--------|-----------|-----------|
| Render task | One per activity (8KB stack each) | Single shared task in `ActivityManager` |
| Render mutex | Per-activity `renderingMutex` | Single global mutex in `ActivityManager` |
| `RenderLock` | Inner class of `Activity` | Standalone class, acquires global mutex |
| Subactivities | `ActivityWithSubactivity` base class | Activity stack managed by `ActivityManager` |
| Navigation | Free functions in `main.cpp` | `activityManager.goHome()`, `goToReader()`, etc. |
| Subactivity results | Callback lambdas stored in parent | `startActivityForResult()` / `setResult()` / `finish()` |
| `requestUpdate()` | Notifies activity's own render task | Delegates to `ActivityManager` (immediate or deferred) |
## Architecture
### Old Model: Per-Activity Render Tasks
Each activity created its own FreeRTOS render task on entry and destroyed it on exit:
```text
┌─────────────────────────────────────────────────────────┐
│ Main Task (Arduino loop) │
│ ┌───────────────────────────────────────────────────┐ │
│ │ currentActivity->loop() │ │
│ │ ├── handle input │ │
│ │ ├── update state (under RenderLock) │ │
│ │ └── requestUpdate() ──notify──► Render Task │ │
│ │ (per-activity)│ │
│ │ 8KB stack │ │
│ │ owns mutex │ │
│ └───────────────────────────────────────────────────┘ │
│ │
│ ActivityWithSubactivity: │
│ ┌──────────────┐ ┌──────────────┐ │
│ │ Parent │────►│ SubActivity │ │
│ │ (has render │ │ (has own │ │
│ │ task) │ │ render task) │ │
│ └──────────────┘ └──────────────┘ │
└─────────────────────────────────────────────────────────┘
```
Problems with this approach:
- **8KB per render task**: Each activity allocated an 8KB FreeRTOS stack for its render task, even though only one renders at a time
- **Dangerous deletion patterns**: `exitActivity()` + `enterNewActivity()` in callbacks led to `delete this` situations where the caller was destroyed while its code was still on the stack
- **Subactivity coupling**: Parents stored callbacks to child results, creating tight coupling and lifetime hazards
### New Model: Centralized ActivityManager
A single `ActivityManager` owns the render task and manages an activity stack:
```text
┌──────────────────────────────────────────────────────────┐
│ Main Task (Arduino loop) │
│ │
│ activityManager.loop() │
│ │ │
│ ├── currentActivity->loop() │
│ │ ├── handle input │
│ │ ├── update state (under RenderLock) │
│ │ └── requestUpdate() │
│ │ │
│ ├── process pending actions (Push / Pop / Replace) │
│ │ │
│ └── if requestedUpdate: ──notify──► Render Task │
│ (single, shared) │
│ 8KB stack │
│ global mutex │
│ │
│ Activity Stack: │
│ ┌──────────┬──────────┬──────────┐ ┌──────────┐ │
│ │ Home │ Settings │ Wifi │ │ Keyboard │ │
│ │ (stack) │ (stack) │ (stack) │ │ (current)│ │
│ └──────────┴──────────┴──────────┘ └──────────┘ │
│ stackActivities[] currentActivity │
└──────────────────────────────────────────────────────────┘
```
## Migration Checklist
### 1. Change Base Class
If your activity extended `ActivityWithSubactivity`, change it to extend `Activity`:
```cpp
// BEFORE
class MyActivity final : public ActivityWithSubactivity {
MyActivity(GfxRenderer& r, MappedInputManager& m, std::function<void()> goBack)
: ActivityWithSubactivity("MyActivity", r, m), goBack(goBack) {}
};
// AFTER
class MyActivity final : public Activity {
MyActivity(GfxRenderer& r, MappedInputManager& m)
: Activity("MyActivity", r, m) {}
};
```
Note that navigation callbacks like `goBack` are no longer stored — use `finish()` or `activityManager.goHome()` instead.
### 2. Replace Navigation Functions
The free functions `exitActivity()` / `enterNewActivity()` in `main.cpp` are gone. Use `ActivityManager` methods:
```cpp
// BEFORE (in main.cpp or via stored callbacks)
exitActivity();
enterNewActivity(new SettingsActivity(renderer, mappedInput, onGoHome));
// AFTER (from any Activity method)
activityManager.goToSettings();
// or for arbitrary navigation:
activityManager.replaceActivity(std::make_unique<MyActivity>(renderer, mappedInput));
```
`replaceActivity()` destroys the current activity and clears the stack. Use it for top-level navigation (home, reader, settings, etc.).
### 3. Replace Subactivity Pattern
The `enterNewActivity()` / `exitActivity()` subactivity pattern is replaced by a stack with typed results:
```cpp
// BEFORE
void MyActivity::launchWifi() {
enterNewActivity(new WifiSelectionActivity(renderer, mappedInput,
[this](bool connected) { onWifiDone(connected); }));
}
// Child calls: onComplete(true); // triggers callback, which may call exitActivity()
// AFTER
void MyActivity::launchWifi() {
startActivityForResult(
std::make_unique<WifiSelectionActivity>(renderer, mappedInput),
[this](const ActivityResult& result) {
if (result.isCancelled) return;
auto& wifi = std::get<WifiResult>(result.data);
onWifiDone(wifi.connected);
});
}
// Child calls:
// setResult(WifiResult{.connected = true, .ssid = ssid});
// finish();
```
Key differences:
- **`startActivityForResult()`** pushes the current activity onto the stack and launches the child
- **`setResult()`** stores a typed result on the child activity
- **`finish()`** signals the manager to pop the child, call the result handler, and resume the parent
- The parent is never deleted during this process — it's safely stored on the stack
### 4. Update `render()` Signature
The `RenderLock` type changed from `Activity::RenderLock` (inner class) to standalone `RenderLock`:
```cpp
// BEFORE
void render(Activity::RenderLock&&) override;
// AFTER
void render(RenderLock&&) override;
```
Include `RenderLock.h` if not transitively included via `Activity.h`.
### 5. Update `onEnter()` / `onExit()`
Activities no longer create or destroy render tasks:
```cpp
// BEFORE
void MyActivity::onEnter() {
Activity::onEnter(); // created render task + logged
// ... allocate resources
requestUpdate();
}
void MyActivity::onExit() {
// ... free resources
Activity::onExit(); // acquired RenderLock, deleted render task
}
// AFTER
void MyActivity::onEnter() {
Activity::onEnter(); // just logs
// ... allocate resources
requestUpdate();
}
void MyActivity::onExit() {
// ... free resources
Activity::onExit(); // just logs
}
```
The render task lifecycle is handled entirely by `ActivityManager::begin()`.
### 6. Update `requestUpdate()` Calls
The signature changed to accept an `immediate` flag:
```cpp
// BEFORE
void requestUpdate(); // always immediate notification to per-activity render task
// AFTER
void requestUpdate(bool immediate = false);
// immediate=false (default): deferred until end of current loop iteration
// immediate=true: sends notification to render task right away
```
**When to use `immediate`**: Almost never. Deferred updates are batched — if `loop()` triggers multiple state changes that each call `requestUpdate()`, only one render happens. Use `immediate` only when you need the render to start before the current function returns (e.g., before a blocking network call).
**`requestUpdateAndWait()`**: Blocks the calling task until the render completes. Use sparingly — it's designed for cases where you need the screen to reflect new state before proceeding (e.g., showing "Checking for update..." before calling a network API).
### 7. Remove Stored Navigation Callbacks
Old activities often stored `std::function` callbacks for navigation:
```cpp
// BEFORE
class SettingsActivity : public ActivityWithSubactivity {
const std::function<void()> goBack; // stored callback
const std::function<void()> goHome; // stored callback
public:
SettingsActivity(GfxRenderer& r, MappedInputManager& m,
std::function<void()> goBack, std::function<void()> goHome)
: ActivityWithSubactivity("Settings", r, m), goBack(goBack), goHome(goHome) {}
};
// AFTER
class SettingsActivity : public Activity {
public:
SettingsActivity(GfxRenderer& r, MappedInputManager& m)
: Activity("Settings", r, m) {}
// Use finish() to go back, activityManager.goHome() to go home
};
```
This removes `std::function` overhead (~2-4KB per unique signature) and eliminates lifetime risks from captured `this` pointers.
## Technical Details
### FreeRTOS Task Model
The firmware runs on an ESP32-C3, a single-core RISC-V microcontroller. FreeRTOS provides cooperative and preemptive multitasking on this single core — only one task executes at any moment, and the scheduler switches between tasks at yield points (blocking calls, `vTaskDelay`, `taskYIELD`) or when a tick interrupt promotes a higher-priority task.
There are two tasks relevant to the activity system:
```text
┌──────────────────────┐ ┌──────────────────────────┐
│ Main Task │ │ Render Task │
│ (Arduino loop) │ │ (ActivityManager-owned) │
│ Priority: 1 │ │ Priority: 1 │
│ │ │ │
│ Runs: │ │ Runs: │
│ - gpio.update() │ │ - ulTaskNotifyTake() │
│ - activity->loop() │ │ (blocks until notified)│
│ - pending actions │ │ - RenderLock (mutex) │
│ - sleep/power mgmt │ │ - activity->render() │
│ - requestUpdate →────┼─────┼─► xTaskNotify() │
│ (end of loop) │ │ │
└──────────────────────┘ └──────────────────────────┘
```
Both tasks run at priority 1. Since the ESP32-C3 is single-core, they alternate execution: the main task runs `loop()`, then at the end of the loop iteration, notifies the render task if an update was requested. The render task wakes, acquires the mutex, calls `render()`, releases the mutex, and blocks again.
Do not use `xTaskCreate` inside activities. If you have a use case that seems to require a background task, open a discussion to propose a lifecycle-aware `Worker` abstraction first.
### The Render Mutex and RenderLock
A single FreeRTOS mutex (`renderingMutex`) protects shared state between `loop()` and `render()`. Since these run on different tasks, any state read by `render()` and written by `loop()` must be guarded.
`RenderLock` is an RAII wrapper:
```cpp
// Standalone class (not tied to any specific activity)
class RenderLock {
bool isLocked = false;
public:
explicit RenderLock(); // acquires activityManager.renderingMutex
explicit RenderLock(Activity&); // same — Activity& param kept for compatibility
~RenderLock(); // releases mutex if still held
void unlock(); // early release
};
```
**Usage patterns:**
```cpp
// In loop(): protect state mutations that render() reads
void MyActivity::loop() {
if (somethingChanged) {
RenderLock lock;
state = newState; // safe — render() can't run while lock is held
}
requestUpdate(); // trigger render after lock is released
}
// In render(): lock is passed in, held for duration of render
void MyActivity::render(RenderLock&&) {
// Lock is held — safe to read shared state
renderer.clearScreen();
renderer.drawText(..., stateString, ...);
renderer.displayBuffer();
// Lock released when RenderLock destructor runs
}
```
**Critical rule**: Never call `requestUpdateAndWait()` while holding a `RenderLock`. The render task needs the mutex to call `render()`, so holding it while waiting for the render to complete is a deadlock:
```text
Main Task Render Task
────────── ───────────
RenderLock lock; (blocked on mutex)
requestUpdateAndWait();
→ notify render task
→ block waiting for
render to complete → wakes up
→ tries to acquire mutex
→ DEADLOCK: main holds mutex,
waits for render; render
waits for mutex
```
### requestUpdate() vs requestUpdateAndWait()
```text
requestUpdate(false) requestUpdate(true)
───────────────── ─────────────────
Sets flag only. Notifies render task
Render happens after immediately.
loop() returns and Render may start
ActivityManager checks before the calling
the flag. function returns.
(Does NOT wait for
render to complete.)
requestUpdateAndWait()
──────────────────────
Notifies render task AND
blocks calling task until
render is done. Uses
FreeRTOS direct-to-task
notification on the
caller's task handle.
```
`requestUpdateAndWait()` flow in detail:
```text
Calling Task Render Task
──────────── ───────────
requestUpdateAndWait()
├─ assert: not render task
├─ assert: not holding RenderLock
├─ store waitingTaskHandle
├─ xTaskNotify(renderTask) → wakes render task
└─ ulTaskNotifyTake() ─┐
(blocked) │ RenderLock lock;
│ activity->render();
│ // render complete
│ taskENTER_CRITICAL
│ waiter = waitingTaskHandle
│ waitingTaskHandle = nullptr
│ taskEXIT_CRITICAL
│ xTaskNotify(waiter) ───┐
│ │
┌──────────────────────┘ │
│ (woken by notification) ◄────────────────────────┘
└─ return
```
### Activity Lifecycle Under ActivityManager
```text
activityManager.replaceActivity(make_unique<MyActivity>(...))
╔═══════════════════════════════════════════════════╗
║ pendingAction = Replace ║
║ pendingActivity = MyActivity ║
╚═══════════════════════════════════════════════════╝
▼ (next loop iteration)
ActivityManager::loop()
├── currentActivity->loop() // old activity's last loop
├── process pending action:
│ ├── RenderLock lock;
│ ├── oldActivity->onExit() // cleanup under lock
│ ├── delete oldActivity
│ ├── clear stack
│ ├── currentActivity = MyActivity
│ ├── lock.unlock()
│ └── MyActivity->onEnter() // init new activity
└── if requestedUpdate:
└── notify render task
```
For push/pop (subactivity) navigation:
```text
Parent calls: startActivityForResult(make_unique<Child>(...), handler)
╔══════════════════════════════════════╗
║ pendingAction = Push ║
║ pendingActivity = Child ║
║ parent->resultHandler = handler ║
╚══════════════════════════════════════╝
▼ (next loop iteration)
├── Parent moved to stackActivities[]
├── currentActivity = Child
└── Child->onEnter()
... child runs ...
Child calls: setResult(MyResult{...}); finish();
╔══════════════════════════════════════╗
║ pendingAction = Pop ║
║ child->result = MyResult{...} ║
╚══════════════════════════════════════╝
▼ (next loop iteration)
├── result = child->result
├── Child->onExit(); delete Child
├── currentActivity = Parent (popped from stack)
├── Parent->resultHandler(result)
└── requestUpdate() // automatic re-render for parent
```
### Common Pitfalls
**Calling `finish()` and continuing to access `this`**: `finish()` sets `pendingAction = Pop` but does not immediately destroy the activity. The activity is destroyed on the next `ActivityManager::loop()` iteration. It's safe to access member variables after `finish()` within the same function, but don't rely on the activity surviving past the current `loop()` call.
**Modifying shared state without `RenderLock`**: If `render()` reads a variable and `loop()` writes it, the write must be under a `RenderLock`. Without it, `render()` could see a half-written value (e.g., a partially updated string or struct).
**Creating background tasks that outlive the activity**: Any FreeRTOS task created in `onEnter()` must be deleted in `onExit()` before the activity is destroyed. The `ActivityManager` does not track or clean up background tasks.
**Holding `RenderLock` across blocking calls**: The render task is blocked on the mutex while you hold the lock. Keep critical sections short — acquire, mutate state, release, then do blocking work.
```cpp
// WRONG — blocks render for the entire network call
void MyActivity::doNetworkStuff() {
RenderLock lock;
state = LOADING;
auto result = http.get(url); // blocks for seconds with lock held
state = DONE;
}
// CORRECT — release lock before blocking
void MyActivity::doNetworkStuff() {
{
RenderLock lock;
state = LOADING;
}
requestUpdate(true); // render "Loading..." immediately, before we block
auto result = http.get(url); // lock is not held
{
RenderLock lock;
state = DONE;
}
requestUpdate();
}
```
+11
View File
@@ -0,0 +1,11 @@
# Contributing Docs
This section is a lightweight contributor guide for CrossPoint Reader.
It is written for software developers who may be new to embedded development.
- [Getting Started](./getting-started.md)
- [Architecture Overview](./architecture.md)
- [Development Workflow](./development-workflow.md)
- [Testing and Debugging](./testing-debugging.md)
If you are new, start with [Getting Started](./getting-started.md).
+199
View File
@@ -0,0 +1,199 @@
# Architecture Overview
CrossPoint is firmware for the Xteink X4 (unaffiliated with Xteink), built with PlatformIO targeting the ESP32-C3 microcontroller.
At a high level, it is firmware that uses an activity-driven application architecture loop with persistent settings/state, SD-card-first caching, and a rendering pipeline optimized for e-ink constraints.
## System at a glance
```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]
```
## Runtime lifecycle
Primary entry point is `src/main.cpp`.
```mermaid
flowchart TD
A[Boot] --> B[Init GPIO and optional serial]
B --> C[Init SD storage]
C --> D[Load settings and app state]
D --> E[Init display and fonts]
E --> F{Resume reader?}
F -->|No| G[Enter Home activity]
F -->|Yes| H[Enter Reader activity]
G --> I[Main loop]
H --> I
I --> J[Poll input and run current activity]
J --> K{Sleep condition met?}
K -->|No| I
K -->|Yes| L[Persist state and enter deep sleep]
```
In each loop iteration, the firmware updates input, runs the active activity, handles auto-sleep/power behavior, and applies a short delay policy to balance responsiveness and power.
## Activity model
Activities are screen-level controllers deriving from `src/activities/Activity.h`.
Some flows use `src/activities/ActivityWithSubactivity.h` to host nested activities.
- `onEnter()` and `onExit()` manage setup/teardown
- `loop()` handles per-frame behavior
- `skipLoopDelay()` and `preventAutoSleep()` are used by long-running flows (for example web server mode)
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/boot_sleep/`: boot and sleep transitions
## Reader and content pipeline
Reader orchestration starts in `src/activities/reader/ReaderActivity.h` and dispatches to format-specific readers.
EPUB processing is implemented in `lib/Epub/`.
```mermaid
flowchart LR
A[Select book] --> B[ReaderActivity]
B --> C{Format}
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]
```
Why caching matters:
- RAM is limited on ESP32-C3, so expensive parsed/layout data is persisted to SD
- repeat opens/page navigation can reuse cached data instead of full reparsing
## Reader internals call graph
This diagram zooms into the EPUB path to show the main control and data flow from activity entry to on-screen draw.
```mermaid
flowchart TD
A[ReaderActivity onEnter] --> B{File type}
B -->|EPUB| C[Create Epub object]
B -->|XTC/TXT| Z[Use format-specific reader]
C --> D[Epub load]
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]
H --> I[EpubReaderActivity]
I --> J{Section cache exists for current settings?}
J -->|Yes| K[Read section bin from SD cache]
J -->|No| L[Parse chapter HTML and layout text]
L --> M[Apply typography settings and hyphenation]
M --> N[Write section cache bin]
K --> O[Build page model]
N --> O
O --> P[GfxRenderer draw calls]
P --> Q[HAL display framebuffer update]
Q --> R[E-ink refresh policy]
S[SETTINGS singleton] -. influences .-> J
S -. influences .-> M
T[APP_STATE singleton] -. persists .-> U[Reading progress and resume context]
U -. used by .-> I
```
Notes:
- "section cache exists" depends on cache-busting parameters such as font and layout-related 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
## State and persistence
Two singletons are central:
- `src/CrossPointSettings.h` (`SETTINGS`): user preferences and behavior flags
- `src/CrossPointState.h` (`APP_STATE`): runtime/session state such as current book and sleep context
Typical persisted areas on SD:
```text
/.crosspoint/
epub_<hash>/
book.bin
progress.bin
cover.bmp
sections/*.bin
settings.bin
state.bin
```
For binary cache formats, see `docs/file-formats.md`.
## Networking architecture
Network file transfer is controlled by `src/activities/network/CrossPointWebServerActivity.h` and served by `src/network/CrossPointWebServer.h`.
Modes:
- STA: join existing WiFi network
- AP: create hotspot
Server behavior:
- HTTP server on port 80
- WebSocket upload server on port 81
- file operations backed by SD storage
- activity requests faster loop responsiveness while server is running
Endpoint reference: `docs/webserver-endpoints.md`.
## Build-time generated assets
Some sources are generated and should not be edited manually.
- `scripts/build_html.py` generates `src/network/html/*.generated.h` from HTML files
- `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.
## Key directories
- `src/`: app orchestration, settings/state, and activity implementations
- `src/network/`: web server and OTA/update networking
- `src/components/`: theming and shared UI components
- `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)
- `docs/`: user and technical documentation
## Embedded constraints that shape design
- constrained RAM drives SD-first caching and careful allocations
- e-ink refresh cost drives render/update batching choices
- main loop responsiveness matters for input, power handling, and watchdog safety
- background/network flows must cooperate with sleep and loop timing logic
## Scope guardrails
Before implementing larger ideas, check:
- [SCOPE.md](../../SCOPE.md)
- [GOVERNANCE.md](../../GOVERNANCE.md)
+44
View File
@@ -0,0 +1,44 @@
# Development Workflow
This page defines the expected local workflow before opening a pull request.
## 1) Fork and create a focused branch
- 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
## 2) Implement with scope in mind
- Confirm your idea is in project scope: [SCOPE.md](../../SCOPE.md)
- Prefer incremental changes over broad refactors
## 3) Run local checks
```sh
./bin/clang-format-fix
pio check --fail-on-defect low --fail-on-defect medium --fail-on-defect high
pio run
```
CI enforces formatting, static analysis, and build checks.
Use clang-format 21+ locally to match CI.
If `clang-format` is missing or too old locally, see [Getting Started](./getting-started.md).
## 4) Open the PR
- Use a semantic title (example: `fix: avoid crash when opening malformed epub`)
- Fill out `.github/PULL_REQUEST_TEMPLATE.md`
- Describe the problem, approach, and any tradeoffs
- Include reproduction and verification steps for bug fixes
## 5) Review etiquette
- Be explicit and concise in responses
- Keep discussions technical and respectful
- Assume good intent and focus on code-level feedback
For community expectations, see [GOVERNANCE.md](../../GOVERNANCE.md).
+87
View File
@@ -0,0 +1,87 @@
# Getting Started
This guide helps you build and run CrossPoint locally.
## Prerequisites
- PlatformIO Core (`pio`) or VS Code + PlatformIO IDE
- Python 3.8+
- `clang-format` 21+ in your `PATH` (CI uses clang-format 21)
- USB-C cable
- Xteink X4 device for hardware testing
If `./bin/clang-format-fix` fails with either of these errors, install clang-format 21:
- `clang-format: No such file or directory`
- `.clang-format: error: unknown key 'AlignFunctionDeclarations'`
Examples:
```sh
# Debian/Ubuntu (try this first)
sudo apt-get update && sudo apt-get install -y clang-format-21
# If the package is unavailable, add LLVM apt repo and retry
wget https://apt.llvm.org/llvm.sh
chmod +x llvm.sh
sudo ./llvm.sh 21
sudo apt-get update
sudo apt-get install -y clang-format-21
# macOS (Homebrew)
brew install clang-format
```
Then verify:
```sh
clang-format-21 --version
```
The reported major version must be 21 or newer.
## Clone and initialize
```sh
git clone --recursive https://github.com/crosspoint-reader/crosspoint-reader
cd crosspoint-reader
```
If you already cloned without submodules:
```sh
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
pio run
```
## Flash
```sh
pio run --target upload
```
## First checks before opening a PR
```sh
./bin/clang-format-fix
pio check --fail-on-defect low --fail-on-defect medium --fail-on-defect high
pio run
```
## What to read next
- [Architecture Overview](./architecture.md)
- [Development Workflow](./development-workflow.md)
- [Testing and Debugging](./testing-debugging.md)
+48
View File
@@ -0,0 +1,48 @@
# Testing and Debugging
CrossPoint runs on real hardware, so debugging usually combines local build checks and on-device logs.
## Local checks
Make sure `clang-format` 21+ is installed and available in `PATH` before running the formatting step.
If needed, see [Getting Started](./getting-started.md).
```sh
./bin/clang-format-fix
pio check --fail-on-defect low --fail-on-defect medium --fail-on-defect high
pio run
```
## Flash and monitor
Flash firmware:
```sh
pio run --target upload
```
Open serial monitor:
```sh
pio device monitor
```
Optional enhanced monitor:
```sh
python3 -m pip install pyserial colorama matplotlib
python3 scripts/debugging_monitor.py
```
## Useful bug report contents
- Firmware version and build environment
- Exact steps to reproduce
- Expected vs actual behavior
- Serial logs from boot through failure
- Whether issue reproduces after clearing `.crosspoint/` cache on SD card
## Common troubleshooting references
- [User Guide troubleshooting section](../../USER_GUIDE.md#7-troubleshooting-issues--escaping-bootloop)
- [Webserver troubleshooting](../troubleshooting.md)
+33
View File
@@ -0,0 +1,33 @@
# Focus Reading
Focus Reading is a reading aid that bolds the first portion of each word, guiding your eyes to natural fixation points and helping you read faster with less effort. Some readers — particularly those with ADHD — find it helps them stay engaged with the text and reduces mind-wandering. It is inspired by the Bionic Reading technique.
<img src="./images/focus-reading/focus-reading.jpg" height="500" alt="Comparison of the same page with and without Focus Reading enabled" />
*Left: Focus Reading off. Right: Focus Reading on. Both using Literata.*
## Enabling Focus Reading
1. Open **Settings > Reader**
2. Toggle **Focus Reading** on
Toggling the setting will trigger a re-index of your current book, the same as when changing font settings. Once indexing is complete, page turns proceed as normal. No changes are made to your EPUB files.
## Examples
<img src="./images/focus-reading/focus-reading-notoserif.jpg" height="500" alt="Focus Reading with Noto Serif font" />
*Focus Reading with Noto Serif font*
<img src="./images/focus-reading/focus-reading-merriweather.jpg" height="500" alt="Focus Reading with Merriweather font" />
*Focus Reading with Merriweather font*
<img src="./images/focus-reading/focus-reading-atkinson.jpg" height="500" alt="Focus Reading with Atkinson Hyperlegible Next font" />
*Focus Reading with Atkinson Hyperlegible Next font*
## Notes
- Focus Reading only applies to regular body text. Already-bold text (headings, emphasis) is left unchanged.
- The setting is per-device, not per-book — it applies to all books while enabled.
+1 -1
View File
@@ -49,5 +49,5 @@ A convenient script `update_hyphenation.sh` is used to update all languages.
To use it, run:
```sh
./scripts/update_hypenation.sh
./scripts/update_hyphenation.sh
```
+11 -7
View File
@@ -12,6 +12,10 @@ This guide explains the multi-language support system in CrossPoint Reader.
- Swedish
- Czech
- Russian
- Ukrainian
- Polish
- Danish
- Turkish
---
@@ -51,7 +55,7 @@ A file looks like this:
```yaml
_language_name: "Español"
_language_code: "SPANISH"
_language_code: "ES"
_order: "1"
STR_CROSSPOINT: "CrossPoint"
@@ -61,13 +65,13 @@ STR_BROWSE_FILES: "Buscar archivos"
**Metadata keys** (prefixed with `_`):
- `_language_name` — Native display name shown to the user (e.g. "Français")
- `_language_code` — C++ enum name (e.g. "FRENCH"). Must be a valid C++ identifier.
- `_language_code` — C++ enum name (e.g. "FR"). Please use the [ISO Code](https://en.wikipedia.org/wiki/List_of_ISO_639_language_codes) of the language. Must be a valid C++ identifier.
- `_order` — Controls the position in the Language enum (English is always 0)
**Rules:**
- Use UTF-8 encoding
- Every line must follow the format: `KEY: "value"`
- Keys must be valid C++ identifiers (uppercase, strats with STR_)
- Keys must be valid C++ identifiers (uppercase, starts with STR_)
- Keys must be unique within a file
- String values must be quoted
- Use `\n` for newlines, `\\` for literal backslashes, `\"` for literal quotes inside values
@@ -127,7 +131,7 @@ Create `lib/I18n/translations/italian.yaml`:
```yaml
_language_name: "Italiano"
_language_code: "ITALIAN"
_language_code: "IT"
_order: "7"
STR_CROSSPOINT: "CrossPoint"
@@ -174,7 +178,7 @@ renderer.drawText(font, x, y, tr(STR_BROWSE_FILES));
Serial.printf("Status: %s\n", tr(STR_CONNECTED));
// I18N - Shorthand for I18n::getInstance()
I18N.setLanguage(Language::SPANISH);
I18N.setLanguage(Language::ES);
Language lang = I18N.getLanguage();
// === Full API ===
@@ -188,7 +192,7 @@ const char* text = I18N.get(StrId::STR_SETTINGS_TITLE); // Direct call
const char* text = I18N[StrId::STR_SETTINGS_TITLE]; // Operator overload
// Set language
I18N.setLanguage(Language::SPANISH);
I18N.setLanguage(Language::ES);
// Get current language
Language lang = I18N.getLanguage();
@@ -200,7 +204,7 @@ I18N.saveSettings();
I18N.loadSettings();
// Get character set for font subsetting (static method)
const char* chars = I18n::getCharacterSet(Language::FRENCH);
const char* chars = I18n::getCharacterSet(Language::FR);
```
---
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

+104
View File
@@ -0,0 +1,104 @@
# 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 WiFi
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. Connect your CrossPoint reader to WiFi
2. Open the web interface in your browser (shown on the WiFi screen)
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+0020-U+007E (Basic Latin) |
| `latin-ext` | European languages (Latin + Extended-A/B) |
| `greek` | Greek + Extended Greek |
| `cyrillic` | Cyrillic + Supplement |
| `cjk` | CJK Unified Ideographs + Hiragana + Katakana + Fullwidth |
| `hangul` | Korean Hangul syllables |
| `builtin` | Matches built-in Bookerly coverage exactly |
Combine presets with commas: `--intervals latin-ext,greek,cyrillic`
Install custom fonts via WiFi upload or manual SD card copy.
+27 -1
View File
@@ -1,6 +1,6 @@
# Translators
Below is a list of users and languages CrossPoint may support in the future.
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.
## Contributing
@@ -20,8 +20,13 @@ If you'd like to add your name to this list, please open a PR adding yourself an
## Portuguese (Brazil)
- [yagofarias](https://github.com/yagofarias)
## Portuguese (Portugal)
- [victordomingos](https://github.com/victordomingos)
## Italian
- [andreaturchet](https://github.com/andreaturchet)
- [fragolinux](https://github.com/fragolinux)
- [alan0ford](https://github.com/alan0ford)
## Russian
- [madebyKir](https://github.com/madebyKir)
@@ -31,6 +36,27 @@ If you'd like to add your name to this list, please open a PR adding yourself an
- [yeyeto2788](https://github.com/yeyeto2788)
- [Skrzakk](https://github.com/Skrzakk)
- [pablohc](https://github.com/pablohc)
- [DaniPhii](https://github.com/DaniPhii)
## Swedish
- [dawiik](https://github.com/dawiik)
- [steka](https://github.com/steka)
## Romanian
- [ariel-lindemann](https://github.com/ariel-lindemann)
## Catalan
- [angeldenom](https://github.com/angeldenom)
## 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)
## Danish
- [hajisan](https://github.com/hajisan)
-37
View File
@@ -1,37 +0,0 @@
This directory is intended for project header files.
A header file is a file containing C declarations and macro definitions
to be shared between several project source files. You request the use of a
header file in your project source file (C, C++, etc) located in `src` folder
by including it, with the C preprocessing directive `#include'.
```src/main.c
#include "header.h"
int main (void)
{
...
}
```
Including a header file produces the same results as copying the header file
into each source file that needs it. Such copying would be time-consuming
and error-prone. With a header file, the related declarations appear
in only one place. If they need to be changed, they can be changed in one
place, and programs that include the header file will automatically use the
new version when next recompiled. The header file eliminates the labor of
finding and changing all the copies as well as the risk that a failure to
find one copy will result in inconsistencies within a program.
In C, the convention is to give header files names that end with `.h'.
Read more about using header files in official GCC documentation:
* Include Syntax
* Include Operation
* Once-Only Headers
* Computed Includes
https://gcc.gnu.org/onlinedocs/cpp/Header-Files.html
+125 -39
View File
@@ -15,37 +15,46 @@ void EpdFont::getTextBounds(const char* string, const int startX, const int star
return;
}
int cursorX = startX;
const int cursorY = startY;
int lastBaseX = startX;
int lastBaseAdvance = 0;
int lastBaseLeft = 0;
int lastBaseWidth = 0;
int lastBaseTop = 0;
bool hasBaseGlyph = false;
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)))) {
const EpdGlyph* glyph = getGlyph(cp);
const bool isCombining = utf8IsCombiningMark(cp);
if (!glyph) {
glyph = getGlyph(REPLACEMENT_GLYPH);
if (!isCombining) {
cp = applyLigatures(cp, string);
}
const EpdGlyph* glyph = getGlyph(cp);
if (!glyph) {
// TODO: Better handle this?
// 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;
}
const bool isCombining = utf8IsCombiningMark(cp);
int raiseBy = 0;
if (isCombining && hasBaseGlyph) {
const int currentGap = glyph->top - glyph->height - lastBaseTop;
if (currentGap < MIN_COMBINING_GAP_PX) {
raiseBy = MIN_COMBINING_GAP_PX - currentGap;
}
const int raiseBy = isCombining ? combiningMark::raiseAboveBase(glyph->top, glyph->height, lastBaseTop) : 0;
if (!isCombining && prevCp != 0) {
const auto kernFP = getKerning(prevCp, cp); // 4.4 fixed-point kern
lastBaseX += fp4::toPixel(prevAdvanceFP + kernFP);
}
const int glyphBaseX = (isCombining && hasBaseGlyph) ? (lastBaseX + lastBaseAdvance / 2) : cursorX;
const int glyphBaseY = cursorY - raiseBy;
const int glyphBaseX =
isCombining ? combiningMark::centerOver(lastBaseX, lastBaseLeft, lastBaseWidth, glyph->left, glyph->width)
: lastBaseX;
const int glyphBaseY = startY - raiseBy;
*minX = std::min(*minX, glyphBaseX + glyph->left);
*maxX = std::max(*maxX, glyphBaseX + glyph->left + glyph->width);
@@ -53,11 +62,11 @@ void EpdFont::getTextBounds(const char* string, const int startX, const int star
*maxY = std::max(*maxY, glyphBaseY + glyph->top);
if (!isCombining) {
lastBaseX = cursorX;
lastBaseAdvance = glyph->advanceX;
lastBaseLeft = glyph->left;
lastBaseWidth = glyph->width;
lastBaseTop = glyph->top;
hasBaseGlyph = true;
cursorX += glyph->advanceX;
prevAdvanceFP = glyph->advanceX; // 12.4 fixed-point
prevCp = cp;
}
}
}
@@ -71,30 +80,107 @@ void EpdFont::getTextDimensions(const char* string, int* w, int* h) const {
*h = maxY - minY;
}
static uint8_t lookupKernClass(const EpdKernClassEntry* entries, const uint16_t count, const uint32_t cp) {
if (!entries || count == 0 || cp > 0xFFFF) {
return 0;
}
const auto target = static_cast<uint16_t>(cp);
const auto* end = entries + count;
// lower_bound: exact-key lookup. Finds the first entry with codepoint >= target,
// then the equality check confirms an exact match exists.
const auto it = std::lower_bound(
entries, end, target, [](const EpdKernClassEntry& entry, uint16_t value) { return entry.codepoint < value; });
if (it != end && it->codepoint == target) {
return it->classId;
}
return 0;
}
int8_t EpdFont::getKerning(const uint32_t leftCp, const uint32_t rightCp) const {
if (!data->kernMatrix) {
return 0;
}
const uint8_t lc = lookupKernClass(data->kernLeftClasses, data->kernLeftEntryCount, leftCp);
if (lc == 0) return 0;
const uint8_t rc = lookupKernClass(data->kernRightClasses, data->kernRightEntryCount, rightCp);
if (rc == 0) return 0;
return data->kernMatrix[(lc - 1) * data->kernRightClassCount + (rc - 1)];
}
uint32_t EpdFont::getLigature(const uint32_t leftCp, const uint32_t rightCp) const {
const auto* pairs = data->ligaturePairs;
const auto count = data->ligaturePairCount;
if (!pairs || count == 0 || leftCp > 0xFFFF || rightCp > 0xFFFF) {
return 0;
}
const uint32_t key = (leftCp << 16) | rightCp;
const auto* end = pairs + count;
// lower_bound: exact-key lookup. Finds the first entry with pair >= key,
// then the equality check confirms an exact match exists.
const auto it =
std::lower_bound(pairs, end, key, [](const EpdLigaturePair& pair, uint32_t value) { return pair.pair < value; });
if (it != end && it->pair == key) {
return it->ligatureCp;
}
return 0;
}
uint32_t EpdFont::applyLigatures(uint32_t cp, const char*& text) const {
if (!data->ligaturePairs || data->ligaturePairCount == 0) {
return cp;
}
while (true) {
const auto saved = reinterpret_cast<const uint8_t*>(text);
const uint32_t nextCp = utf8NextCodepoint(reinterpret_cast<const uint8_t**>(&text));
if (nextCp == 0) break;
const uint32_t lig = getLigature(cp, nextCp);
if (lig == 0) {
text = reinterpret_cast<const char*>(saved);
break;
}
cp = lig;
}
return cp;
}
const EpdGlyph* EpdFont::getGlyph(const uint32_t cp) const {
const EpdUnicodeInterval* intervals = data->intervals;
const int count = data->intervalCount;
if (count == 0 && !data->glyphMissHandler) return nullptr;
if (count == 0) return nullptr;
if (count > 0) {
const EpdUnicodeInterval* intervals = data->intervals;
const auto* end = intervals + count;
// Binary search for O(log n) lookup instead of O(n)
// Critical for Korean fonts with many unicode intervals
int left = 0;
int right = count - 1;
// 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; });
while (left <= right) {
const int mid = left + (right - left) / 2;
const EpdUnicodeInterval* interval = &intervals[mid];
if (cp < interval->first) {
right = mid - 1;
} else if (cp > interval->last) {
left = mid + 1;
} else {
// Found: cp >= interval->first && 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);
}
return nullptr;
}
+12
View File
@@ -11,4 +11,16 @@ class EpdFont {
void getTextDimensions(const char* string, int* w, int* h) const;
const EpdGlyph* getGlyph(uint32_t cp) const;
/// Returns the kerning adjustment (4.4 fixed-point in pixels) between two codepoints.
/// Returns 0 if no kerning data exists for the pair.
int8_t getKerning(uint32_t leftCp, uint32_t rightCp) const;
/// Returns the ligature codepoint for a pair, or 0 if no ligature exists.
uint32_t getLigature(uint32_t leftCp, uint32_t rightCp) const;
/// Greedily applies ligature substitutions starting from cp, consuming
/// as many following codepoints from text as possible. Returns the
/// (possibly substituted) codepoint; advances text past consumed chars.
uint32_t applyLigatures(uint32_t cp, const char*& text) const;
};
+102 -4
View File
@@ -4,11 +4,73 @@
#pragma once
#include <cstdint>
/// 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 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.
namespace fp4 {
constexpr int FRAC_BITS = 4;
constexpr int32_t HALF = 1 << (FRAC_BITS - 1); // 8, added before shift for round-to-nearest
/// Convert an integer pixel value to 12.4 fixed-point.
constexpr int32_t fromPixel(int px) { return static_cast<int32_t>(px) << FRAC_BITS; }
/// Snap a fixed-point value to the nearest integer pixel.
constexpr int toPixel(int32_t fp) { return static_cast<int>((fp + HALF) >> FRAC_BITS); }
/// Convert a fixed-point value to float (mainly useful for debug logging).
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;
/// Compute the cursor-X at which to render a combining mark so its bitmap
/// is visually centered over the base glyph's bitmap.
constexpr int centerOver(int baseCursorPos, int baseLeft, int baseWidth, int markLeft, int markWidth) {
return baseCursorPos + baseLeft + baseWidth / 2 - markWidth / 2 - markLeft;
}
/// Rotated-90CW variant of centerOver. In the rotated coordinate system
/// renderCharImpl uses (cursorY - left) instead of (cursorX + left), so
/// every left/width term inverts sign.
constexpr int centerOverRotated90CW(int baseCursorPos, int baseLeft, int baseWidth, int markLeft, int markWidth) {
return baseCursorPos - baseLeft - baseWidth / 2 + markWidth / 2 + 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).
constexpr int raiseAboveBase(int markTop, int markHeight, int baseTop) {
if (markTop - markHeight <= 0) return 0;
const int gap = markTop - markHeight - baseTop;
return (gap < MIN_GAP_PX) ? (MIN_GAP_PX - gap) : 0;
}
} // namespace combiningMark
/// 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)
/// Both share 4 fractional bits so they combine directly in an accumulator.
/// Font data stored PER GLYPH
typedef struct {
uint8_t width; ///< Bitmap dimensions in pixels
uint8_t height; ///< Bitmap dimensions in pixels
uint8_t advanceX; ///< Distance to advance cursor (x axis)
uint16_t advanceX; ///< Distance to advance cursor (x axis), 12.4 fixed-point in pixels
int16_t left; ///< X dist from cursor pos to UL corner
int16_t top; ///< Y dist from cursor pos to UL corner
uint16_t dataLength; ///< Size of the font data.
@@ -21,7 +83,7 @@ typedef struct {
uint32_t compressedSize; ///< Compressed DEFLATE stream size
uint32_t uncompressedSize; ///< Decompressed size
uint16_t glyphCount; ///< Number of glyphs in this group
uint16_t firstGlyphIndex; ///< First glyph index in the global glyph array
uint32_t firstGlyphIndex; ///< First glyph index in the global glyph array
} EpdFontGroup;
/// Glyph interval structure
@@ -31,6 +93,20 @@ typedef struct {
uint32_t offset; ///< Index of the first code point into the glyph array
} EpdUnicodeInterval;
/// 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).
typedef struct {
uint16_t codepoint; ///< Unicode codepoint
uint8_t classId; ///< 1-based kerning class ID
} __attribute__((packed)) EpdKernClassEntry;
/// Ligature substitution for a specific glyph pair, sorted by `pair` for binary search.
/// `pair` encodes (leftCodepoint << 16 | rightCodepoint) for single-key lookup.
typedef struct {
uint32_t pair; ///< Packed codepoint pair (left << 16 | right)
uint32_t ligatureCp; ///< Codepoint of the replacement ligature glyph
} __attribute__((packed)) EpdLigaturePair;
/// Data stored for FONT AS A WHOLE
typedef struct {
const uint8_t* bitmap; ///< Glyph bitmaps, concatenated
@@ -41,6 +117,28 @@ typedef struct {
int ascender; ///< Maximal height of a glyph above the base line
int descender; ///< Maximal height of a glyph below the base line
bool is2Bit;
const EpdFontGroup* groups; ///< NULL for uncompressed fonts
uint16_t groupCount; ///< 0 for uncompressed fonts
const EpdFontGroup* groups; ///< NULL for uncompressed fonts
uint16_t groupCount; ///< 0 for uncompressed fonts
const uint16_t* glyphToGroup; ///< Per-glyph group ID (nullptr for contiguous-group fonts)
const EpdKernClassEntry* kernLeftClasses; ///< Sorted left-side class map (nullptr if none)
const EpdKernClassEntry* kernRightClasses; ///< Sorted right-side class map (nullptr if none)
const int8_t* kernMatrix; ///< Flat leftClassCount x rightClassCount matrix, 4.4 fixed-point in pixels
uint16_t kernLeftEntryCount; ///< Entries in kernLeftClasses
uint16_t kernRightEntryCount; ///< Entries in kernRightClasses
uint8_t kernLeftClassCount; ///< Number of distinct left classes (matrix rows)
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;
+9 -1
View File
@@ -26,4 +26,12 @@ const EpdFontData* EpdFontFamily::getData(const Style style) const { return getF
const EpdGlyph* EpdFontFamily::getGlyph(const uint32_t cp, const Style style) const {
return getFont(style)->getGlyph(cp);
};
}
int8_t EpdFontFamily::getKerning(const uint32_t leftCp, const uint32_t rightCp, const Style style) const {
return getFont(style)->getKerning(leftCp, rightCp);
}
uint32_t EpdFontFamily::applyLigatures(const uint32_t cp, const char*& text, const Style style) const {
return getFont(style)->applyLigatures(cp, text);
}
+2
View File
@@ -12,6 +12,8 @@ class EpdFontFamily {
void getTextDimensions(const char* string, int* w, int* h, Style style = REGULAR) const;
const EpdFontData* getData(Style style = REGULAR) const;
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;
private:
const EpdFont* regular;
+468 -97
View File
@@ -1,37 +1,55 @@
#include "FontDecompressor.h"
#include <Arduino.h>
#include <Logging.h>
#include <uzlib.h>
#include <Utf8.h>
#include <cstdlib>
#include <cstring>
FontDecompressor::~FontDecompressor() { deinit(); }
bool FontDecompressor::init() {
clearCache();
memset(&decomp, 0, sizeof(decomp));
return true;
}
void FontDecompressor::freeAllEntries() {
for (auto& entry : cache) {
if (entry.data) {
free(entry.data);
entry.data = nullptr;
}
entry.valid = false;
}
void FontDecompressor::deinit() {
freePageBuffer();
freeHotGroup();
}
void FontDecompressor::deinit() { freeAllEntries(); }
void FontDecompressor::clearCache() {
freeAllEntries();
accessCounter = 0;
freePageBuffer();
freeHotGroup();
}
uint16_t FontDecompressor::getGroupIndex(const EpdFontData* fontData, uint16_t glyphIndex) {
void FontDecompressor::freePageBuffer() {
for (uint8_t s = 0; s < pageSlotCount; s++) {
free(pageSlots[s].buffer);
free(pageSlots[s].glyphs);
pageSlots[s] = {};
}
pageSlotCount = 0;
}
void FontDecompressor::freeHotGroup() {
hotGroup.clear();
hotGroup.shrink_to_fit();
hotGroupFont = nullptr;
hotGroupIndex = UINT16_MAX;
hotGlyphBuf.clear();
hotGlyphBuf.shrink_to_fit();
}
uint16_t FontDecompressor::getGroupIndex(const EpdFontData* fontData, uint32_t glyphIndex) {
// O(1) path for frequency-grouped fonts with glyphToGroup mapping
if (fontData->glyphToGroup != nullptr) {
return fontData->glyphToGroup[glyphIndex];
}
// Contiguous-group fonts: linear scan
for (uint16_t i = 0; i < fontData->groupCount; i++) {
uint16_t first = fontData->groups[i].firstGlyphIndex;
uint32_t first = fontData->groups[i].firstGlyphIndex;
if (glyphIndex >= first && glyphIndex < first + fontData->groups[i].glyphCount) {
return i;
}
@@ -39,109 +57,462 @@ uint16_t FontDecompressor::getGroupIndex(const EpdFontData* fontData, uint16_t g
return fontData->groupCount; // sentinel = not found
}
FontDecompressor::CacheEntry* FontDecompressor::findInCache(const EpdFontData* fontData, uint16_t groupIndex) {
for (auto& entry : cache) {
if (entry.valid && entry.font == fontData && entry.groupIndex == groupIndex) {
return &entry;
}
}
return nullptr;
}
FontDecompressor::CacheEntry* FontDecompressor::findEvictionCandidate() {
// Find an invalid slot first
for (auto& entry : cache) {
if (!entry.valid) {
return &entry;
}
}
// Otherwise evict LRU
CacheEntry* lru = &cache[0];
for (auto& entry : cache) {
if (entry.lastUsed < lru->lastUsed) {
lru = &entry;
}
}
return lru;
}
bool FontDecompressor::decompressGroup(const EpdFontData* fontData, uint16_t groupIndex, CacheEntry* entry) {
bool FontDecompressor::decompressGroup(const EpdFontData* fontData, uint16_t groupIndex, uint8_t* outBuf,
uint32_t outSize) {
const EpdFontGroup& group = fontData->groups[groupIndex];
// Free old buffer if reusing a slot
if (entry->data) {
free(entry->data);
entry->data = nullptr;
}
entry->valid = false;
// Allocate output buffer
auto* outBuf = static_cast<uint8_t*>(malloc(group.uncompressedSize));
if (!outBuf) {
LOG_ERR("FDC", "Failed to allocate %u bytes for group %u", group.uncompressedSize, groupIndex);
const uint32_t tDecomp = millis();
inflateReader.init(false);
inflateReader.setSource(&fontData->bitmap[group.compressedOffset], group.compressedSize);
if (!inflateReader.read(outBuf, outSize)) {
stats.decompressTimeMs += millis() - tDecomp;
LOG_ERR("FDC", "Decompression failed for group %u", groupIndex);
return false;
}
// Decompress using uzlib
const uint8_t* inputBuf = &fontData->bitmap[group.compressedOffset];
uzlib_uncompress_init(&decomp, NULL, 0);
decomp.source = inputBuf;
decomp.source_limit = inputBuf + group.compressedSize;
decomp.dest_start = outBuf;
decomp.dest = outBuf;
decomp.dest_limit = outBuf + group.uncompressedSize;
int res = uzlib_uncompress(&decomp);
if (res < 0 || decomp.dest != decomp.dest_limit) {
LOG_ERR("FDC", "Decompression failed for group %u (status %d)", groupIndex, res);
free(outBuf);
return false;
}
entry->font = fontData;
entry->groupIndex = groupIndex;
entry->data = outBuf;
entry->dataSize = group.uncompressedSize;
entry->valid = true;
stats.decompressTimeMs += millis() - tDecomp;
return true;
}
const uint8_t* FontDecompressor::getBitmap(const EpdFontData* fontData, const EpdGlyph* glyph, uint16_t glyphIndex) {
// --- Byte-aligned helpers ---
uint32_t FontDecompressor::getAlignedOffset(const EpdFontData* fontData, uint16_t groupIndex, uint32_t glyphIndex) {
uint32_t offset = 0;
auto accumGlyph = [&](const EpdGlyph& g) {
if (g.width > 0 && g.height > 0) {
offset += ((g.width + 3) / 4) * g.height;
}
};
if (fontData->glyphToGroup) {
// Frequency-grouped: scan glyphs before glyphIndex that belong to this group
for (uint32_t i = 0; i < glyphIndex; i++) {
if (fontData->glyphToGroup[i] == groupIndex) {
accumGlyph(fontData->glyph[i]);
}
}
} else {
// Contiguous-group: sum aligned sizes of preceding glyphs in the group
const EpdFontGroup& group = fontData->groups[groupIndex];
for (uint32_t i = group.firstGlyphIndex; i < glyphIndex; i++) {
accumGlyph(fontData->glyph[i]);
}
}
return offset;
}
void FontDecompressor::compactSingleGlyph(const uint8_t* alignedSrc, uint8_t* packedDst, uint8_t width,
uint8_t height) {
if (width == 0 || height == 0) return;
const uint32_t rowStride = (width + 3) / 4;
if (width % 4 == 0) {
memcpy(packedDst, alignedSrc, rowStride * height);
return;
}
uint8_t outByte = 0, outBits = 0;
uint32_t writeIdx = 0;
for (uint8_t y = 0; y < height; y++) {
for (uint8_t x = 0; x < width; x++) {
outByte = (outByte << 2) | ((alignedSrc[y * rowStride + x / 4] >> ((3 - (x % 4)) * 2)) & 0x3);
outBits += 2;
if (outBits == 8) {
packedDst[writeIdx++] = outByte;
outByte = 0;
outBits = 0;
}
}
}
if (outBits > 0) packedDst[writeIdx] = outByte << (8 - outBits);
}
// --- getBitmap: page buffer → hot group → decompress ---
const uint8_t* FontDecompressor::getBitmap(const EpdFontData* fontData, const EpdGlyph* glyph, uint32_t glyphIndex) {
const uint32_t tStart = micros();
stats.getBitmapCalls++;
if (!fontData->groups || fontData->groupCount == 0) {
stats.getBitmapTimeUs += micros() - tStart;
return &fontData->bitmap[glyph->dataOffset];
}
// Check page buffer slots (populated by prewarmCache — one slot per font style)
for (uint8_t s = 0; s < pageSlotCount; s++) {
const auto& slot = pageSlots[s];
if (slot.fontData != fontData || slot.glyphCount == 0) continue;
int left = 0, right = slot.glyphCount - 1;
while (left <= right) {
int mid = left + (right - left) / 2;
if (slot.glyphs[mid].glyphIndex == glyphIndex) {
if (slot.glyphs[mid].bufferOffset != UINT32_MAX) {
stats.cacheHits++;
stats.getBitmapTimeUs += micros() - tStart;
return &slot.buffer[slot.glyphs[mid].bufferOffset];
}
break; // Not extracted during prewarm; fall through to hot-group path
}
if (slot.glyphs[mid].glyphIndex < glyphIndex)
left = mid + 1;
else
right = mid - 1;
}
break; // Found the right slot but glyph wasn't in it; don't check other slots
}
// Fallback: hot group slot
uint16_t groupIndex = getGroupIndex(fontData, glyphIndex);
if (groupIndex >= fontData->groupCount) {
LOG_ERR("FDC", "Glyph %u not found in any group", glyphIndex);
stats.getBitmapTimeUs += micros() - tStart;
return nullptr;
}
// Check cache
CacheEntry* entry = findInCache(fontData, groupIndex);
if (entry) {
entry->lastUsed = ++accessCounter;
if (glyph->dataOffset + glyph->dataLength > entry->dataSize) {
LOG_ERR("FDC", "dataOffset %u + dataLength %u out of bounds for group %u (size %u)", glyph->dataOffset,
glyph->dataLength, groupIndex, entry->dataSize);
// Check if hot group already has this group decompressed — if not, decompress it
if (!(!hotGroup.empty() && hotGroupFont == fontData && hotGroupIndex == groupIndex)) {
stats.cacheMisses++;
const EpdFontGroup& group = fontData->groups[groupIndex];
hotGroup.resize(group.uncompressedSize);
if (hotGroup.empty()) {
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;
}
return &entry->data[glyph->dataOffset];
if (!decompressGroup(fontData, groupIndex, hotGroup.data(), group.uncompressedSize)) {
hotGroup.clear();
hotGroup.shrink_to_fit();
hotGroupFont = nullptr;
hotGroupIndex = UINT16_MAX;
stats.getBitmapTimeUs += micros() - tStart;
return nullptr;
}
hotGroupFont = fontData;
hotGroupIndex = groupIndex;
stats.hotGroupBytes = group.uncompressedSize;
} else {
stats.cacheHits++;
}
// Cache miss - decompress
entry = findEvictionCandidate();
if (!decompressGroup(fontData, groupIndex, entry)) {
// Compact just the requested glyph from byte-aligned data into scratch buffer
if (glyph->dataLength > hotGlyphBuf.size()) {
hotGlyphBuf.resize(glyph->dataLength);
}
if (hotGlyphBuf.empty()) {
stats.getBitmapTimeUs += micros() - tStart;
return nullptr;
}
entry->lastUsed = ++accessCounter;
if (glyph->dataOffset + glyph->dataLength > entry->dataSize) {
LOG_ERR("FDC", "dataOffset %u + dataLength %u out of bounds for group %u (size %u)", glyph->dataOffset,
glyph->dataLength, groupIndex, entry->dataSize);
return nullptr;
uint32_t alignedOff = getAlignedOffset(fontData, groupIndex, glyphIndex);
compactSingleGlyph(&hotGroup[alignedOff], hotGlyphBuf.data(), glyph->width, glyph->height);
stats.getBitmapTimeUs += micros() - tStart;
return hotGlyphBuf.data();
}
// --- Prewarm: pre-decompress glyph bitmaps for a page of text ---
int32_t FontDecompressor::findGlyphIndex(const EpdFontData* fontData, uint32_t codepoint) {
const EpdUnicodeInterval* intervals = fontData->intervals;
const int count = fontData->intervalCount;
if (count == 0) return -1;
// Binary search
int left = 0;
int right = count - 1;
while (left <= right) {
const int mid = left + (right - left) / 2;
const EpdUnicodeInterval* interval = &intervals[mid];
if (codepoint < interval->first) {
right = mid - 1;
} else if (codepoint > interval->last) {
left = mid + 1;
} else {
return static_cast<int32_t>(interval->offset + (codepoint - interval->first));
}
}
return &entry->data[glyph->dataOffset];
return -1;
}
int FontDecompressor::prewarmCache(const EpdFontData* fontData, const char* utf8Text) {
if (!fontData || !fontData->groups || !utf8Text) return 0;
// Allocate the next available slot (caller must call freePageBuffer/clearCache to reset)
if (pageSlotCount >= MAX_PAGE_SLOTS) {
LOG_ERR("FDC", "All %u page buffer slots full, cannot prewarm fontData=%p", MAX_PAGE_SLOTS, (void*)fontData);
return -1;
}
PageSlot& slot = pageSlots[pageSlotCount];
// Step 1: Collect unique glyph indices needed for this page
uint32_t neededGlyphs[MAX_PAGE_GLYPHS];
uint16_t glyphCount = 0;
bool glyphCapWarned = false;
const unsigned char* p = reinterpret_cast<const unsigned char*>(utf8Text);
while (*p) {
uint32_t cp = utf8NextCodepoint(&p);
if (cp == 0) break;
int32_t glyphIdx = findGlyphIndex(fontData, cp);
if (glyphIdx < 0) continue;
// Deduplicate
bool found = false;
for (uint16_t i = 0; i < glyphCount; i++) {
if (neededGlyphs[i] == static_cast<uint32_t>(glyphIdx)) {
found = true;
break;
}
}
if (!found) {
if (glyphCount < MAX_PAGE_GLYPHS) {
neededGlyphs[glyphCount++] = static_cast<uint32_t>(glyphIdx);
} else if (!glyphCapWarned) {
LOG_DBG("FDC", "Glyph cap (%u) reached during prewarm; excess glyphs will use hot-group fallback",
MAX_PAGE_GLYPHS);
glyphCapWarned = true;
}
}
}
// 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
uint32_t totalBytes = 0;
uint16_t neededGroups[128];
uint8_t groupCount = 0;
bool groupCapWarned = false;
for (uint16_t i = 0; i < glyphCount; i++) {
totalBytes += fontData->glyph[neededGlyphs[i]].dataLength;
uint16_t gi = getGroupIndex(fontData, neededGlyphs[i]);
bool found = false;
for (uint8_t j = 0; j < groupCount; j++) {
if (neededGroups[j] == gi) {
found = true;
break;
}
}
if (!found) {
if (groupCount < 128) {
neededGroups[groupCount++] = gi;
} else if (!groupCapWarned) {
LOG_DBG("FDC", "Group cap (128) reached during prewarm; some groups will use hot-group fallback");
groupCapWarned = true;
}
}
}
stats.uniqueGroupsAccessed = groupCount;
// Step 3: Allocate page buffer and lookup table for this slot
slot.buffer = static_cast<uint8_t*>(malloc(totalBytes));
slot.glyphs = static_cast<PageGlyphEntry*>(malloc(glyphCount * sizeof(PageGlyphEntry)));
if (!slot.buffer || !slot.glyphs) {
LOG_ERR("FDC", "Failed to allocate page buffer (%u bytes, %u glyphs)", totalBytes, glyphCount);
free(slot.buffer);
free(slot.glyphs);
slot = {};
return glyphCount;
}
stats.pageBufferBytes += totalBytes;
stats.pageGlyphsBytes += glyphCount * sizeof(PageGlyphEntry);
slot.fontData = fontData;
slot.glyphCount = glyphCount;
pageSlotCount++;
// Initialize lookup entries (bufferOffset = UINT32_MAX means not yet extracted)
for (uint16_t i = 0; i < glyphCount; i++) {
slot.glyphs[i] = {neededGlyphs[i], UINT32_MAX, 0};
}
// Sort by glyphIndex for binary search in getBitmap()
for (uint16_t i = 1; i < glyphCount; i++) {
PageGlyphEntry key = slot.glyphs[i];
int j = i - 1;
while (j >= 0 && slot.glyphs[j].glyphIndex > key.glyphIndex) {
slot.glyphs[j + 1] = slot.glyphs[j];
j--;
}
slot.glyphs[j + 1] = key;
}
// Step 3b: Pre-scan to compute each needed glyph's byte-aligned offset within its group.
// This avoids recomputing aligned offsets per group during extraction in step 4.
uint32_t groupAlignedTracker[128] = {}; // running byte-aligned offset for each needed group
if (fontData->glyphToGroup) {
// Frequency-grouped: single O(totalGlyphs) pass through glyphToGroup
const auto& lastInterval = fontData->intervals[fontData->intervalCount - 1];
const uint32_t totalGlyphs = lastInterval.offset + (lastInterval.last - lastInterval.first + 1);
for (uint32_t i = 0; i < totalGlyphs; i++) {
const uint16_t gi = fontData->glyphToGroup[i];
// Find this glyph's group position in neededGroups
uint8_t gpPos = groupCount;
for (uint8_t j = 0; j < groupCount; j++) {
if (neededGroups[j] == gi) {
gpPos = j;
break;
}
}
if (gpPos == groupCount) continue; // not a needed group
const EpdGlyph& glyph = fontData->glyph[i];
// Binary search in sorted slot.glyphs to find if glyph i is needed
int left = 0, right = (int)slot.glyphCount - 1;
while (left <= right) {
const int mid = left + (right - left) / 2;
if (slot.glyphs[mid].glyphIndex == i) {
slot.glyphs[mid].alignedOffset = groupAlignedTracker[gpPos];
break;
}
if (slot.glyphs[mid].glyphIndex < i)
left = mid + 1;
else
right = mid - 1;
}
if (glyph.width > 0 && glyph.height > 0) {
groupAlignedTracker[gpPos] += ((glyph.width + 3) / 4) * glyph.height;
}
}
} else {
// Contiguous-group: iterate each needed group's glyphs directly
for (uint8_t g = 0; g < groupCount; g++) {
const EpdFontGroup& group = fontData->groups[neededGroups[g]];
uint32_t alignedOff = 0;
for (uint16_t j = 0; j < group.glyphCount; j++) {
const uint32_t glyphI = group.firstGlyphIndex + j;
const EpdGlyph& glyph = fontData->glyph[glyphI];
int left = 0, right = (int)slot.glyphCount - 1;
while (left <= right) {
const int mid = left + (right - left) / 2;
if (slot.glyphs[mid].glyphIndex == glyphI) {
slot.glyphs[mid].alignedOffset = alignedOff;
break;
}
if (slot.glyphs[mid].glyphIndex < glyphI)
left = mid + 1;
else
right = mid - 1;
}
if (glyph.width > 0 && glyph.height > 0) {
alignedOff += ((glyph.width + 3) / 4) * glyph.height;
}
}
}
}
// Step 4: For each unique group, decompress to temp buffer and extract needed glyphs
uint32_t writeOffset = 0;
int missed = 0;
for (uint8_t g = 0; g < groupCount; g++) {
uint16_t groupIdx = neededGroups[g];
const EpdFontGroup& group = fontData->groups[groupIdx];
auto* tempBuf = static_cast<uint8_t*>(malloc(group.uncompressedSize));
if (!tempBuf) {
LOG_ERR("FDC", "Failed to allocate temp buffer (%u bytes) for group %u", group.uncompressedSize, groupIdx);
missed++;
continue;
}
if (group.uncompressedSize > stats.peakTempBytes) {
stats.peakTempBytes = group.uncompressedSize;
}
if (!decompressGroup(fontData, groupIdx, tempBuf, group.uncompressedSize)) {
free(tempBuf);
missed++;
continue;
}
// Extract needed glyphs directly from the byte-aligned temp buffer, compacting on the fly.
// alignedOffset was pre-computed in step 3b — no full-group compact scan needed.
for (uint16_t i = 0; i < slot.glyphCount; i++) {
if (slot.glyphs[i].bufferOffset != UINT32_MAX) continue; // already extracted
if (getGroupIndex(fontData, slot.glyphs[i].glyphIndex) != groupIdx) continue;
const EpdGlyph& glyph = fontData->glyph[slot.glyphs[i].glyphIndex];
compactSingleGlyph(&tempBuf[slot.glyphs[i].alignedOffset], &slot.buffer[writeOffset], glyph.width, glyph.height);
slot.glyphs[i].bufferOffset = writeOffset;
writeOffset += glyph.dataLength;
}
free(tempBuf);
}
LOG_DBG("FDC", "Prewarm: %u glyphs in %u bytes from %u groups (%d missed)", glyphCount, writeOffset, groupCount,
missed);
return missed;
}
// --- Stats ---
void FontDecompressor::resetStats() { stats = Stats{}; }
void FontDecompressor::logStats(const char* label) {
const uint32_t total = stats.cacheHits + stats.cacheMisses;
LOG_DBG("FDC", "[%s] hits=%lu misses=%lu (%.1f%% hit rate)", label, stats.cacheHits, stats.cacheMisses,
total > 0 ? 100.0f * stats.cacheHits / total : 0.0f);
LOG_DBG("FDC", "[%s] decompress=%lums groups_accessed=%u", label, stats.decompressTimeMs, stats.uniqueGroupsAccessed);
LOG_DBG("FDC", "[%s] mem: pageBuf=%lu pageGlyphs=%lu hotGroup=%lu peakTemp=%lu", label, stats.pageBufferBytes,
stats.pageGlyphsBytes, stats.hotGroupBytes, stats.peakTempBytes);
if (stats.getBitmapCalls > 0) {
LOG_DBG("FDC", "[%s] getBitmap: %lu calls, %luus total, %luus/call avg", label, stats.getBitmapCalls,
stats.getBitmapTimeUs, stats.getBitmapTimeUs / stats.getBitmapCalls);
}
resetStats();
}
+65 -22
View File
@@ -1,42 +1,85 @@
#pragma once
#include <uzlib.h>
#include <InflateReader.h>
#include <cstdint>
#include <vector>
#include "EpdFontData.h"
class FontDecompressor {
public:
static constexpr uint16_t MAX_PAGE_GLYPHS = 512;
static constexpr uint8_t MAX_PAGE_SLOTS = 4; // One per font style (R/B/I/BI)
FontDecompressor() = default;
~FontDecompressor();
bool init();
void deinit();
// Returns pointer to decompressed bitmap data for the given glyph.
// Valid until LRU eviction (safe for the duration of one glyph render).
const uint8_t* getBitmap(const EpdFontData* fontData, const EpdGlyph* glyph, uint16_t glyphIndex);
// Checks the page buffer (from prewarm) first, then falls back to the hot group slot.
const uint8_t* getBitmap(const EpdFontData* fontData, const EpdGlyph* glyph, uint32_t glyphIndex);
// Evict all cached decompressed groups (call between pages for within-page-only caching).
// Free all cached data (page buffer + hot group).
void clearCache();
private:
static constexpr uint8_t CACHE_SLOTS = 4;
// Pre-scan UTF-8 text and extract needed glyph bitmaps into a flat page buffer.
// Each group is decompressed once into a temp buffer; only needed glyphs are kept.
// Returns the number of glyphs that couldn't be loaded (0 on full success).
int prewarmCache(const EpdFontData* fontData, const char* utf8Text);
struct CacheEntry {
const EpdFontData* font = nullptr;
uint16_t groupIndex = 0;
uint8_t* data = nullptr;
uint32_t dataSize = 0;
uint32_t lastUsed = 0;
bool valid = false;
struct Stats {
uint32_t cacheHits = 0;
uint32_t cacheMisses = 0;
uint32_t decompressTimeMs = 0;
uint16_t uniqueGroupsAccessed = 0;
uint32_t pageBufferBytes = 0; // pageBuffer allocation
uint32_t pageGlyphsBytes = 0; // pageGlyphs lookup table allocation
uint32_t hotGroupBytes = 0; // current hot group allocation
uint32_t peakTempBytes = 0; // largest temp buffer in prewarm
uint32_t getBitmapTimeUs = 0; // cumulative getBitmap time (micros)
uint32_t getBitmapCalls = 0; // number of getBitmap calls
};
void logStats(const char* label = "FDC");
void resetStats();
const Stats& getStats() const { return stats; }
struct uzlib_uncomp decomp = {};
CacheEntry cache[CACHE_SLOTS] = {};
uint32_t accessCounter = 0;
private:
Stats stats;
InflateReader inflateReader;
void freeAllEntries();
uint16_t getGroupIndex(const EpdFontData* fontData, uint16_t glyphIndex);
CacheEntry* findInCache(const EpdFontData* fontData, uint16_t groupIndex);
CacheEntry* findEvictionCandidate();
bool decompressGroup(const EpdFontData* fontData, uint16_t groupIndex, CacheEntry* entry);
// Page buffer slots: each style gets its own flat glyph buffer with sorted lookup.
// Up to MAX_PAGE_SLOTS (4) styles can be prewarmed simultaneously.
struct PageGlyphEntry {
uint32_t glyphIndex;
uint32_t bufferOffset;
uint32_t alignedOffset; // byte-aligned offset within its decompressed group (set during prewarm pre-scan)
};
struct PageSlot {
uint8_t* buffer = nullptr;
const EpdFontData* fontData = nullptr;
PageGlyphEntry* glyphs = nullptr;
uint16_t glyphCount = 0;
};
PageSlot pageSlots[MAX_PAGE_SLOTS] = {};
uint8_t pageSlotCount = 0;
// 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.
const EpdFontData* hotGroupFont = nullptr;
uint16_t hotGroupIndex = UINT16_MAX;
std::vector<uint8_t> hotGroup;
// Scratch buffer for compacting a single glyph from the hot group.
// Valid until the next getBitmap() call.
std::vector<uint8_t> hotGlyphBuf;
void freePageBuffer();
void freeHotGroup();
uint16_t getGroupIndex(const EpdFontData* fontData, uint32_t glyphIndex);
uint32_t getAlignedOffset(const EpdFontData* fontData, uint16_t groupIndex, uint32_t glyphIndex);
bool decompressGroup(const EpdFontData* fontData, uint16_t groupIndex, uint8_t* outBuf, uint32_t outSize);
static void compactSingleGlyph(const uint8_t* alignedSrc, uint8_t* packedDst, uint8_t width, uint8_t height);
static int32_t findGlyphIndex(const EpdFontData* fontData, uint32_t codepoint);
};
File diff suppressed because it is too large Load Diff
+254
View File
@@ -0,0 +1,254 @@
#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.
// Returns number of codepoints not found in font coverage.
int buildAdvanceTable(const char* utf8Text, uint8_t styleMask = 0x0F);
int buildAdvanceTable(const std::vector<std::string>& words, bool includeHyphen, uint8_t styleMask = 0x0F);
// Look up advanceX for a codepoint from the advance table.
// 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, restore stub EpdFontData.
// Also clears the temporary advance table (built per layout pass) but
// preserves the persistent advance cache (reused across passes).
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;
// 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);
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);
};
+98
View File
@@ -0,0 +1,98 @@
#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 by ordinal position: sort available sizes, then map the font size
// enum (SMALL=0 .. EXTRA_LARGE=3) to the corresponding slot. When the
// family has fewer sizes than 4, clamp to the last available size.
auto sizes = family.availableSizes();
if (sizes.empty()) {
LOG_ERR("SDMGR", "Family %s has no files to load", family.name.c_str());
return false;
}
uint8_t idx = fontSizeEnum;
if (idx >= sizes.size()) idx = sizes.size() - 1;
const SdCardFontFileInfo* selected = family.findFile(sizes[idx]);
auto* font = new (std::nothrow) SdCardFont();
if (!font) {
LOG_ERR("SDMGR", "Failed to allocate SdCardFont for %s", selected->path.c_str());
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 matching fontSizeEnum (SMALL=0 .. EXTRA_LARGE=3) by
// ordinal position in the family's sorted size list. Only one .cpfont file
// is loaded; other sizes remain on disk. This keeps resident interval +
// kern/ligature tables to one size's worth of memory.
// Returns true on success.
bool loadFamily(const SdCardFontFamilyInfo& family, GfxRenderer& renderer, uint8_t 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 (closest match to targetPtSize).
// 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_;
};
+230
View File
@@ -0,0 +1,230 @@
#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;
}
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) {
FsFile dir = Storage.open(dirPath);
if (!dir || !dir.isDirectory()) return;
char nameBuffer[128];
while (true) {
FsFile 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) {
FsFile 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) {
FsFile 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;
}
+58
View File
@@ -0,0 +1,58 @@
#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;
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 -16
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>
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
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
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