Compare commits

..
Author SHA1 Message Date
Justin Mitchell a328fd912d Add SD catalog as default EPUB container backend
Changes the container backend strategy to use SD catalog by default for all EPUBs. On first open or stale index, builds the catalog once by borrowing the framebuffer (which can be released since e-ink holds the loading popup image). Subsequent opens use the fast path with compact resident tables. Falls back to legacy in-RAM open if catalog build fails due to IoError or OOM.
2026-07-08 14:08:15 -04:00
Justin Mitchell e3f2e27078 Add catalog-backed book cover extraction support
Implement cover extraction for books stored in catalog.fibc omnibus format to avoid loading large (~400KB) book arenas into RAM. Uses catalog's pre-resolved cover entries with minimal memory footprint (~42KB resident). Includes arena management for catalog opening and ZIP entry extraction with configurable scratch buffers.
2026-07-08 13:51:58 -04:00
Justin Mitchell 9bb488dc86 Add catalog mode for large EPUB containers
Implements a two-tier system for handling EPUB books: small books use in-RAM metadata (probe ladder with 48KB/72KB/96KB rungs), while large containers (e.g. webnovel omnibuses with 1500+ spine items) automatically fall back to SD-backed BookCatalog with persistent index. The catalog fast path reuses existing catalog.fibc files to skip probing on subsequent opens. Adds framebuffer borrowing for larger probe rungs and incremental chapter building for giant chapters.
2026-07-08 13:41:16 -04:00
Justin Mitchell c331651e4e Update freeink-sdk submodule 2026-07-08 05:43:38 -04:00
Justin Mitchell bfb8aacdae Add framebuffer release and font metadata access
Introduce methods to temporarily release the 48KB framebuffer during memory-intensive chapter builds, allowing the memory to be reused. Add font metadata query methods (hasGlyphMeta, ensureAdvance) that check glyph existence and fetch advance metrics without loading full bitmap data, avoiding expensive SD reads during layout measurement.
2026-07-08 04:46:25 -04:00
Justin Mitchell e027f60bf0 Optimize memory allocation in BookPaginator
Replace fixed-size arena buffers with two-pass allocation strategy. First pass parses book into temporary full-size arena to determine actual memory footprint (5-40 KB vs 48 KB cap), then reallocates exact-sized buffers. This reduces peak memory usage by avoiding simultaneous allocation of all arenas with chapter-build scratch buffer (~100 KB) on constrained heap (~140 KB free). Stylesheet is also compacted to actual size, and all open-time transients are freed before per-chapter arenas are allocated.
2026-07-08 02:17:29 -04:00
Justin Mitchell 78c71f1c77 Extract book cover utilities into BookCoverUtils
Refactors book cover and metadata handling out of Epub class into a dedicated BookCoverUtils utility. Moves FootnoteEntry struct into reader activity directory. Simplifies cache clearing to use direct storage operations instead of Epub class.
2026-07-08 01:31:24 -04:00
Justin Mitchell d364e54a69 Add KOReader XPath sync support for EPUB navigation
Implements bidirectional conversion between KOReader XPath positions and FreeInkBook character offsets. Uses streaming SAX parsing to match chapter text accounting with ChapterLayout, enabling position sync without requiring DOM or pagination. Handles element ancestry tracking, text offset calculation, and spine index extraction.
2026-07-08 01:11:29 -04:00
Justin Mitchell 3421171670 Add character-offset bookmarks for FreeInkBook
Implements layout-independent bookmarks using character offsets within chapters instead of percentage-based positions. This allows bookmarks to restore to the exact position regardless of font size or orientation changes. Adds TOC navigation helpers, book progress calculation based on spine item sizes, and a NONE BidiBaseDir option for pre-reordered text. Legacy bookmarks without character offsets fall back to percentage-based positioning.
2026-07-08 00:56:53 -04:00
Justin Mitchell 0848649ce2 Add FreeInkBook EPUB engine migration guide
Document migration plan from CrossPoint's lib/Epub to FreeInkBook, a memory-efficient clean-room EPUB engine. Covers advantages (O(paragraph+page) memory via SAX parsing, arena allocation, host-testable, full UAX#14 typography, runtime TTF/OTF support), 9-step migration strategy, ESP32-C3 constraints, and integration approach preserving existing UI layer while replacing parsing/layout stack.
2026-07-08 00:22:21 -04:00
Nick 5776911af0 fix: FontDecompressor OOM aborts on the render path (-fno-exceptions makes vector resize fatal) (#2526) 2026-07-07 09:06:18 +03:00
Pietro Campagnano 3c5c5e8aa7 feat: preview image files inline in web file browser (#2429) 2026-07-06 16:45:22 -04:00
Justin Mitchell 6f5c5a0900 fix: Flatten TextBlock word storage into single allocation (#2547) 2026-07-06 23:41:00 +03:00
Uri Tauber f2b00f6b93 chore: Replace product link with affiliate tracking link (#2401) 2026-07-06 23:13:27 +03:00
Uri TauberandBrooks Ilg 44ff313740 fix: render <br> between paragraphs as a visible section break (#2548)
Co-authored-by: Brooks Ilg <brooksmilg@gmail.com>
2026-07-06 20:10:31 +03:00
Jacob Latonis ca1b833126 fix: follow spec for zxing qr code generation (#2540) 2026-07-06 00:33:34 +03:00
Tom-Inge Larsen 3e1dd31e53 feat(reader): End of Book next-book suggestions (#2499) (#2532) 2026-07-04 23:12:59 +03:00
685d4e88f9 feat: Lazy incremental EPUB section indexing (#2452)
Co-authored-by: Uri Tauber <uritaube@gmail.com>
Co-authored-by: Julia Nguyen <julia@uxj.io>
2026-07-04 21:21:24 +03:00
Jakub Sypiański 79b4d63ee2 fix: settings persist on font clear, reserve() before push_back, minor (#2519) 2026-07-03 18:58:24 +03:00
Leopoldo Pla SempereandJiangoJ 57447a56a6 feat(epub): improve text-decoration support (#2397)
Co-authored-by: JiangoJ <jiangj2620@gmail.com>
2026-07-02 12:35:24 +03:00
Rahat Ahmed 5c86bfe1fd feat(network): add device serial number to web UI (#2506) 2026-07-02 00:53:55 +03:00
Justin Mitchell 4cdefb137c Update README with firmware flashing warnings and crosspet status
Removed warning about flashing Papyrix on USB-locked devices and updated crosspet status to unmaintained.
2026-07-01 14:22:27 -04:00
Justin Mitchell e7f3406704 fix: Add socket module import to build-sd-fonts script (#2504) (#2505) 2026-07-01 12:10:15 -04:00
Bastian 6f08a88b12 fix: changing translation in czech to fix overflowing navigation (#2502) 2026-07-01 11:58:24 -04:00
Tom-Inge LarsenandClaude Opus 4.8 e4d3981469 fix(reader): correct slider side-button direction and legend on X3 (#2402) (#2428)
## Summary

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

## Additional Context

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

---

### AI Usage

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

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 11:06:31 -04:00
Bastian c9d27826f9 docs: update TOC in userguide (#2479) 2026-06-30 08:48:21 +03:00
Bastian 13bd00bf9d fix: sort sleep screen menu options to be more logically consistent (#2480) 2026-06-30 08:47:25 +03:00
Uri Tauberandpablohc fef39da23c feat: Selection Popup (#2358)
Co-authored-by: pablohc <pablonoviello@outlook.com>
2026-06-29 16:59:25 -04:00
Uri Tauber fd43ca2fe1 chore: Initial multi-core compatibility (#2294) 2026-06-29 21:57:21 +03:00
Pietro Campagnano 28255061fb fix(icons): align home menu icons with their labels (#2470) 2026-06-29 13:59:54 -04:00
Julia Nguyen 43ff1b6644 Merge remote-tracking branch 'origin/master' into develop 2026-06-29 12:29:58 -04:00
Егор Мартынов 2754a5ff01 feat: add Vollkorn font (#2473) 2026-06-29 15:54:01 +03:00
Justin Mitchell 6edeed73c5 fix: x4 sleep/boot ghosting (#2471) 2026-06-29 12:04:33 +03:00
Justin Mitchell 8ac302018c fix: x4 ghosting by bumping sdk (#2469) 2026-06-28 22:38:35 +03:00
Rodrigo Matsuura a11517a62d fix: update Portuguese (Brasil) translations (#2458) 2026-06-28 15:07:52 +03:00
310 changed files with 7058 additions and 103693 deletions

No files matched your search

+3 -5
View File
@@ -8,6 +8,8 @@ CrossPoint is open-source e-reader firmware - community-built, fully hackable, f
![CrossPoint Reader running on Xteink device](./docs/images/cover.jpg)
> If you're planning to buy an Xteink device, consider purchasing an **X3/X4 Developer Edition** through https://crosspointreader.com. CrossPoint receives a small share of each sale, helping fund development costs.
## What can CrossPoint do?
- **Reader engine**: EPUB 2/3 rendering with embedded-style option, image handling, hyphenation, kerning, chapter navigation, footnotes, bookmarks, go-to-percent, auto page turn, orientation control, focus reading, KOReader progress sync and more.
@@ -67,10 +69,6 @@ USB port or browser before assuming the device is locked. Only reach for the unl
> 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
@@ -249,7 +247,7 @@ One of the best things about open source is that anyone can take the code in a d
- [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.
- ~~[crosspet](https://github.com/trilwu/crosspet) — A Vietnamese fork that adds a Tamagotchi-style virtual chicken that grows based on your reading milestones (pages read, streaks, care). Also: Flashcards, Weather, Pomodoro timer, and mini-games.~~ (Unmaintained)
- [crosspoint-reader-cjk](https://github.com/aBER0724/crosspoint-reader-cjk) — Purpose-built for Chinese, Japanese, and Korean reading.
+21 -12
View File
@@ -5,6 +5,7 @@ Welcome to the **CrossPoint** firmware. This guide outlines the hardware control
- [CrossPoint User Guide](#crosspoint-user-guide)
- [1. Hardware Overview](#1-hardware-overview)
- [Button Layout](#button-layout)
- [Taking a Screenshot](#taking-a-screenshot)
- [2. Power \& Startup](#2-power--startup)
- [Power On / Off](#power-on--off)
- [First Launch](#first-launch)
@@ -14,7 +15,11 @@ Welcome to the **CrossPoint** firmware. This guide outlines the hardware control
- [3.3 Browse Files Screen](#33-browse-files-screen)
- [3.4 Recent Books Screen](#34-recent-books-screen)
- [3.5 File Transfer Screen](#35-file-transfer-screen)
- [3.5.1 Calibre Wireless Transfers](#351-calibre-wireless-transfers)
- [3.5.1 Calibre Wireless Transfers](#351-calibre-wireless-transfers)
- [Installing the Plugin in Calibre](#installing-the-plugin-in-calibre)
- [Configuring the CrossPoint Plugin in Calibre](#configuring-the-crosspoint-plugin-in-calibre)
- [Uploading Books](#uploading-books)
- [Removing a Book](#removing-a-book)
- [3.6 Settings](#36-settings)
- [3.6.1 Display](#361-display)
- [3.6.2 Reader](#362-reader)
@@ -23,21 +28,25 @@ Welcome to the **CrossPoint** firmware. This guide outlines the hardware control
- [3.6.5 OPDS Servers (Multiple Libraries)](#365-opds-servers-multiple-libraries)
- [3.6.6 Web Settings (Wi-Fi + OPDS)](#366-web-settings-wi-fi--opds)
- [3.6.7 KOReader Sync Quick Setup](#367-koreader-sync-quick-setup)
- [Option A: Free Public Server (`sync.koreader.rocks`)](#option-a-free-public-server-synckoreaderrocks)
- [Option B: Self-Hosted Server (Docker Compose)](#option-b-self-hosted-server-docker-compose)
- [3.7 Sleep Screen](#37-sleep-screen)
- [Cover settings](#cover-settings)
- [Custom images](#custom-images)
- [3.8 Custom Fonts (SD Card)](#38-custom-fonts-sd-card)
- [4. Reading Mode](#4-reading-mode)
- [Page Turning](#page-turning)
- [Chapter Navigation](#chapter-navigation)
- [Auto Page Turn](#auto-page-turn)
- [Tilt Page Turn (X3 only)](#tilt-page-turn-x3-only)
- [Footnote Navigation](#footnote-navigation)
- [System Navigation](#system-navigation)
- [Supported Languages](#supported-languages)
- [Page Turning](#page-turning)
- [Chapter Navigation](#chapter-navigation)
- [Auto Page Turn](#auto-page-turn)
- [Tilt Page Turn (X3 only)](#tilt-page-turn-x3-only)
- [Footnote Navigation](#footnote-navigation)
- [System Navigation](#system-navigation)
- [Supported Languages](#supported-languages)
- [5. Reader Menu](#5-reader-menu)
- [5.1 Chapter Selection](#51-chapter-selection)
- [5.2 Bookmarks](#52-bookmarks)
- [6. Current Limitations & Roadmap](#6-current-limitations--roadmap)
- [7. Troubleshooting Issues & Escaping Bootloop](#7-troubleshooting-issues--escaping-bootloop)
- [5.1 Chapter Selection](#51-chapter-selection)
- [5.2 Bookmarks](#52-bookmarks)
- [6. Current Limitations \& Roadmap](#6-current-limitations--roadmap)
- [7. Troubleshooting Issues \& Escaping Bootloop](#7-troubleshooting-issues--escaping-bootloop)
## 1. Hardware Overview
+22 -10
View File
@@ -90,13 +90,13 @@ if (parsedSize != fileSize) {
## `section.bin`
### Version 25
### Version 29
Each file in `sections/*.bin` stores one laid-out spine section. The header is
also the cache-busting key: if any layout-affecting setting differs from the
current reader settings, the section is discarded and rebuilt.
Version 25 includes:
Version 29 includes:
- cache-busting fields for paragraph alignment, hyphenation, embedded CSS,
image rendering mode, and Focus Reading
@@ -105,6 +105,12 @@ Version 25 includes:
- paragraph and list-item LUTs used by KOReader sync page refinement
- optional per-word Focus Reading split metadata
- per-page footnote entries
- serialized word style bits for underline, strikethrough, superscript, and
subscript
- flat TextBlock word storage (v29): per-word arrays plus one shared
NUL-terminated text blob, replacing v28's length-prefixed word strings. The
on-disk order mirrors the in-RAM arena so the firmware reads a whole block
payload with a single allocation and a single SD read
ImHex pattern:
@@ -113,7 +119,7 @@ import std.mem;
import std.string;
import std.core;
#define EXPECTED_VERSION 25
#define EXPECTED_VERSION 29
#define MAX_STRING_LENGTH 65535
#define FOOTNOTE_NUMBER_LEN 32
#define FOOTNOTE_HREF_LEN 96
@@ -174,14 +180,20 @@ struct BlockStyle {
struct TextBlock {
u16 wordCount;
String words[wordCount];
s16 wordXPos[wordCount];
WordStyle wordStyle[wordCount];
u8 hasFocus;
if (hasFocus != 0) {
u8 wordFocusBoundary[wordCount] [[comment("UTF-8 byte boundary between bold prefix and suffix")]];
u16 wordFocusSuffixX[wordCount] [[comment("Suffix x offset from word start")]];
u16 textBytes [[comment("Total size of text[], including one NUL per word")]];
if (wordCount > 0) {
u16 textOff[wordCount] [[comment("Byte offset of word i's text within text[]")]];
s16 wordXPos[wordCount];
if (hasFocus != 0) {
u16 wordFocusSuffixX[wordCount] [[comment("Suffix x offset from word start")]];
}
WordStyle wordStyle[wordCount];
if (hasFocus != 0) {
u8 wordFocusBoundary[wordCount] [[comment("UTF-8 byte boundary between bold prefix and suffix")]];
}
char text[textBytes] [[comment("All words back to back, each NUL-terminated")]];
}
BlockStyle blockStyle;
+15
View File
@@ -154,6 +154,21 @@ uint32_t EpdFont::applyLigatures(uint32_t cp, const char*& text) const {
return cp;
}
bool EpdFont::hasGlyph(const uint32_t cp) const {
const int count = data->intervalCount;
if (count > 0) {
const EpdUnicodeInterval* intervals = data->intervals;
const auto it =
std::upper_bound(intervals, intervals + count, cp,
[](uint32_t value, const EpdUnicodeInterval& interval) { return value < interval.first; });
if (it != intervals && cp <= (it - 1)->last) return true;
}
if (data->glyphMissHandler) {
return data->glyphMissHandler(data->glyphMissCtx, cp) != nullptr;
}
return false;
}
const EpdGlyph* EpdFont::getGlyph(const uint32_t cp) const {
const int count = data->intervalCount;
if (count == 0 && !data->glyphMissHandler) return nullptr;
+6
View File
@@ -12,6 +12,12 @@ class EpdFont {
const EpdGlyph* getGlyph(uint32_t cp) const;
/// True when the font actually provides `cp` (interval table or on-demand
/// SD load) — unlike getGlyph(), never satisfied by the replacement glyph.
/// Used by layout shaping to decide whether a substitution (ligature,
/// Arabic presentation form) can really be drawn.
bool hasGlyph(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;
+7 -1
View File
@@ -1,7 +1,7 @@
#include "EpdFontFamily.h"
const EpdFont* EpdFontFamily::getFont(const Style style) const {
// Extract font style bits (ignore UNDERLINE bit for font selection)
// Extract font style bits; render-time overlay bits do not affect font selection.
const bool hasBold = (style & BOLD) != 0;
const bool hasItalic = (style & ITALIC) != 0;
@@ -28,10 +28,16 @@ const EpdGlyph* EpdFontFamily::getGlyph(const uint32_t cp, const Style style) co
return getFont(style)->getGlyph(cp);
}
bool EpdFontFamily::hasGlyph(const uint32_t cp, const Style style) const { return getFont(style)->hasGlyph(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::getLigature(const uint32_t leftCp, const uint32_t rightCp, const Style style) const {
return getFont(style)->getLigature(leftCp, rightCp);
}
uint32_t EpdFontFamily::applyLigatures(const uint32_t cp, const char*& text, const Style style) const {
return getFont(style)->applyLigatures(cp, text);
}
+6
View File
@@ -17,6 +17,7 @@ class EpdFontFamily {
SUP = 16, // superscript: glyph scaled 50%, raised ~40% of ascender
SUB = 32, // subscript: glyph scaled 50%, lowered ~25% of ascender
};
static constexpr uint8_t TEXT_DECORATION_MASK = static_cast<uint8_t>(UNDERLINE | STRIKETHROUGH);
explicit EpdFontFamily(const EpdFont* regular, const EpdFont* bold = nullptr, const EpdFont* italic = nullptr,
const EpdFont* boldItalic = nullptr)
@@ -25,8 +26,13 @@ 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;
bool hasGlyph(uint32_t cp, Style style = REGULAR) const;
int8_t getKerning(uint32_t leftCp, uint32_t rightCp, Style style = REGULAR) const;
uint32_t getLigature(uint32_t leftCp, uint32_t rightCp, Style style = REGULAR) const;
uint32_t applyLigatures(uint32_t cp, const char*& text, Style style = REGULAR) const;
static constexpr bool hasTextDecoration(const Style style) {
return (static_cast<uint8_t>(style) & TEXT_DECORATION_MASK) != 0;
}
private:
const EpdFont* regular;
+26 -20
View File
@@ -33,12 +33,24 @@ void FontDecompressor::freePageBuffer() {
}
void FontDecompressor::freeHotGroup() {
hotGroup.clear();
hotGroup.shrink_to_fit();
free(hotGroup);
hotGroup = nullptr;
hotGroupCapacity = 0;
hotGroupFont = nullptr;
hotGroupIndex = UINT16_MAX;
hotGlyphBuf.clear();
hotGlyphBuf.shrink_to_fit();
free(hotGlyphBuf);
hotGlyphBuf = nullptr;
hotGlyphBufCapacity = 0;
}
bool FontDecompressor::ensureCapacity(uint8_t*& buf, uint32_t& capacity, uint32_t needed) {
if (capacity >= needed) return true;
// Grow-only, free-then-malloc: every caller fully rewrites the buffer after a grow, so the
// old contents are dead -- freeing first gives the allocator its best shot on a tight heap.
free(buf);
buf = static_cast<uint8_t*>(malloc(needed)); // owned by FontDecompressor, freed in freeHotGroup()
capacity = buf ? needed : 0;
return buf != nullptr;
}
uint16_t FontDecompressor::getGroupIndex(const EpdFontData* fontData, uint32_t glyphIndex) {
@@ -170,24 +182,20 @@ const uint8_t* FontDecompressor::getBitmap(const EpdFontData* fontData, const Ep
}
// Check if hot group already has this group decompressed — if not, decompress it
if (!(!hotGroup.empty() && hotGroupFont == fontData && hotGroupIndex == groupIndex)) {
if (!(hotGroup != nullptr && hotGroupFont == fontData && hotGroupIndex == groupIndex)) {
stats.cacheMisses++;
const EpdFontGroup& group = fontData->groups[groupIndex];
hotGroup.resize(group.uncompressedSize);
if (hotGroup.empty()) {
// ensureCapacity may free the buffer, so the cached-group identity dies with it either way.
hotGroupFont = nullptr;
hotGroupIndex = UINT16_MAX;
if (!ensureCapacity(hotGroup, hotGroupCapacity, group.uncompressedSize)) {
LOG_ERR("FDC", "Failed to allocate %u bytes for hot group %u", group.uncompressedSize, groupIndex);
hotGroupFont = nullptr;
hotGroupIndex = UINT16_MAX;
stats.getBitmapTimeUs += micros() - tStart;
return nullptr;
}
if (!decompressGroup(fontData, groupIndex, hotGroup.data(), group.uncompressedSize)) {
hotGroup.clear();
hotGroup.shrink_to_fit();
hotGroupFont = nullptr;
hotGroupIndex = UINT16_MAX;
if (!decompressGroup(fontData, groupIndex, hotGroup, group.uncompressedSize)) {
stats.getBitmapTimeUs += micros() - tStart;
return nullptr;
}
@@ -200,18 +208,16 @@ const uint8_t* FontDecompressor::getBitmap(const EpdFontData* fontData, const Ep
}
// Compact just the requested glyph from byte-aligned data into scratch buffer
if (glyph->dataLength > hotGlyphBuf.size()) {
hotGlyphBuf.resize(glyph->dataLength);
}
if (hotGlyphBuf.empty()) {
if (!ensureCapacity(hotGlyphBuf, hotGlyphBufCapacity, glyph->dataLength)) {
LOG_ERR("FDC", "Failed to allocate %u bytes for glyph scratch", (unsigned)glyph->dataLength);
stats.getBitmapTimeUs += micros() - tStart;
return nullptr;
}
uint32_t alignedOff = getAlignedOffset(fontData, groupIndex, glyphIndex);
compactSingleGlyph(&hotGroup[alignedOff], hotGlyphBuf.data(), glyph->width, glyph->height);
compactSingleGlyph(&hotGroup[alignedOff], hotGlyphBuf, glyph->width, glyph->height);
stats.getBitmapTimeUs += micros() - tStart;
return hotGlyphBuf.data();
return hotGlyphBuf;
}
// --- Prewarm: pre-decompress glyph bitmaps for a page of text ---
+12 -5
View File
@@ -2,8 +2,6 @@
#include <InflateReader.h>
#include <vector>
#include "EpdFontData.h"
class FontDecompressor {
@@ -67,13 +65,22 @@ class FontDecompressor {
// Hot group: last decompressed group (byte-aligned) for non-prewarmed fallback path.
// Kept in byte-aligned format; individual glyphs are compacted on demand into hotGlyphBuf.
// Nothrow high-water malloc buffers, NOT std::vector: getBitmap() runs on the render path,
// and under -fno-exceptions a vector resize that hits OOM abort()s the firmware instead of
// failing (field crash: hotGroup.resize() -> std::bad_alloc -> abort with ~11 KB free).
// ensureCapacity() returns false on OOM so the caller can skip the glyph gracefully.
const EpdFontData* hotGroupFont = nullptr;
uint16_t hotGroupIndex = UINT16_MAX;
std::vector<uint8_t> hotGroup;
uint8_t* hotGroup = nullptr; // owned; freed in freeHotGroup()/dtor
uint32_t hotGroupCapacity = 0;
// Scratch buffer for compacting a single glyph from the hot group.
// Valid until the next getBitmap() call.
std::vector<uint8_t> hotGlyphBuf;
// Valid until the next getBitmap() call. Same ownership/OOM contract as hotGroup.
uint8_t* hotGlyphBuf = nullptr;
uint32_t hotGlyphBufCapacity = 0;
// Grow (never shrink) an owned buffer to at least `needed` bytes; false on OOM, buffer freed.
static bool ensureCapacity(uint8_t*& buf, uint32_t& capacity, uint32_t needed);
void freePageBuffer();
void freeHotGroup();
+22
View File
@@ -1061,6 +1061,28 @@ void SdCardFont::mergeIntoAdvanceTable(uint8_t styleIdx, const AdvanceEntry* sor
advanceTableSize_[styleIdx] = k;
}
bool SdCardFont::hasGlyphMeta(uint8_t styleIdx, uint32_t codepoint) const {
styleIdx &= (MAX_STYLES - 1);
const PerStyle& s = styles_[styleIdx];
if (!loaded_ || !s.present) return false;
return findGlobalGlyphIndex(s, codepoint) >= 0;
}
uint16_t SdCardFont::ensureAdvance(uint8_t styleIdx, uint32_t codepoint) {
styleIdx &= (MAX_STYLES - 1);
if (!loaded_ || !styles_[styleIdx].present) return 0;
const uint16_t cached = getAdvance(codepoint, styleIdx);
if (cached != 0) return cached;
// 0 is either "not in table" or a genuine zero-width glyph; only pay the
// SD read when the resident interval table says the glyph exists.
if (findGlobalGlyphIndex(styles_[styleIdx], codepoint) < 0) return 0;
uint32_t cp = codepoint;
fetchAdvancesForCodepoints(&cp, 1, static_cast<uint8_t>(1u << styleIdx));
return getAdvance(codepoint, styleIdx);
}
uint8_t SdCardFont::styleIdxFromMissCtx(void* ctx) { return static_cast<OverflowContext*>(ctx)->styleIdx; }
bool SdCardFont::hasAdvanceTable() const {
for (uint8_t i = 0; i < MAX_STYLES; i++) {
if (advanceTable_[i]) return true;
+17
View File
@@ -55,6 +55,23 @@ class SdCardFont {
// Returns the 12.4 fixed-point advance, or 0 if not found.
uint16_t getAdvance(uint32_t codepoint, uint8_t style) const;
// Layout-side metric access — the per-glyph path CpFontAdapter uses while
// the FreeInkBook engine measures a chapter. Neither touches bitmap data:
// going through getGlyph() would stream every occurrence's bitmap through
// the 8-slot overflow ring (one SD read per character — a crawling build).
//
// hasGlyphMeta answers existence from the resident interval table (no I/O).
bool hasGlyphMeta(uint8_t styleIdx, uint32_t codepoint) const;
// ensureAdvance returns the 12.4 fixed-point advanceX, fetching the glyph
// METADATA from SD once on a miss and merging it into the persistent
// advance table (so each unique codepoint costs one small read per
// session). Returns 0 when the style lacks the codepoint.
uint16_t ensureAdvance(uint8_t styleIdx, uint32_t codepoint);
// Recover the style index from an EpdFontData::glyphMissCtx (see
// fromMissCtx below for recovering the SdCardFont itself).
static uint8_t styleIdxFromMissCtx(void* ctx);
// Returns true if advance table is populated for at least one style.
bool hasAdvanceTable() const;
-2
View File
@@ -37,5 +37,3 @@
#include <builtinFonts/ubuntu_10_regular.h>
#include <builtinFonts/ubuntu_12_bold.h>
#include <builtinFonts/ubuntu_12_regular.h>
#include <builtinFonts/ubuntu_14_bold.h>
#include <builtinFonts/ubuntu_14_regular.h>
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
-7
View File
@@ -94,13 +94,6 @@ ruby -rdigest -e 'puts [
].map{|f| Digest::SHA256.hexdigest(File.read(f)).to_i(16) }.sum % (2 ** 32) - (2 ** 31)'
))"
echo "#define UI_14_FONT_ID ($(
ruby -rdigest -e 'puts [
"./ubuntu_14_regular.h",
"./ubuntu_14_bold.h",
].map{|f| Digest::SHA256.hexdigest(File.read(f)).to_i(16) }.sum % (2 ** 32) - (2 ** 31)'
))"
echo "#define SMALL_FONT_ID ($(
ruby -rdigest -e 'puts [
"./notosans_8_regular.h",
+35 -7
View File
@@ -33,6 +33,7 @@ import sys
import tempfile
import threading
import time
import socket
import urllib.request
from concurrent.futures import ProcessPoolExecutor, as_completed
from pathlib import Path
@@ -49,17 +50,44 @@ INSTANCE_DIR = SCRIPT_DIR / "instanced_fonts"
DEFAULT_FALLBACK_FONT = EPDFONTS_DIR / "builtinFonts/source/NotoSans/NotoSans-Regular.ttf"
def download_font(url: str, dest: Path) -> Path:
"""Download a font file if not already cached. Returns the local path."""
_orig_getaddrinfo = socket.getaddrinfo
def _ipv4_only_getaddrinfo(*args, **kwargs):
"""getaddrinfo variant that drops AAAA records (IPv4 only)."""
return [ai for ai in _orig_getaddrinfo(*args, **kwargs) if ai[0] == socket.AF_INET]
def download_font(url: str, dest: Path, retries: int = 3) -> Path:
"""Download a font file if not already cached. Returns the local path.
Some sources (e.g. mirrors.ctan.org) are round-robin redirectors that land
on a different mirror each request; a mirror may advertise an IPv6 address a
host without an IPv6 route cannot reach ([Errno 101] Network is unreachable).
Retry on failure, forcing IPv4 resolution after the first attempt.
"""
if dest.exists():
return dest
dest.parent.mkdir(parents=True, exist_ok=True)
print(f" Downloading {dest.name}...")
try:
urllib.request.urlretrieve(url, dest)
except Exception as e:
dest.unlink(missing_ok=True)
raise RuntimeError(f"Failed to download {url}: {e}") from e
last_err = None
for attempt in range(1, retries + 1):
force_ipv4 = attempt > 1
if force_ipv4:
socket.getaddrinfo = _ipv4_only_getaddrinfo
try:
urllib.request.urlretrieve(url, dest)
break
except Exception as e: # noqa: BLE001 - reported via RuntimeError below
last_err = e
dest.unlink(missing_ok=True)
if attempt < retries:
print(f" Attempt {attempt} failed ({e}); retrying (IPv4-only)...")
finally:
if force_ipv4:
socket.getaddrinfo = _orig_getaddrinfo
else:
raise RuntimeError(f"Failed to download {url}: {last_err}") from last_err
size_kb = dest.stat().st_size / 1024
print(f" Downloaded {dest.name} ({size_kb:.0f} KB)")
return dest
+12 -31
View File
@@ -28,42 +28,23 @@ for size in ${NOTOSANS_FONT_SIZES[@]}; do
done
done
# Small UI chrome (button devices, uiScale 1.0). Rendered 1-bit: crisp at these
# sizes and half the flash of 2-bit.
UI_FONT_SIZES=(10 12)
# Larger UI chrome substituted in on touch/high-density boards via the uiScale
# remap (see src/main.cpp setupFonts). Rendered 2-bit so it stays smooth when
# enlarged. Touch boards use uiScale 1.2, so UI_12 -> 14.4 -> 14 is the size the
# remap actually lands on; add larger sizes here if a board adopts a higher scale.
UI_FONT_SIZES_LARGE=(14)
UI_FONT_STYLES=("Regular" "Bold")
# Ubuntu lacks the Latin Extended Additional block (U+1EA0-U+1EF9) used for
# Vietnamese tone marks. Append a Vietnamese-only Ubuntu cut so those glyphs are
# filled from it while every glyph Ubuntu already has stays unchanged (fontstack
# is ordered by descending priority). NotoSansHebrew fills U+05D0-U+05EA so the
# Hebrew UI translation renders in menus and settings.
generate_ui_font() {
local size="$1" style="$2" extra_flags="$3"
local font_name="ubuntu_${size}_$(echo $style | tr '[:upper:]' '[:lower:]')"
local font_path="../builtinFonts/source/Ubuntu/Ubuntu-${style}.ttf"
local hebrew_path="../builtinFonts/source/NotoSansHebrew/NotoSansHebrew-${style}.ttf"
local viet_path="../builtinFonts/source/Ubuntu/Ubuntu-Vietnamese-${style}.ttf"
local output_path="../builtinFonts/${font_name}.h"
python fontconvert.py $font_name $size $font_path $hebrew_path $viet_path \
--additional-intervals 0x05D0,0x05EA $extra_flags > $output_path
echo "Generated $output_path"
}
for size in ${UI_FONT_SIZES[@]}; do
for style in ${UI_FONT_STYLES[@]}; do
generate_ui_font $size $style ""
done
done
for size in ${UI_FONT_SIZES_LARGE[@]}; do
for style in ${UI_FONT_STYLES[@]}; do
generate_ui_font $size $style "--2bit --compress"
font_name="ubuntu_${size}_$(echo $style | tr '[:upper:]' '[:lower:]')"
font_path="../builtinFonts/source/Ubuntu/Ubuntu-${style}.ttf"
hebrew_path="../builtinFonts/source/NotoSansHebrew/NotoSansHebrew-${style}.ttf"
# Ubuntu lacks the Latin Extended Additional block (U+1EA0-U+1EF9) used for
# Vietnamese tone marks. Append a Vietnamese-only Ubuntu cut so those glyphs
# are filled from it while every glyph Ubuntu already has stays unchanged
# (fontstack is ordered by descending priority).
viet_path="../builtinFonts/source/Ubuntu/Ubuntu-Vietnamese-${style}.ttf"
output_path="../builtinFonts/${font_name}.h"
python fontconvert.py $font_name $size $font_path $hebrew_path $viet_path \
--additional-intervals 0x05D0,0x05EA > $output_path
echo "Generated $output_path"
done
done
+10 -1
View File
@@ -132,7 +132,16 @@ families:
bold: {url: "https://raw.githubusercontent.com/impallari/Libre-Baskerville/master/fonts/ttf/LibreBaskerville-Bold.ttf"}
italic: {url: "https://raw.githubusercontent.com/impallari/Libre-Baskerville/master/fonts/ttf/LibreBaskerville-Italic.ttf"}
bolditalic: {url: "https://raw.githubusercontent.com/impallari/Libre-Baskerville/master/fonts/ttf/LibreBaskerville-BoldItalic.ttf"}
- name: Vollkorn
description: "A serif for bread and butter use by Friedrich Althausen (Latin, Greek, Cyrillic)"
intervals: latin-ext,greek,cyrillic
sizes: [12, 14, 16, 18]
styles:
regular: {url: "https://raw.githubusercontent.com/google/fonts/main/ofl/vollkorn/Vollkorn%5Bwght%5D.ttf", variable: {wght: 400}}
bold: {url: "https://raw.githubusercontent.com/google/fonts/main/ofl/vollkorn/Vollkorn%5Bwght%5D.ttf", variable: {wght: 700}}
italic: {url: "https://raw.githubusercontent.com/google/fonts/main/ofl/vollkorn/Vollkorn-Italic%5Bwght%5D.ttf", variable: {wght: 400}}
bolditalic: {url: "https://raw.githubusercontent.com/google/fonts/main/ofl/vollkorn/Vollkorn-Italic%5Bwght%5D.ttf", variable: {wght: 700}}
# ── Sans-serif ─────────────────────────────────────────────────────────
-927
View File
File diff suppressed because it is too large. Load diff
Loaded 20 of 310 files, more files were not shown because too many files have changed in this diff. Show more