Compare commits
13
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8959836e46 | ||
|
|
fd79074d43 | ||
|
|
b971f7bda4 | ||
|
|
c26e410c58 | ||
|
|
16e5e5f00b | ||
|
|
74e75746df | ||
|
|
a7586f20a1 | ||
|
|
b145e4437c | ||
|
|
fcb8793640 | ||
|
|
6c97061629 | ||
|
|
9b2d8388ef | ||
|
|
bb3a44f584 | ||
|
|
bd546093cb |
@@ -1,122 +1,169 @@
|
|||||||
# CrossPoint Reader
|
# CrossPoint Reader
|
||||||
|
|
||||||
Firmware for the **Xteink X4** e-paper display reader (unaffiliated with Xteink).
|
CrossPoint is open-source e-reader firmware - community-built, fully hackable, free forever. It's maintained by a growing community of developers and readers who believe your device should do what you want - not what a manufacturer decided for you.
|
||||||
Built using **PlatformIO** and targeting the **ESP32-C3** microcontroller.
|
|
||||||
|
|
||||||
CrossPoint Reader is a purpose-built firmware designed to be a drop-in, fully open-source replacement for the official
|
**Now running on:** ESP32C3-based Xteink [X4](https://www.xteink.com/products/xteink-x4) and [X3](https://www.xteink.com/products/xteink-x3).
|
||||||
Xteink firmware. It aims to match or improve upon the standard EPUB reading experience.
|
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
## Motivation
|
## What can CrossPoint do?
|
||||||
|
|
||||||
E-paper devices are fantastic for reading, but most commercially available readers are closed systems with limited
|
- **Reader engine**: EPUB 2/3 rendering with embedded-style option, image handling, hyphenation, kerning, chapter navigation, footnotes, go-to-percent, auto page turn, orientation control, focus reading, KOReader progress sync and more.
|
||||||
customisation. The **Xteink X4** is an affordable, e-paper device, however the official firmware remains closed.
|
|
||||||
CrossPoint exists partly as a fun side-project and partly to open up the ecosystem and truly unlock the device's
|
|
||||||
potential.
|
|
||||||
|
|
||||||
CrossPoint Reader aims to:
|
- **Various formats**: native handling for `.epub`, `.xtc/.xtch`, `.txt`, and `.bmp`.
|
||||||
* Provide a **fully open-source alternative** to the official firmware.
|
|
||||||
* Offer a **document reader** capable of handling EPUB content on constrained hardware.
|
|
||||||
* Support **customisable font, layout, and display** options.
|
|
||||||
* Run purely on the **Xteink X4 hardware**.
|
|
||||||
|
|
||||||
This project is **not affiliated with Xteink**; it's built as a community project.
|
- **Screenshots.**
|
||||||
|
|
||||||
## Features & Usage
|
- **Custom fonts**: install your favorite fonts on the SD card.
|
||||||
|
|
||||||
- [x] EPUB parsing and rendering (EPUB 2 and EPUB 3)
|
- **Tilt page turn (X3 only)**.
|
||||||
- [x] Image support within EPUB
|
|
||||||
- [x] Saved reading position
|
|
||||||
- [x] File explorer with file picker
|
|
||||||
- [x] Basic EPUB picker from root directory
|
|
||||||
- [x] Support nested folders
|
|
||||||
- [ ] EPUB picker with cover art
|
|
||||||
- [x] Custom sleep screen
|
|
||||||
- [x] Cover sleep screen
|
|
||||||
- [x] Wifi book upload
|
|
||||||
- [x] Wifi OTA updates
|
|
||||||
- [x] KOReader Sync integration for cross-device reading progress
|
|
||||||
- [x] Configurable font, layout, and display options
|
|
||||||
- [ ] User provided fonts
|
|
||||||
- [ ] Full UTF support
|
|
||||||
- [x] Screen rotation
|
|
||||||
|
|
||||||
Multi-language support: Read EPUBs in various languages, including English, Spanish, French, German, Italian, Portuguese, Russian, Ukrainian, Polish, Swedish, Norwegian, [and more](./USER_GUIDE.md#supported-languages).
|
- **Library workflow**: folder browser, hidden-file toggle, long-press delete, recent books, SD-cache management.
|
||||||
|
|
||||||
See [the user guide](./USER_GUIDE.md) for instructions on operating CrossPoint, including the
|
- **Wireless workflows**:
|
||||||
[KOReader Sync quick setup](./USER_GUIDE.md#367-koreader-sync-quick-setup).
|
|
||||||
|
- File transfer web UI
|
||||||
|
- EPUB Optimizer
|
||||||
|
- Web settings UI/API (edit many device settings from browser)
|
||||||
|
- WebSocket fast uploads
|
||||||
|
- WebDAV handler
|
||||||
|
- AP mode (hotspot) and STA mode (join existing WiFi), both with QR helpers
|
||||||
|
- Calibre wireless connect flow
|
||||||
|
- OPDS browser with saved servers (up to 8), search, pagination, and direct download
|
||||||
|
- OTA update checks and installs from GitHub releases
|
||||||
|
|
||||||
For more details about the scope of the project, see the [SCOPE.md](SCOPE.md) document.
|
- **Customization**: multiple themes (Classic, Lyra, Lyra Extended, RoundedRaff), sleep screen modes, front/side button remapping, status bar controls, power-button behavior, refresh cadence, and more.
|
||||||
|
|
||||||
## Installing
|
- **Localization**: 22 UI languages and counting.
|
||||||
|
|
||||||
### Web (latest firmware)
|
### Coming soon:
|
||||||
|
|
||||||
1. Connect your Xteink X4 to your computer via USB-C and wake/unlock the device
|
- RTL support — Arabic, Hebrew, and Farsi.
|
||||||
2. Go to https://xteink.dve.al/ and click "Flash CrossPoint firmware"
|
|
||||||
|
|
||||||
To revert back to the official firmware, you can flash the latest official firmware from https://xteink.dve.al/, or swap
|
- Bookmarks.
|
||||||
back to the other partition using the "Swap boot partition" button here https://xteink.dve.al/debug.
|
|
||||||
|
|
||||||
### Web (specific firmware version)
|
- Dictionary lookup — inline word lookup without leaving the reader.
|
||||||
|
|
||||||
1. Connect your Xteink X4 to your computer via USB-C
|
- More themes.
|
||||||
2. Download the `firmware.bin` file from the release of your choice via the [releases page](https://github.com/crosspoint-reader/crosspoint-reader/releases)
|
|
||||||
3. Go to https://xteink.dve.al/ and flash the firmware file using the "OTA fast flash controls" section
|
|
||||||
|
|
||||||
To revert back to the official firmware, you can flash the latest official firmware from https://xteink.dve.al/, or swap
|
- Much more! stay tuned.
|
||||||
back to the other partition using the "Swap boot partition" button here https://xteink.dve.al/debug.
|
|
||||||
|
|
||||||
### Command line (specific firmware version)
|
---
|
||||||
|
|
||||||
|
## USB-locked devices (Xteink Unlocker)
|
||||||
|
|
||||||
|
Some Xteink units purchased from third-party stores (e.g. AliExpress) ship with USB flashing locked from the factory.
|
||||||
|
If your device is locked, you will need to use the **Xteink Unlocker** tool available at
|
||||||
|
https://crosspointreader.com/#unlock-tool before you can flash CrossPoint.
|
||||||
|
|
||||||
|
**You do not need this tool if you bought your device directly from xteink.com.** Those units are not locked.
|
||||||
|
|
||||||
|
**Not sure if your device is locked?** Power it on, connect the USB-C cable, and try flashing via the web flasher first (see
|
||||||
|
[Install firmware](#install-firmware) below). If the browser's serial device picker does not show your device, try a different
|
||||||
|
USB port or browser before assuming the device is locked. Only reach for the unlocker if the device still doesn't appear.
|
||||||
|
|
||||||
|
> ### ⚠️ WARNING: READ THIS BEFORE USING THE UNLOCKER ⚠️
|
||||||
|
>
|
||||||
|
> **The only officially supported firmwares in the unlock tool are CrossPoint and CrossInk.**
|
||||||
|
>
|
||||||
|
> Flashing any other firmware on a USB-locked device may **permanently brick the device** or leave it **permanently
|
||||||
|
> stuck on that firmware with no recovery path**. Once USB flashing is re-locked, your only way back is via OTA, and if
|
||||||
|
> the firmware you flashed doesn't support OTA, **there is no way out**.
|
||||||
|
>
|
||||||
|
> **The Papyrix fork has removed OTA update support from its code.** If you flash Papyrix onto a
|
||||||
|
> USB-locked unit, you will have **zero update or recovery path** and will be stuck on it forever. **Do not flash
|
||||||
|
> Papyrix (or any other unsupported firmware) on a locked device.**
|
||||||
|
|
||||||
|
## Install firmware
|
||||||
|
|
||||||
|
### Web installer (recommended)
|
||||||
|
|
||||||
|
1. Connect your device to your computer via USB-C and wake/unlock the device
|
||||||
|
2. Go to https://crosspointreader.com/#flash-tools, select device (X3 or X4), and choose an official CrossPoint release.
|
||||||
|
|
||||||
|
### Web installer (specific version)
|
||||||
|
|
||||||
|
1. Connect your device to your computer via USB-C and wake/unlock the device
|
||||||
|
2. Download a `firmware.bin` from [Releases](https://github.com/crosspoint-reader/crosspoint-reader/releases), local build, or continuous integration artifact.
|
||||||
|
3. Go to https://crosspointreader.com/#flash-tools, select device (X3 or X4), click "Custom .bin" and upload a `firmware.bin`.
|
||||||
|
|
||||||
|
### Revert to Official Firmware
|
||||||
|
|
||||||
|
To revert to the official firmware, you can also flash the latest official firmware using https://crosspointreader.com/#flash-tools.
|
||||||
|
|
||||||
|
### Command line
|
||||||
|
|
||||||
|
1. Install [`esptool`](https://github.com/espressif/esptool):
|
||||||
|
|
||||||
1. Install [`esptool`](https://github.com/espressif/esptool) :
|
|
||||||
```bash
|
```bash
|
||||||
pip install esptool
|
pip install esptool
|
||||||
```
|
```
|
||||||
2. Download the `firmware.bin` file from the release of your choice via the [releases page](https://github.com/crosspoint-reader/crosspoint-reader/releases)
|
|
||||||
3. Connect your Xteink X4 to your computer via USB-C.
|
2. Download `firmware.bin` from the [releases page](https://github.com/crosspoint-reader/crosspoint-reader/releases).
|
||||||
4. Note the device location. On Linux, run `dmesg` after connecting. On MacOS, run :
|
3. Connect your device via USB-C.
|
||||||
|
4. Find the device port. On Linux, run `dmesg` after connecting. On macOS:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
log stream --predicate 'subsystem == "com.apple.iokit"' --info
|
log stream --predicate 'subsystem == "com.apple.iokit"' --info
|
||||||
```
|
```
|
||||||
5. Flash the firmware :
|
|
||||||
|
5. Flash:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
esptool.py --chip esp32c3 --port /dev/ttyACM0 --baud 921600 write_flash 0x10000 /path/to/firmware.bin
|
esptool.py --chip esp32c3 --port /dev/ttyACM0 --baud 921600 write_flash 0x10000 /path/to/firmware.bin
|
||||||
```
|
```
|
||||||
Change `/dev/ttyACM0` to the device for your system.
|
|
||||||
|
Adjust `/dev/ttyACM0` to match your system.
|
||||||
|
|
||||||
### Manual
|
### Manual
|
||||||
|
|
||||||
See [Development](#development) below.
|
See [Development quick start](#development-quick-start) below.
|
||||||
|
|
||||||
## Development
|
---
|
||||||
|
|
||||||
|
## Documentation
|
||||||
|
|
||||||
|
- [User Guide](./USER_GUIDE.md)
|
||||||
|
- [Web server usage](./docs/webserver.md)
|
||||||
|
- [Web server endpoints](./docs/webserver-endpoints.md)
|
||||||
|
- [Project scope](./SCOPE.md)
|
||||||
|
- [Contributing docs](./docs/contributing/README.md)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Development quick start
|
||||||
|
|
||||||
### Prerequisites
|
### Prerequisites
|
||||||
|
|
||||||
* **PlatformIO Core** (`pio`) or **VS Code + PlatformIO IDE**
|
- [pioarduino](https://github.com/pioarduino/pioarduino) or VS Code + pioarduino plugin
|
||||||
* Python 3.8+
|
- Python 3.8+
|
||||||
* USB-C cable for flashing the ESP32-C3
|
- `clang-format` 21
|
||||||
* Xteink X4
|
- USB-C cable supporting data transfer
|
||||||
|
|
||||||
### Checking out the code
|
### Setup
|
||||||
|
|
||||||
CrossPoint uses PlatformIO for building and flashing the firmware. To get started, clone the repository:
|
```bash
|
||||||
|
|
||||||
```
|
|
||||||
git clone --recursive https://github.com/crosspoint-reader/crosspoint-reader
|
git clone --recursive https://github.com/crosspoint-reader/crosspoint-reader
|
||||||
|
cd crosspoint-reader
|
||||||
|
|
||||||
# Or, if you've already cloned without --recursive:
|
# if cloned without --recursive:
|
||||||
git submodule update --init --recursive
|
git submodule update --init --recursive
|
||||||
```
|
```
|
||||||
|
|
||||||
### Flashing your device
|
### Build / flash / monitor
|
||||||
|
|
||||||
Connect your Xteink X4 to your computer via USB-C and run the following command.
|
```bash
|
||||||
|
|
||||||
```sh
|
|
||||||
pio run --target upload
|
pio run --target upload
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Contributor pre-PR checks
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./bin/clang-format-fix
|
||||||
|
pio check -e default
|
||||||
|
pio run -e default
|
||||||
|
```
|
||||||
|
|
||||||
### Debugging
|
### Debugging
|
||||||
|
|
||||||
After flashing the new features, it’s recommended to capture detailed logs from the serial port.
|
After flashing the new features, it’s recommended to capture detailed logs from the serial port.
|
||||||
@@ -126,7 +173,9 @@ First, make sure all required Python packages are installed:
|
|||||||
```python
|
```python
|
||||||
python3 -m pip install pyserial colorama matplotlib
|
python3 -m pip install pyserial colorama matplotlib
|
||||||
```
|
```
|
||||||
after that run the script:
|
|
||||||
|
After that run the script:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
# For Linux
|
# For Linux
|
||||||
# This was tested on Debian and should work on most Linux systems.
|
# This was tested on Debian and should work on most Linux systems.
|
||||||
@@ -135,63 +184,72 @@ python3 scripts/debugging_monitor.py
|
|||||||
# For macOS
|
# For macOS
|
||||||
python3 scripts/debugging_monitor.py /dev/cu.usbmodem2101
|
python3 scripts/debugging_monitor.py /dev/cu.usbmodem2101
|
||||||
```
|
```
|
||||||
|
|
||||||
Minor adjustments may be required for Windows.
|
Minor adjustments may be required for Windows.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Internals
|
## Internals
|
||||||
|
|
||||||
CrossPoint Reader is pretty aggressive about caching data down to the SD card to minimise RAM usage. The ESP32-C3 only
|
CrossPoint Reader is pretty aggressive about caching data down to the SD card to minimise RAM usage. The ESP32-C3 only has ~380KB of usable RAM, so we have to be careful. A lot of the decisions made in the design of the firmware were based on this constraint.
|
||||||
has ~380KB of usable RAM, so we have to be careful. A lot of the decisions made in the design of the firmware were based
|
|
||||||
on this constraint.
|
|
||||||
|
|
||||||
### Data caching
|
### Data caching
|
||||||
|
|
||||||
The first time chapters of a book are loaded, they are cached to the SD card. Subsequent loads are served from the
|
The first time chapters of a book are loaded, they are cached to the SD card. Subsequent loads are served from the
|
||||||
cache. This cache directory exists at `.crosspoint` on the SD card. The structure is as follows:
|
cache. This cache directory exists at `.crosspoint` on the SD card. The structure is as follows:
|
||||||
|
|
||||||
|
```text
|
||||||
```
|
|
||||||
.crosspoint/
|
.crosspoint/
|
||||||
├── epub_12471232/ # Each EPUB is cached to a subdirectory named `epub_<hash>`
|
├── epub_<hash>/ # one directory per book, named by content hash
|
||||||
│ ├── progress.bin # Stores reading progress (chapter, page, etc.)
|
│ ├── progress.bin # reading position (chapter, page, etc.)
|
||||||
│ ├── cover.bmp # Book cover image (once generated)
|
│ ├── cover.bmp # generated cover image
|
||||||
│ ├── book.bin # Book metadata (title, author, spine, table of contents, etc.)
|
│ ├── book.bin # metadata: title, author, spine, TOC
|
||||||
│ └── sections/ # All chapter data is stored in the sections subdirectory
|
│ └── sections/ # per-chapter layout cache
|
||||||
│ ├── 0.bin # Chapter data (screen count, all text layout info, etc.)
|
│ ├── 0.bin
|
||||||
│ ├── 1.bin # files are named by their index in the spine
|
│ ├── 1.bin
|
||||||
│ └── ...
|
│ └── ...
|
||||||
│
|
|
||||||
└── epub_189013891/
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Deleting the `.crosspoint` directory will clear the entire cache.
|
Removing `/.crosspoint` clears all cached metadata and forces a full regeneration on next open. Note: the cache isn't cleared automatically when you delete a book, and moving a file to a new path resets its reading progress.
|
||||||
|
|
||||||
Due the way it's currently implemented, the cache is not automatically cleared when a book is deleted and moving a book
|
|
||||||
file will use a new cache directory, resetting the reading progress.
|
|
||||||
|
|
||||||
For more details on the internal file structures, see the [file formats document](./docs/file-formats.md).
|
For more details on the internal file structures, see the [file formats document](./docs/file-formats.md).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
|
|
||||||
Contributions are very welcome!
|
Contributions are welcome. If you're new to the codebase, start with the [contributing docs](./docs/contributing/README.md). For things to work on, check the [ideas discussion board](https://github.com/crosspoint-reader/crosspoint-reader/discussions/categories/ideas) — leave a comment before starting so we don't duplicate effort.
|
||||||
|
|
||||||
If you are new to the codebase, start with the [contributing docs](./docs/contributing/README.md).
|
Everyone here is a volunteer, so please be respectful and patient. For governance and community expectations, see [GOVERNANCE.md](./GOVERNANCE.md).
|
||||||
|
|
||||||
If you're looking for a way to help out, take a look at the [ideas discussion board](https://github.com/crosspoint-reader/crosspoint-reader/discussions/categories/ideas).
|
|
||||||
If there's something there you'd like to work on, leave a comment so that we can avoid duplicated effort.
|
|
||||||
|
|
||||||
Everyone here is a volunteer, so please be respectful and patient. For more details on our governance and community
|
|
||||||
principles, please see [GOVERNANCE.md](GOVERNANCE.md).
|
|
||||||
|
|
||||||
### To submit a contribution:
|
|
||||||
|
|
||||||
1. Fork the repo
|
|
||||||
2. Create a branch (`feature/dithering-improvement`)
|
|
||||||
3. Make changes
|
|
||||||
4. Submit a PR
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
CrossPoint Reader is **not affiliated with Xteink or any manufacturer of the X4 hardware**.
|
## Community forks
|
||||||
|
|
||||||
Huge shoutout to [**diy-esp32-epub-reader** by atomic14](https://github.com/atomic14/diy-esp32-epub-reader), which was a project I took a lot of inspiration from as I
|
One of the best things about open source is that anyone can take the code in a different direction. If you need something outside CrossPoint's [scope](./SCOPE.md), check out the community forks:
|
||||||
was making CrossPoint.
|
|
||||||
|
- [CrossInk](https://github.com/uxjulia/CrossInk) — Typography and reading tracking: Bionic Reading (bolds word stems to create fixation points), guide dots between words, improved paragraph indents, and replaces the default fonts with ChareInk/Lexend/Bitter.
|
||||||
|
|
||||||
|
- [papyrix-reader](https://github.com/bigbag/papyrix-reader) — Adds FB2 and MD format support. Actively maintained with Arabic script support. Custom themes via SD card.
|
||||||
|
|
||||||
|
- [crosspet](https://github.com/trilwu/crosspet) — A Vietnamese fork that adds a Tamagotchi-style virtual chicken that grows based on your reading milestones (pages read, streaks, care). Also: Flashcards, Weather, Pomodoro timer, and mini-games.
|
||||||
|
|
||||||
|
- [crosspoint-reader (jpirnay)](https://github.com/jpirnay/crosspoint-reader) — Faster integration of functionality. Tracks upstream PRs and integrates the good ones ahead of the official merge.
|
||||||
|
|
||||||
|
- [crosspoint-reader-cjk](https://github.com/aBER0724/crosspoint-reader-cjk) — Purpose-built for Chinese, Japanese, and Korean reading.
|
||||||
|
|
||||||
|
- [inx](https://github.com/obijuankenobiii/inx) — Completely reimagines the user interface with tabbed navigation.
|
||||||
|
|
||||||
|
- ~~[PlusPoint](https://github.com/ngxson/pluspoint-reader) — custom JS apps support.~~ (Unmaintained)
|
||||||
|
|
||||||
|
- [crosspoint-reader-papers3](https://github.com/juicecultus/crosspoint-reader-papers3) — Crosspoint port for M5Stack Paper S3.
|
||||||
|
|
||||||
|
**Note:** Many of these features will make their way into CrossPoint over time. We maintain a slower pace to ensure rock-solid stability and squash bugs before they reach your device.
|
||||||
|
|
||||||
|
Want to build your own device? Be sure to check out the [de-link](https://github.com/iandchasse/de-link) project.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
CrossPoint Reader is **not affiliated with Xteink or any device manufacturer**.
|
||||||
|
|
||||||
|
Huge shoutout to [diy-esp32-epub-reader](https://github.com/atomic14/diy-esp32-epub-reader), which inspired this project.
|
||||||
|
|||||||
+121
-67
@@ -9,16 +9,20 @@
|
|||||||
#include <cstring>
|
#include <cstring>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
|
|
||||||
|
#include "EpdFontFamily.h"
|
||||||
|
|
||||||
static_assert(sizeof(EpdGlyph) == 16, "EpdGlyph must be 16 bytes to match .cpfont file layout");
|
static_assert(sizeof(EpdGlyph) == 16, "EpdGlyph must be 16 bytes to match .cpfont file layout");
|
||||||
static_assert(sizeof(EpdUnicodeInterval) == 12, "EpdUnicodeInterval must be 12 bytes to match .cpfont file layout");
|
static_assert(sizeof(EpdUnicodeInterval) == 12, "EpdUnicodeInterval must be 12 bytes to match .cpfont file layout");
|
||||||
static_assert(sizeof(EpdKernClassEntry) == 3, "EpdKernClassEntry must be 3 bytes to match .cpfont file layout");
|
static_assert(sizeof(EpdKernClassEntry) == 3, "EpdKernClassEntry must be 3 bytes to match .cpfont file layout");
|
||||||
static_assert(sizeof(EpdLigaturePair) == 8, "EpdLigaturePair must be 8 bytes to match .cpfont file layout");
|
static_assert(sizeof(EpdLigaturePair) == 8, "EpdLigaturePair must be 8 bytes to match .cpfont file layout");
|
||||||
|
|
||||||
// FNV-1a hash for content-based font ID generation
|
namespace {
|
||||||
static constexpr uint32_t FNV_OFFSET = 2166136261u;
|
|
||||||
static constexpr uint32_t FNV_PRIME = 16777619u;
|
|
||||||
|
|
||||||
static uint32_t fnv1a(const uint8_t* data, size_t len, uint32_t hash = FNV_OFFSET) {
|
// FNV-1a hash for content-based font ID generation
|
||||||
|
constexpr uint32_t FNV_OFFSET = 2166136261u;
|
||||||
|
constexpr uint32_t FNV_PRIME = 16777619u;
|
||||||
|
|
||||||
|
uint32_t fnv1a(const uint8_t* data, size_t len, uint32_t hash = FNV_OFFSET) {
|
||||||
for (size_t i = 0; i < len; i++) {
|
for (size_t i = 0; i < len; i++) {
|
||||||
hash ^= data[i];
|
hash ^= data[i];
|
||||||
hash *= FNV_PRIME;
|
hash *= FNV_PRIME;
|
||||||
@@ -27,16 +31,44 @@ static uint32_t fnv1a(const uint8_t* data, size_t len, uint32_t hash = FNV_OFFSE
|
|||||||
}
|
}
|
||||||
|
|
||||||
// .cpfont magic bytes
|
// .cpfont magic bytes
|
||||||
static constexpr char CPFONT_MAGIC[8] = {'C', 'P', 'F', 'O', 'N', 'T', '\0', '\0'};
|
constexpr char CPFONT_MAGIC[8] = {'C', 'P', 'F', 'O', 'N', 'T', '\0', '\0'};
|
||||||
// CPFONT_VERSION is defined as a #define in SdCardFont.h so it can be
|
// CPFONT_VERSION is defined as a #define in SdCardFont.h so it can be
|
||||||
// stringified into FONT_MANIFEST_URL.
|
// stringified into FONT_MANIFEST_URL.
|
||||||
static constexpr uint32_t HEADER_SIZE = 32;
|
constexpr uint32_t HEADER_SIZE = 32;
|
||||||
static constexpr uint32_t STYLE_TOC_ENTRY_SIZE = 32;
|
constexpr uint32_t STYLE_TOC_ENTRY_SIZE = 32;
|
||||||
|
|
||||||
// Helper to read little-endian values from byte buffer
|
// Helper to read little-endian values from byte buffer
|
||||||
static inline uint16_t readU16(const uint8_t* p) { return p[0] | (p[1] << 8); }
|
inline uint16_t readU16(const uint8_t* p) { return p[0] | (p[1] << 8); }
|
||||||
static inline int16_t readI16(const uint8_t* p) { return static_cast<int16_t>(p[0] | (p[1] << 8)); }
|
inline int16_t readI16(const uint8_t* p) { return static_cast<int16_t>(p[0] | (p[1] << 8)); }
|
||||||
static inline uint32_t readU32(const uint8_t* p) { return p[0] | (p[1] << 8) | (p[2] << 16) | (p[3] << 24); }
|
inline uint32_t readU32(const uint8_t* p) { return p[0] | (p[1] << 8) | (p[2] << 16) | (p[3] << 24); }
|
||||||
|
|
||||||
|
// Walks a null-terminated UTF-8 string and appends each unique codepoint to
|
||||||
|
// codepoints[0..cpCount-1] via O(n²) dedup. Returns true if the buffer
|
||||||
|
// reached maxCount (cap hit), false if all codepoints fit.
|
||||||
|
bool collectUniqueCodepoints(const char* text, uint32_t* codepoints, uint32_t& cpCount, uint32_t maxCount) {
|
||||||
|
const unsigned char* p = reinterpret_cast<const unsigned char*>(text);
|
||||||
|
while (*p) {
|
||||||
|
uint32_t cp = utf8NextCodepoint(&p);
|
||||||
|
if (cp == 0) break;
|
||||||
|
bool found = false;
|
||||||
|
for (uint32_t i = 0; i < cpCount; i++) {
|
||||||
|
if (codepoints[i] == cp) {
|
||||||
|
found = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!found) {
|
||||||
|
if (cpCount >= maxCount) return true;
|
||||||
|
codepoints[cpCount++] = cp;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const char* asCStr(const std::string& s) { return s.c_str(); }
|
||||||
|
const char* asCStr(const char* s) { return s; }
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
SdCardFont::~SdCardFont() { freeAll(); }
|
SdCardFont::~SdCardFont() { freeAll(); }
|
||||||
|
|
||||||
@@ -587,6 +619,8 @@ int32_t SdCardFont::findGlobalGlyphIndex(const PerStyle& s, uint32_t codepoint)
|
|||||||
|
|
||||||
int SdCardFont::prewarm(const char* utf8Text, uint8_t styleMask, bool metadataOnly) {
|
int SdCardFont::prewarm(const char* utf8Text, uint8_t styleMask, bool metadataOnly) {
|
||||||
if (!loaded_) return -1;
|
if (!loaded_) return -1;
|
||||||
|
styleMask = resolveStyleMask(styleMask);
|
||||||
|
if (styleMask == 0) return 0;
|
||||||
|
|
||||||
unsigned long startMs = millis();
|
unsigned long startMs = millis();
|
||||||
|
|
||||||
@@ -1015,62 +1049,10 @@ uint16_t SdCardFont::getAdvance(uint32_t codepoint, uint8_t style) const {
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
int SdCardFont::buildAdvanceTable(const char* utf8Text, uint8_t styleMask) {
|
// Given a sorted array of unique codepoints, resolve glyph indices per style,
|
||||||
if (!loaded_) return -1;
|
// batch-read advanceX from SD, and merge into the persistent advance table.
|
||||||
|
// Caller owns the codepoints buffer.
|
||||||
// Note: advance table is preserved across calls. We only fetch codepoints
|
int SdCardFont::fetchAdvancesForCodepoints(uint32_t* codepoints, uint32_t cpCount, uint8_t styleMask) {
|
||||||
// not already present, then merge them in. Use clearPersistentCache() to
|
|
||||||
// wipe the table when the font/size/family changes.
|
|
||||||
|
|
||||||
unsigned long startMs = millis();
|
|
||||||
|
|
||||||
// Step 1: Extract unique codepoints, capped at MAX_UNIQUE_CODEPOINTS.
|
|
||||||
// The dedup buffer is sized to the cap, not total chars — a large EPUB section
|
|
||||||
// may contain 50K+ characters but real text has far fewer unique codepoints.
|
|
||||||
// 4096 × 4 bytes = 16KB temporary; bounded regardless of input size.
|
|
||||||
static constexpr uint32_t MAX_UNIQUE_CODEPOINTS = 4096;
|
|
||||||
uint32_t* codepoints = new (std::nothrow) uint32_t[MAX_UNIQUE_CODEPOINTS];
|
|
||||||
if (!codepoints) {
|
|
||||||
LOG_ERR("SDCF", "buildAdvanceTable: failed to allocate codepoint buffer (%u bytes)", MAX_UNIQUE_CODEPOINTS * 4);
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
uint32_t cpCount = 0;
|
|
||||||
bool hitCap = false;
|
|
||||||
|
|
||||||
// Second pass: collect unique codepoints via O(n²) dedup.
|
|
||||||
// Bounded by uniqueCount × totalChars comparisons. For 2000 unique from 2291 total,
|
|
||||||
// worst case ~4.6M comparisons of uint32_t — ~30ms on 160MHz RISC-V, acceptable
|
|
||||||
// for one-time section indexing.
|
|
||||||
const unsigned char* p = reinterpret_cast<const unsigned char*>(utf8Text);
|
|
||||||
while (*p) {
|
|
||||||
uint32_t cp = utf8NextCodepoint(&p);
|
|
||||||
if (cp == 0) break;
|
|
||||||
|
|
||||||
bool found = false;
|
|
||||||
for (uint32_t i = 0; i < cpCount; i++) {
|
|
||||||
if (codepoints[i] == cp) {
|
|
||||||
found = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!found) {
|
|
||||||
if (cpCount >= MAX_UNIQUE_CODEPOINTS) {
|
|
||||||
hitCap = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
codepoints[cpCount++] = cp;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (hitCap) {
|
|
||||||
LOG_ERR("SDCF", "buildAdvanceTable: unique codepoint cap (%u) hit, layout may be approximate",
|
|
||||||
MAX_UNIQUE_CODEPOINTS);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sort for ordered glyph index mapping and final table output
|
|
||||||
std::sort(codepoints, codepoints + cpCount);
|
|
||||||
|
|
||||||
// Step 2: For each requested style, fetch any codepoints not yet cached and
|
|
||||||
// merge them into the persistent advance table.
|
|
||||||
int totalMissed = 0;
|
int totalMissed = 0;
|
||||||
for (uint8_t si = 0; si < MAX_STYLES; si++) {
|
for (uint8_t si = 0; si < MAX_STYLES; si++) {
|
||||||
if (!(styleMask & (1 << si)) || !styles_[si].present) continue;
|
if (!(styleMask & (1 << si)) || !styles_[si].present) continue;
|
||||||
@@ -1165,12 +1147,55 @@ int SdCardFont::buildAdvanceTable(const char* utf8Text, uint8_t styleMask) {
|
|||||||
ADVANCE_CACHE_LIMIT);
|
ADVANCE_CACHE_LIMIT);
|
||||||
}
|
}
|
||||||
|
|
||||||
delete[] codepoints;
|
return totalMissed;
|
||||||
|
}
|
||||||
|
|
||||||
|
template <typename Iter>
|
||||||
|
int SdCardFont::buildAdvanceTableRange(Iter begin, Iter end, bool includeSpace, bool includeHyphen, uint8_t styleMask) {
|
||||||
|
if (!loaded_) return -1;
|
||||||
|
styleMask = resolveStyleMask(styleMask);
|
||||||
|
if (styleMask == 0) return 0;
|
||||||
|
|
||||||
|
unsigned long startMs = millis();
|
||||||
|
|
||||||
|
// +2 reserved slots for space and hyphen injected after the main scan.
|
||||||
|
static constexpr uint32_t MAX_UNIQUE_CODEPOINTS = 4096;
|
||||||
|
uint32_t* codepoints = new (std::nothrow) uint32_t[MAX_UNIQUE_CODEPOINTS + 2];
|
||||||
|
if (!codepoints) {
|
||||||
|
LOG_ERR("SDCF", "buildAdvanceTable: failed to allocate codepoint buffer (%u bytes)", MAX_UNIQUE_CODEPOINTS * 4);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
uint32_t cpCount = 0;
|
||||||
|
bool hitCap = false;
|
||||||
|
|
||||||
|
for (auto it = begin; it != end && !hitCap; ++it) {
|
||||||
|
hitCap = collectUniqueCodepoints(asCStr(*it), codepoints, cpCount, MAX_UNIQUE_CODEPOINTS);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (includeSpace && std::none_of(codepoints, codepoints + cpCount, [](uint32_t c) { return c == ' '; }))
|
||||||
|
codepoints[cpCount++] = ' ';
|
||||||
|
if (includeHyphen && std::none_of(codepoints, codepoints + cpCount, [](uint32_t c) { return c == '-'; }))
|
||||||
|
codepoints[cpCount++] = '-';
|
||||||
|
|
||||||
|
if (hitCap) {
|
||||||
|
LOG_ERR("SDCF", "buildAdvanceTable: unique codepoint cap (%u) hit, layout may be approximate",
|
||||||
|
MAX_UNIQUE_CODEPOINTS);
|
||||||
|
}
|
||||||
|
std::sort(codepoints, codepoints + cpCount);
|
||||||
|
int totalMissed = fetchAdvancesForCodepoints(codepoints, cpCount, styleMask);
|
||||||
|
delete[] codepoints;
|
||||||
stats_.prewarmTotalMs = millis() - startMs;
|
stats_.prewarmTotalMs = millis() - startMs;
|
||||||
return totalMissed;
|
return totalMissed;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int SdCardFont::buildAdvanceTable(const char* utf8Text, uint8_t styleMask) {
|
||||||
|
return buildAdvanceTableRange(&utf8Text, &utf8Text + 1, false, false, styleMask);
|
||||||
|
}
|
||||||
|
|
||||||
|
int SdCardFont::buildAdvanceTable(const std::vector<std::string>& words, bool includeHyphen, uint8_t styleMask) {
|
||||||
|
return buildAdvanceTableRange(words.begin(), words.end(), words.size() > 1, includeHyphen, styleMask);
|
||||||
|
}
|
||||||
|
|
||||||
// --- Stats ---
|
// --- Stats ---
|
||||||
|
|
||||||
void SdCardFont::logStats(const char* label) {
|
void SdCardFont::logStats(const char* label) {
|
||||||
@@ -1190,6 +1215,35 @@ EpdFont* SdCardFont::getEpdFont(uint8_t style) {
|
|||||||
|
|
||||||
bool SdCardFont::hasStyle(uint8_t style) const { return styles_[style & (MAX_STYLES - 1)].present; }
|
bool SdCardFont::hasStyle(uint8_t style) const { return styles_[style & (MAX_STYLES - 1)].present; }
|
||||||
|
|
||||||
|
uint8_t SdCardFont::resolveStyle(uint8_t style) const {
|
||||||
|
static const uint8_t kFallbacks[MAX_STYLES][MAX_STYLES] = {
|
||||||
|
// REGULAR: REGULAR -> BOLD -> ITALIC -> BOLD_ITALIC
|
||||||
|
{EpdFontFamily::REGULAR, EpdFontFamily::BOLD, EpdFontFamily::ITALIC, EpdFontFamily::BOLD_ITALIC},
|
||||||
|
// BOLD: BOLD -> REGULAR -> BOLD_ITALIC -> ITALIC
|
||||||
|
{EpdFontFamily::BOLD, EpdFontFamily::REGULAR, EpdFontFamily::BOLD_ITALIC, EpdFontFamily::ITALIC},
|
||||||
|
// ITALIC: ITALIC -> REGULAR -> BOLD_ITALIC -> BOLD
|
||||||
|
{EpdFontFamily::ITALIC, EpdFontFamily::REGULAR, EpdFontFamily::BOLD_ITALIC, EpdFontFamily::BOLD},
|
||||||
|
// BOLD_ITALIC: BOLD_ITALIC -> BOLD -> ITALIC -> REGULAR
|
||||||
|
{EpdFontFamily::BOLD_ITALIC, EpdFontFamily::BOLD, EpdFontFamily::ITALIC, EpdFontFamily::REGULAR},
|
||||||
|
};
|
||||||
|
|
||||||
|
const uint8_t styleBits = style & (MAX_STYLES - 1);
|
||||||
|
for (uint8_t candidate : kFallbacks[styleBits]) {
|
||||||
|
if (styles_[candidate].present) return candidate;
|
||||||
|
}
|
||||||
|
return EpdFontFamily::REGULAR;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t SdCardFont::resolveStyleMask(uint8_t styleMask) const {
|
||||||
|
uint8_t resolvedMask = 0;
|
||||||
|
for (uint8_t si = 0; si < MAX_STYLES; si++) {
|
||||||
|
if (styleMask & (1 << si)) {
|
||||||
|
resolvedMask |= static_cast<uint8_t>(1u << resolveStyle(si));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return resolvedMask;
|
||||||
|
}
|
||||||
|
|
||||||
// --- On-demand glyph loading (overflow buffer) ---
|
// --- On-demand glyph loading (overflow buffer) ---
|
||||||
|
|
||||||
const EpdGlyph* SdCardFont::onGlyphMiss(void* ctx, uint32_t codepoint) {
|
const EpdGlyph* SdCardFont::onGlyphMiss(void* ctx, uint32_t codepoint) {
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
#include "EpdFont.h"
|
#include "EpdFont.h"
|
||||||
#include "EpdFontData.h"
|
#include "EpdFontData.h"
|
||||||
@@ -43,10 +45,11 @@ class SdCardFont {
|
|||||||
int prewarm(const char* utf8Text, uint8_t styleMask = 0x0F, bool metadataOnly = false);
|
int prewarm(const char* utf8Text, uint8_t styleMask = 0x0F, bool metadataOnly = false);
|
||||||
|
|
||||||
// Build a compact advance-only table for layout measurement.
|
// Build a compact advance-only table for layout measurement.
|
||||||
// Extracts ALL unique codepoints from utf8Text (no MAX_PAGE_GLYPHS cap),
|
// Extracts ALL unique codepoints from words (no MAX_PAGE_GLYPHS cap),
|
||||||
// batch-reads advanceX from SD, stores in a sorted per-style table.
|
// batch-reads advanceX from SD, stores in a sorted per-style table.
|
||||||
// Returns number of codepoints not found in font coverage.
|
// Returns number of codepoints not found in font coverage.
|
||||||
int buildAdvanceTable(const char* utf8Text, uint8_t styleMask = 0x0F);
|
int buildAdvanceTable(const char* utf8Text, uint8_t styleMask = 0x0F);
|
||||||
|
int buildAdvanceTable(const std::vector<std::string>& words, bool includeHyphen, uint8_t styleMask = 0x0F);
|
||||||
|
|
||||||
// Look up advanceX for a codepoint from the advance table.
|
// Look up advanceX for a codepoint from the advance table.
|
||||||
// Returns the 12.4 fixed-point advance, or 0 if not found.
|
// Returns the 12.4 fixed-point advance, or 0 if not found.
|
||||||
@@ -71,6 +74,13 @@ class SdCardFont {
|
|||||||
// Returns true if the given style is present in this font file.
|
// Returns true if the given style is present in this font file.
|
||||||
bool hasStyle(uint8_t style) const;
|
bool hasStyle(uint8_t style) const;
|
||||||
|
|
||||||
|
// Resolve requested style bits to the closest present style.
|
||||||
|
uint8_t resolveStyle(uint8_t style) const;
|
||||||
|
|
||||||
|
// Resolve every requested style bit through fallback and return the actual
|
||||||
|
// styles that need cache/advance preparation.
|
||||||
|
uint8_t resolveStyleMask(uint8_t styleMask) const;
|
||||||
|
|
||||||
// Number of styles present in this font file.
|
// Number of styles present in this font file.
|
||||||
uint8_t styleCount() const { return styleCount_; }
|
uint8_t styleCount() const { return styleCount_; }
|
||||||
|
|
||||||
@@ -229,6 +239,9 @@ class SdCardFont {
|
|||||||
void applyKernLigaturePointers(PerStyle& s, EpdFontData& data) const;
|
void applyKernLigaturePointers(PerStyle& s, EpdFontData& data) const;
|
||||||
void applyGlyphMissCallback(uint8_t styleIdx);
|
void applyGlyphMissCallback(uint8_t styleIdx);
|
||||||
int32_t findGlobalGlyphIndex(const PerStyle& s, uint32_t codepoint) const;
|
int32_t findGlobalGlyphIndex(const PerStyle& s, uint32_t codepoint) const;
|
||||||
|
int fetchAdvancesForCodepoints(uint32_t* codepoints, uint32_t cpCount, uint8_t styleMask);
|
||||||
|
template <typename Iter>
|
||||||
|
int buildAdvanceTableRange(Iter begin, Iter end, bool includeSpace, bool includeHyphen, uint8_t styleMask);
|
||||||
int prewarmStyle(uint8_t styleIdx, const uint32_t* codepoints, uint32_t cpCount, bool metadataOnly);
|
int prewarmStyle(uint8_t styleIdx, const uint32_t* codepoints, uint32_t cpCount, bool metadataOnly);
|
||||||
|
|
||||||
// Global helpers
|
// Global helpers
|
||||||
|
|||||||
@@ -255,20 +255,6 @@ void ParsedText::layoutAndExtractLines(const GfxRenderer& renderer, const int fo
|
|||||||
// (advanceX only, no bitmaps) for all unique codepoints in this paragraph so
|
// (advanceX only, no bitmaps) for all unique codepoints in this paragraph so
|
||||||
// that calculateWordWidths() can measure text without on-demand SD I/O.
|
// that calculateWordWidths() can measure text without on-demand SD I/O.
|
||||||
if (renderer.isSdCardFont(fontId)) {
|
if (renderer.isSdCardFont(fontId)) {
|
||||||
// Reserve upfront so the joined text allocates exactly once. Without this,
|
|
||||||
// paragraphs with many words trigger a chain of vector-like reallocations
|
|
||||||
// inside std::string during layout — visible in prewarm timings for SD fonts.
|
|
||||||
size_t totalSize = hyphenationEnabled ? 1 : 0;
|
|
||||||
if (!words.empty()) totalSize += words.size() - 1; // inter-word spaces
|
|
||||||
for (const auto& w : words) totalSize += w.size();
|
|
||||||
std::string allText;
|
|
||||||
allText.reserve(totalSize);
|
|
||||||
for (size_t i = 0; i < words.size(); i++) {
|
|
||||||
if (i > 0) allText += ' ';
|
|
||||||
allText += words[i];
|
|
||||||
}
|
|
||||||
if (hyphenationEnabled) allText += '-';
|
|
||||||
|
|
||||||
// Style mask: only ask the SD font to load advances for styles actually
|
// Style mask: only ask the SD font to load advances for styles actually
|
||||||
// used in this paragraph. Style index is the low two bits (regular/bold/
|
// used in this paragraph. Style index is the low two bits (regular/bold/
|
||||||
// italic/bold-italic); the underline bit is irrelevant to advance metrics.
|
// italic/bold-italic); the underline bit is irrelevant to advance metrics.
|
||||||
@@ -277,7 +263,7 @@ void ParsedText::layoutAndExtractLines(const GfxRenderer& renderer, const int fo
|
|||||||
styleMask |= static_cast<uint8_t>(1u << (static_cast<uint8_t>(s) & 0x03));
|
styleMask |= static_cast<uint8_t>(1u << (static_cast<uint8_t>(s) & 0x03));
|
||||||
}
|
}
|
||||||
if (styleMask == 0) styleMask = 0x01; // defensive: regular only
|
if (styleMask == 0) styleMask = 0x01; // defensive: regular only
|
||||||
renderer.ensureSdCardFontReady(fontId, allText.c_str(), styleMask);
|
renderer.ensureSdCardFontReady(fontId, words, hyphenationEnabled, styleMask);
|
||||||
}
|
}
|
||||||
|
|
||||||
const int pageWidth = viewportWidth;
|
const int pageWidth = viewportWidth;
|
||||||
|
|||||||
@@ -19,23 +19,7 @@ const char* resolveVisualText(const char* text, std::string& visualBuffer, int p
|
|||||||
* Falls back gracefully when the font lacks the requested variant.
|
* Falls back gracefully when the font lacks the requested variant.
|
||||||
*/
|
*/
|
||||||
uint8_t resolveSdCardStyle(const SdCardFont& font, const EpdFontFamily::Style style) {
|
uint8_t resolveSdCardStyle(const SdCardFont& font, const EpdFontFamily::Style style) {
|
||||||
// Indexed by styleBits (0=REGULAR, 1=BOLD, 2=ITALIC, 3=BOLD_ITALIC)
|
return font.resolveStyle(static_cast<uint8_t>(style));
|
||||||
static const uint8_t kFallbacks[4][4] = {
|
|
||||||
// REGULAR: REGULAR → BOLD → ITALIC → BOLD_ITALIC
|
|
||||||
{EpdFontFamily::REGULAR, EpdFontFamily::BOLD, EpdFontFamily::ITALIC, EpdFontFamily::BOLD_ITALIC},
|
|
||||||
// BOLD: BOLD → BOLD_ITALIC → REGULAR → ITALIC
|
|
||||||
{EpdFontFamily::BOLD, EpdFontFamily::BOLD_ITALIC, EpdFontFamily::REGULAR, EpdFontFamily::ITALIC},
|
|
||||||
// ITALIC: ITALIC → REGULAR → BOLD → BOLD_ITALIC (REGULAR before BOLD!)
|
|
||||||
{EpdFontFamily::ITALIC, EpdFontFamily::REGULAR, EpdFontFamily::BOLD, EpdFontFamily::BOLD_ITALIC},
|
|
||||||
// BOLD_ITALIC: BOLD_ITALIC → BOLD → ITALIC → REGULAR
|
|
||||||
{EpdFontFamily::BOLD_ITALIC, EpdFontFamily::BOLD, EpdFontFamily::ITALIC, EpdFontFamily::REGULAR},
|
|
||||||
};
|
|
||||||
|
|
||||||
const uint8_t styleBits = static_cast<uint8_t>(style) & 0x03;
|
|
||||||
for (uint8_t candidate : kFallbacks[styleBits]) {
|
|
||||||
if (font.hasStyle(candidate)) return candidate;
|
|
||||||
}
|
|
||||||
return EpdFontFamily::REGULAR; // no-variant-at-all safety net
|
|
||||||
}
|
}
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
@@ -69,11 +53,22 @@ const uint8_t* GfxRenderer::getGlyphBitmap(const EpdFontData* fontData, const Ep
|
|||||||
|
|
||||||
void GfxRenderer::ensureSdCardFontReady(int fontId, const char* utf8Text, uint8_t styleMask) const {
|
void GfxRenderer::ensureSdCardFontReady(int fontId, const char* utf8Text, uint8_t styleMask) const {
|
||||||
auto it = sdCardFonts_.find(fontId);
|
auto it = sdCardFonts_.find(fontId);
|
||||||
|
if (it != sdCardFonts_.end()) {
|
||||||
|
int missed = it->second->buildAdvanceTable(utf8Text, styleMask);
|
||||||
|
if (missed > 0) {
|
||||||
|
LOG_DBG("GFX", "ensureSdCardFontReady: %d glyph(s) not found", missed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void GfxRenderer::ensureSdCardFontReady(int fontId, const std::vector<std::string>& words, bool includeHyphen,
|
||||||
|
uint8_t styleMask) const {
|
||||||
|
auto it = sdCardFonts_.find(fontId);
|
||||||
if (it != sdCardFonts_.end()) {
|
if (it != sdCardFonts_.end()) {
|
||||||
// Augment the persistent advance-only table for layout measurement.
|
// Augment the persistent advance-only table for layout measurement.
|
||||||
// The table survives across paragraphs/sections (capped per font), so
|
// The table survives across paragraphs/sections (capped per font), so
|
||||||
// repeated indexing of the same SD font amortizes glyph-metric SD reads.
|
// repeated indexing of the same SD font amortizes glyph-metric SD reads.
|
||||||
int missed = it->second->buildAdvanceTable(utf8Text, styleMask);
|
int missed = it->second->buildAdvanceTable(words, includeHyphen, styleMask);
|
||||||
if (missed > 0) {
|
if (missed > 0) {
|
||||||
LOG_DBG("GFX", "ensureSdCardFontReady: %d glyph(s) not found", missed);
|
LOG_DBG("GFX", "ensureSdCardFontReady: %d glyph(s) not found", missed);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -94,6 +94,8 @@ class GfxRenderer {
|
|||||||
// (which holds a const GfxRenderer&) before measuring word widths. Safe to call on non-SD fonts (no-op).
|
// (which holds a const GfxRenderer&) before measuring word widths. Safe to call on non-SD fonts (no-op).
|
||||||
// styleMask: bitmask of styles to prepare (bit 0=regular, 1=bold, 2=italic, 3=bold-italic).
|
// styleMask: bitmask of styles to prepare (bit 0=regular, 1=bold, 2=italic, 3=bold-italic).
|
||||||
void ensureSdCardFontReady(int fontId, const char* utf8Text, uint8_t styleMask = 0x0F) const;
|
void ensureSdCardFontReady(int fontId, const char* utf8Text, uint8_t styleMask = 0x0F) const;
|
||||||
|
void ensureSdCardFontReady(int fontId, const std::vector<std::string>& words, bool includeHyphen,
|
||||||
|
uint8_t styleMask = 0x0F) const;
|
||||||
|
|
||||||
// Orientation control (affects logical width/height and coordinate transforms)
|
// Orientation control (affects logical width/height and coordinate transforms)
|
||||||
void setOrientation(const Orientation o) { orientation = o; }
|
void setOrientation(const Orientation o) { orientation = o; }
|
||||||
|
|||||||
+1
-1
@@ -4,7 +4,7 @@ build_cache_dir = .cache
|
|||||||
extra_configs = platformio.local.ini
|
extra_configs = platformio.local.ini
|
||||||
|
|
||||||
[crosspoint]
|
[crosspoint]
|
||||||
version = 1.2.0
|
version = 1.3.0
|
||||||
|
|
||||||
[base]
|
[base]
|
||||||
platform = https://github.com/pioarduino/platform-espressif32/releases/download/55.03.37/platform-espressif32.zip
|
platform = https://github.com/pioarduino/platform-espressif32/releases/download/55.03.37/platform-espressif32.zip
|
||||||
|
|||||||
@@ -192,6 +192,17 @@ bool TxtReaderActivity::loadPageAtOffset(size_t offset, std::vector<std::string>
|
|||||||
}
|
}
|
||||||
buffer[chunkSize] = '\0';
|
buffer[chunkSize] = '\0';
|
||||||
|
|
||||||
|
// Prime the SD card font's advance table with this chunk's codepoints.
|
||||||
|
// Without this, every getTextAdvanceX() call in the wrap loop below triggers
|
||||||
|
// on-demand glyph loads through the 8-slot overflow ring buffer, which
|
||||||
|
// thrashes for any text with more than 8 unique chars (i.e. all English),
|
||||||
|
// floods the heap with short-lived bitmap allocations, and eventually
|
||||||
|
// corrupts FreeRTOS state. The advance table persists across calls per
|
||||||
|
// font, so the cost amortizes to ~ASCII-size after the first chunk.
|
||||||
|
if (renderer.isSdCardFont(cachedFontId)) {
|
||||||
|
renderer.ensureSdCardFontReady(cachedFontId, reinterpret_cast<const char*>(buffer), /*styleMask=*/0x01);
|
||||||
|
}
|
||||||
|
|
||||||
// Parse lines from buffer
|
// Parse lines from buffer
|
||||||
size_t pos = 0;
|
size_t pos = 0;
|
||||||
|
|
||||||
@@ -231,7 +242,7 @@ bool TxtReaderActivity::loadPageAtOffset(size_t offset, std::vector<std::string>
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
int lineWidth = renderer.getTextWidth(cachedFontId, line.c_str());
|
int lineWidth = renderer.getTextAdvanceX(cachedFontId, line.c_str(), EpdFontFamily::REGULAR);
|
||||||
|
|
||||||
if (lineWidth <= viewportWidth) {
|
if (lineWidth <= viewportWidth) {
|
||||||
outLines.push_back(line);
|
outLines.push_back(line);
|
||||||
@@ -242,7 +253,8 @@ bool TxtReaderActivity::loadPageAtOffset(size_t offset, std::vector<std::string>
|
|||||||
|
|
||||||
// Find break point
|
// Find break point
|
||||||
size_t breakPos = line.length();
|
size_t breakPos = line.length();
|
||||||
while (breakPos > 0 && renderer.getTextWidth(cachedFontId, line.substr(0, breakPos).c_str()) > viewportWidth) {
|
while (breakPos > 0 && renderer.getTextAdvanceX(cachedFontId, line.substr(0, breakPos).c_str(),
|
||||||
|
EpdFontFamily::REGULAR) > viewportWidth) {
|
||||||
// Try to break at space
|
// Try to break at space
|
||||||
size_t spacePos = line.rfind(' ', breakPos - 1);
|
size_t spacePos = line.rfind(' ', breakPos - 1);
|
||||||
if (spacePos != std::string::npos && spacePos > 0) {
|
if (spacePos != std::string::npos && spacePos > 0) {
|
||||||
@@ -354,12 +366,12 @@ void TxtReaderActivity::renderPage() {
|
|||||||
// x already set to left margin
|
// x already set to left margin
|
||||||
break;
|
break;
|
||||||
case CrossPointSettings::CENTER_ALIGN: {
|
case CrossPointSettings::CENTER_ALIGN: {
|
||||||
int textWidth = renderer.getTextWidth(cachedFontId, line.c_str());
|
int textWidth = renderer.getTextAdvanceX(cachedFontId, line.c_str(), EpdFontFamily::REGULAR);
|
||||||
x = cachedOrientedMarginLeft + (contentWidth - textWidth) / 2;
|
x = cachedOrientedMarginLeft + (contentWidth - textWidth) / 2;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case CrossPointSettings::RIGHT_ALIGN: {
|
case CrossPointSettings::RIGHT_ALIGN: {
|
||||||
int textWidth = renderer.getTextWidth(cachedFontId, line.c_str());
|
int textWidth = renderer.getTextAdvanceX(cachedFontId, line.c_str(), EpdFontFamily::REGULAR);
|
||||||
x = cachedOrientedMarginLeft + contentWidth - textWidth;
|
x = cachedOrientedMarginLeft + contentWidth - textWidth;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user