Compare commits

..
13 Commits
Author SHA1 Message Date
Zach Nelson 8959836e46 Restore lost block-quote on WARNING 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
Zach Nelson fcb8793640 Merge branch 'master' into release/1.3.0 2026-05-10 11:07:12 -05:00
Zach Nelson 6c97061629 Merge branch 'master' into release/1.3.0 2026-05-09 21:19:15 -05:00
Zach Nelson 9b2d8388ef Merge branch 'master' into release/1.3.0 2026-05-08 22:08:28 -05:00
Zach Nelson bb3a44f584 Merge branch 'master' into release/1.3.0 2026-05-08 21:57:15 -05:00
Zach Nelson bd546093cb chore: Bump version to 1.3.0 2026-05-04 21:20:17 -05:00
177 changed files with 1402 additions and 16459 deletions

No files matched your search

+2 -1
View File
@@ -1 +1,2 @@
custom: ["https://app.royalty.dev/crosspoint-reader/crosspoint-reader"]
github: [daveallie]
ko_fi: daveallie
+23 -65
View File
@@ -58,7 +58,6 @@ find src -name "*.cpp" -o -name "*.h" | xargs clang-format -i
6. `constexpr` First: Compile-time constants and lookup tables must be `constexpr`, not just `static const`. This moves computation to compile time, enables dead-branch elimination, and guarantees flash placement. Use `static constexpr` for class-level constants.
7. `std::vector` Pre-allocation: Always call `.reserve(N)` before any `push_back()` loop. Each growth event allocates a new block (2×), copies all elements, then frees the old one — three heap operations that fragment DRAM. When the final size is unknown, estimate conservatively.
8. SPIFFS Write Throttling: Never write a settings file on every user interaction. Guard all writes with a value-change check (`if (newVal == _current) return;`). Progress saves during reading must be debounced — write on activity exit or every N page turns, not on every turn. SPIFFS sectors have a finite erase cycle limit.
9. `new` is not nothrow on ESP32: With `-fno-exceptions`, bare `new` that fails calls `abort()` — it does NOT return `nullptr`. Always use `new (std::nothrow)` and null-check the result, or use `makeUniqueNoThrow<T>()` from `lib/Memory/Memory.h`. Never write bare `new` for any fallible allocation.
---
@@ -161,12 +160,6 @@ if (Storage.openFileForRead("MODULE", "/path/to/file.bin", file)) {
**Usage**: See example above. Uses `FsFile` (SdFat), NOT Arduino `File`. Do NOT add `file.close()` for local variables (see DESTRUCTOR_CLOSES_FILE above).
**SdFat is not thread-safe; all SD access MUST go through HalStorage**:
- SdFat's `SdSpiCard` tracks SPI bus state with an unsynchronized `m_spiActive` bool. Two tasks calling SdFat concurrently can confuse that state machine and end with one task calling `SPIClass::endTransaction()` against a paramLock the *other* task is holding. That trips FreeRTOS's `xTaskPriorityDisinherit` assert (`tasks.c:5156, pxTCB == pxCurrentTCBs[0]`) and panics the system. See SdFat issue #518.
- `HalStorage` serializes everything via `storageMutex`. Downstream code includes `<HalStorage.h>`, which transparently `using FsFile = HalFile;`; every method call (read, write, seek, close) takes the mutex. `HalFile`'s destructor also takes the mutex before letting the underlying SdFat `FsFile` close.
- **Never** call into `SdFat` / `SdSpiCard` / `FsBaseFile` / `SDCardManager` directly. **Never** define `HAL_STORAGE_IMPL` outside `HalStorage.cpp`; that disables the `FsFile -> HalFile` typedef and you'll get a raw SdFat handle that bypasses the mutex.
- If you're storing a raw `FsFile` in a place that won't transitively include `<HalStorage.h>` (rare), include the header explicitly so the typedef applies.
---
## Coding Standards
@@ -273,78 +266,43 @@ When a template is necessary, limit instantiations: use explicit template instan
**Rules**: NO exceptions, NO abort(), ALWAYS log before error return
### Heap Buffer Allocation
### Acceptable malloc/free Patterns
**Prefer `makeUniqueNoThrow` over `malloc`.** Both are nothrow (return `nullptr` on OOM rather than calling `abort()`), but `malloc` requires a manual `free` on every return path — a common source of leaks. `makeUniqueNoThrow<uint8_t[]>(size)` from `lib/Memory/Memory.h` frees automatically when it goes out of scope.
**Source**: [src/activities/home/HomeActivity.cpp:166](../src/activities/home/HomeActivity.cpp), [lib/GfxRenderer/GfxRenderer.cpp:439-440](../lib/GfxRenderer/GfxRenderer.cpp)
**Preferred pattern**:
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
#include <Memory.h>
auto buffer = makeUniqueNoThrow<uint8_t[]>(bufferSize);
// Allocate
auto* buffer = static_cast<uint8_t*>(malloc(bufferSize));
if (!buffer) {
LOG_ERR("MODULE", "OOM: %d bytes", bufferSize);
return false;
LOG_ERR("MODULE", "malloc failed: %d bytes", bufferSize);
return false; // Handle allocation failure
}
processData(buffer.get(), bufferSize);
// freed automatically — no manual free needed, no leak on early return
```
// Use buffer
processData(buffer, bufferSize);
**`malloc` or `new (std::nothrow)` are still acceptable** when the buffer must be passed to a C API that takes ownership and frees it itself (e.g., certain SDK callbacks). In that case follow the manual pattern:
```cpp
auto* buffer = static_cast<uint8_t*>(malloc(bufferSize)); // or new (std::nothrow) uint8_t[bufferSize]
if (!buffer) {
LOG_ERR("MODULE", "OOM: %d bytes", bufferSize);
return false;
}
sdkApiThatTakesOwnership(buffer, bufferSize); // SDK calls free() / delete[]
// Free immediately after use
free(buffer);
buffer = nullptr;
```
**Rules**:
- **Prefer `makeUniqueNoThrow`** — automatic cleanup eliminates leak risk on error paths
- **ALWAYS check for nullptr** after any allocation and `LOG_ERR` before returning false
- **Raw allocation only** when a C API takes ownership; document why in a comment
- **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**:
- Memory utilities: [Memory.h](../lib/Memory/Memory.h) (`makeUniqueNoThrow`)
- Cover image buffers: [HomeActivity.cpp:166](../src/activities/home/HomeActivity.cpp)
- Text chunk buffers: [TxtReaderActivity.cpp:259](../src/activities/reader/TxtReaderActivity.cpp)
- Bitmap rendering: [GfxRenderer.cpp:439-440](../lib/GfxRenderer/GfxRenderer.cpp)
### Heap Allocation with `new`: Always Use `makeUniqueNoThrow`
**CRITICAL**: With `-fno-exceptions`, bare `new` on OOM calls `abort()` — it does NOT return `nullptr`. Always use `makeUniqueNoThrow` from `lib/Memory/Memory.h`, which wraps `new (std::nothrow)` and returns a `std::unique_ptr` that is null on OOM and automatically frees on scope exit.
**Preferred pattern**:
```cpp
#include <Memory.h>
auto obj = makeUniqueNoThrow<MyClass>(args);
if (!obj) { LOG_ERR("MOD", "OOM: MyClass"); return false; }
auto buf = makeUniqueNoThrow<uint8_t[]>(size);
if (!buf) { LOG_ERR("MOD", "OOM: %d bytes", size); return false; }
// Pass to C APIs via .get(); unique_ptr frees automatically on return
someApi(buf.get(), size);
```
**`new (std::nothrow)` directly is acceptable** when the object must be passed to a C API that takes ownership and calls `delete` itself:
```cpp
auto* obj = new (std::nothrow) MyClass(args);
if (!obj) { LOG_ERR("MOD", "OOM: MyClass"); return false; }
sdkApiThatTakesOwnership(obj); // SDK calls delete
```
**Rules**:
- **Prefer `makeUniqueNoThrow`** — automatic cleanup eliminates leak risk on error paths
- **NEVER use bare `new`** — always `makeUniqueNoThrow` or `new (std::nothrow)`
- **ALWAYS `LOG_ERR` before returning false** on OOM
- **Use `.get()`** to pass the raw pointer to C-style APIs; ownership stays with the `unique_ptr`
- **`new (std::nothrow)` directly only** when a C API takes ownership; document why in a comment
**Examples in codebase**:
- Memory utilities: [Memory.h](../lib/Memory/Memory.h) (`makeUniqueNoThrow`)
- OTA update buffer: [OtaUpdater.cpp:40](../src/network/OtaUpdater.cpp)
---
-16
View File
@@ -1,7 +1,5 @@
# CrossPoint Reader
[![Fund contributors](https://img.shields.io/badge/%F0%9F%91%91_Fund_contributors-royalty.dev-BB953A?style=for-the-badge&labelColor=1a1a1a)](https://app.royalty.dev/crosspoint-reader/crosspoint-reader)
CrossPoint 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.
**Now running on:** ESP32C3-based Xteink [X4](https://www.xteink.com/products/xteink-x4) and [X3](https://www.xteink.com/products/xteink-x3).
@@ -123,20 +121,6 @@ See [Development quick start](#development-quick-start) below.
---
## Custom SD-card fonts
Convert your own TTF/OTF files into `.cpfont` files that load from the SD card. No firmware reflash is needed.
1. Go to https://crosspointreader.com/fonts and open the "SD-card font builder" form.
2. Upload up to four styles (regular, bold, italic, bold-italic), set the family name, point sizes, and Unicode range.
3. Download the generated `.cpfont` files.
4. Copy them to your SD card under `/fonts/YourFont/` (or `/.fonts/YourFont/` to hide the folder).
5. Select the font on the device from the font settings.
Conversion runs the firmware repo's `lib/EpdFont/scripts/fontconvert_sdcard.py` script unmodified, so output matches a local host build.
---
## Documentation
- [User Guide](./USER_GUIDE.md)
+15 -36
View File
@@ -61,29 +61,29 @@ struct TocEntry {
struct BookBin {
// Header
u8 version [[comment("Format version"), color("FFD93D")]];
// Version validation
if (version != EXPECTED_VERSION) {
std::error(std::format("Unsupported version: {} (expected {})", version, EXPECTED_VERSION));
}
u32 lutOffset [[comment("Offset to lookup tables"), color("6BCB77")]];
u16 spineCount [[comment("Number of spine entries"), color("4D96FF")]];
u16 tocCount [[comment("Number of TOC entries"), color("FF6B9D")]];
// Metadata section
Metadata metadata [[comment("Book metadata")]];
// Validate LUT offset alignment
u32 currentOffset = $;
if (currentOffset != lutOffset) {
std::warning(std::format("LUT offset mismatch: expected 0x{:X}, got 0x{:X}", lutOffset, currentOffset));
}
// Lookup Tables
u32 spineLut[spineCount] [[comment("Spine entry offsets"), color("4D96FF")]];
u32 tocLut[tocCount] [[comment("TOC entry offsets"), color("FF6B9D")]];
// Data Entries
SpineEntry spines[spineCount] [[comment("Spine entries (reading order)")]];
TocEntry toc[tocCount] [[comment("Table of contents entries")]];
@@ -104,7 +104,7 @@ if (parsedSize != fileSize) {
## `section.bin`
### Version 24
### Version 8
ImHex Pattern:
@@ -114,7 +114,7 @@ import std.string;
import std.core;
// === Configuration ===
#define EXPECTED_VERSION 24
#define EXPECTED_VERSION 8
#define MAX_STRING_LENGTH 65535
// === String Structure ===
@@ -133,10 +133,8 @@ fn format_string(String s) {
// === Page Structure ===
enum PageElementTag : u8 {
PageLine = 1,
PageImage = 2,
PageHorizontalRule = 3
enum StorageType : u8 {
PageLine = 1
};
enum WordStyle : u8 {
@@ -163,29 +161,10 @@ struct PageLine {
BlockStyle blockStyle;
};
struct PageImage {
s16 xPos;
s16 yPos;
String imagePath;
s16 width;
s16 height;
};
struct PageHorizontalRule {
s16 xPos;
s16 yPos;
u16 width;
u8 thickness;
};
struct PageElement {
u8 pageElementType;
if (pageElementType == 1) {
PageLine pageLine [[inline]];
} else if (pageElementType == 2) {
PageImage pageImage [[inline]];
} else if (pageElementType == 3) {
PageHorizontalRule horizontalRule [[inline]];
} else {
std::error(std::format("Unknown page element type: {}", pageElementType));
}
@@ -201,12 +180,12 @@ struct Page {
struct SectionBin {
// Header
u8 version [[comment("Format version"), color("FFD93D")]];
// Version validation
if (version != EXPECTED_VERSION) {
std::error(std::format("Unsupported version: {} (expected {})", version, EXPECTED_VERSION));
}
// Cache busting parameters
s32 fontId;
float lineCompression;
@@ -215,15 +194,15 @@ struct SectionBin {
u16 vieportHeight;
u16 pageCount;
u32 lutOffset;
Page page[pageCount];
// Validate LUT offset alignment
u32 currentOffset = $;
if (currentOffset != lutOffset) {
std::warning(std::format("LUT offset mismatch: expected 0x{:X}, got 0x{:X}", lutOffset, currentOffset));
}
// Lookup Tables
u32 lut[pageCount];
};
-1
View File
@@ -97,7 +97,6 @@ To convert your own TrueType/OpenType fonts:
| `cyrillic` | Cyrillic + Supplement |
| `cjk` | CJK Unified Ideographs + Hiragana + Katakana + Fullwidth |
| `hangul` | Korean Hangul syllables |
| `reading` | Literary fiction coverage: Latin, Greek, Cyrillic, math/symbol blocks, supplemental punctuation, and CJK quote marks |
| `builtin` | Matches built-in Bookerly coverage exactly |
Combine presets with commas: `--intervals latin-ext,greek,cyrillic`
+18 -19
View File
@@ -204,41 +204,40 @@ Folder created: NewFolder
### POST `/delete` - Delete File or Folder
Deletes one or more files or empty folders from the SD card.
Deletes a file or folder from the SD card.
**Request:**
```bash
# Delete a file
curl -X POST -d "path=/Books/mybook.epub" http://crosspoint.local/delete
curl -X POST -d "path=/Books/mybook.epub&type=file" http://crosspoint.local/delete
# Delete an empty folder
curl -X POST -d "path=/OldFolder" http://crosspoint.local/delete
# Delete multiple items
curl -X POST -d 'paths=["/Books/old.epub","/OldFolder"]' http://crosspoint.local/delete
curl -X POST -d "path=/OldFolder&type=folder" http://crosspoint.local/delete
```
**Form Parameters:**
| Parameter | Required | Default | Description |
| --------- | -------- | ------- | ----------- |
| `path` | Yes, unless `paths` is provided | - | Path to one item to delete |
| `paths` | Yes, unless `path` is provided | - | JSON array of paths to delete |
| Parameter | Required | Default | Description |
| --------- | -------- | ------- | -------------------------------- |
| `path` | Yes | - | Path to the item to delete |
| `type` | No | `file` | Type of item: `file` or `folder` |
**Response (200 OK):**
```text
All items deleted successfully
```
Deleted successfully
```
**Error Responses:**
| Status | Body | Cause |
| ------ | ------------------------------------------- | ---------------------------------- |
| 400 | `Missing "path" or "paths" argument` | Neither parameter was provided |
| 400 | `Provide either 'path' or 'paths', not both` | Both delete parameters were sent |
| 400 | `Invalid paths format` | `paths` was not valid JSON |
| 400 | `No paths provided` | `paths` was an empty JSON array |
| 500 | `Failed to delete some items: ...` | One or more paths could not be deleted |
| Status | Body | Cause |
| ------ | --------------------------------------------- | ----------------------------- |
| 400 | `Missing path` | `path` parameter not provided |
| 400 | `Cannot delete root directory` | Attempted to delete `/` |
| 400 | `Folder is not empty. Delete contents first.` | Non-empty folder |
| 403 | `Cannot delete system files` | Hidden file (starts with `.`) |
| 403 | `Cannot delete protected items` | Protected system folder |
| 404 | `Item not found` | Path does not exist |
| 500 | `Failed to delete item` | SD card error |
**Protected Items:**
- Files/folders starting with `.`
+2 -6
View File
@@ -1079,17 +1079,13 @@ int SdCardFont::fetchAdvancesForCodepoints(uint32_t* codepoints, uint32_t cpCoun
uint32_t needCount = 0;
uint32_t missedThisStyle = 0;
const int32_t replacementIdx = findGlobalGlyphIndex(s, REPLACEMENT_GLYPH);
for (uint32_t i = 0; i < cpCount; i++) {
const uint32_t cp = codepoints[i];
if (advanceTableLookup(si, cp, nullptr)) continue; // already cached
int32_t idx = findGlobalGlyphIndex(s, cp);
if (idx < 0) {
if (replacementIdx < 0) {
missedThisStyle++;
continue;
}
idx = replacementIdx;
missedThisStyle++;
continue;
}
mappings[needCount].codepoint = cp;
mappings[needCount].glyphIndex = idx;
+1 -3
View File
@@ -59,14 +59,12 @@ INTERVAL_PRESETS = {
(0x2190, 0x21FF), (0x2200, 0x22FF), (0x2500, 0x257F),
(0x25A0, 0x25FF), (0x2600, 0x26FF), (0x2700, 0x27BF)],
# Composite preset for English-language literary fiction including scifi/popsci.
# Greek for physics terms, math operators, geometric shapes, uncommon
# dialogue punctuation, CJK quote marks, miscellaneous symbols (♪♫♬), dingbats.
# Greek for physics terms, math operators, miscellaneous symbols (♪♫♬), dingbats.
"reading": [(0x0020, 0x024F), (0x0300, 0x036F), (0x0370, 0x03FF),
(0x0400, 0x04FF), (0x1E00, 0x1EFF), (0x2000, 0x206F),
(0x2070, 0x209F), (0x20A0, 0x20CF), (0x2150, 0x218F),
(0x2190, 0x21FF), (0x2200, 0x22FF), (0x2500, 0x257F),
(0x25A0, 0x25FF), (0x2600, 0x26FF), (0x2700, 0x27BF),
(0x2900, 0x29FF), (0x2E00, 0x2E7F), (0x3000, 0x303F),
(0xFB00, 0xFB06)],
# Matches the built-in font intervals from fontconvert.py exactly
"builtin": [(0x0000, 0x007F), (0x0080, 0x00FF), (0x0100, 0x017F),
-11
View File
@@ -113,17 +113,6 @@ families:
italic: {url: "https://raw.githubusercontent.com/google/fonts/main/ofl/bitter/Bitter-Italic%5Bwght%5D.ttf", variable: {wght: 500}}
bolditalic: {url: "https://raw.githubusercontent.com/google/fonts/main/ofl/bitter/Bitter-Italic%5Bwght%5D.ttf", variable: {wght: 700}}
- name: Domitian
description: "A humanist serif for literary reading (Latin, Greek, Cyrillic)"
intervals: latin-ext,greek,cyrillic
sizes: [12, 14, 16, 18]
styles:
regular: {url: "https://mirrors.ctan.org/fonts/domitian/opentype/Domitian-Roman.otf"}
bold: {url: "https://mirrors.ctan.org/fonts/domitian/opentype/Domitian-Bold.otf"}
italic: {url: "https://mirrors.ctan.org/fonts/domitian/opentype/Domitian-Italic.otf"}
bolditalic: {url: "https://mirrors.ctan.org/fonts/domitian/opentype/Domitian-BoldItalic.otf"}
# ── Sans-serif ─────────────────────────────────────────────────────────
- name: NotoSansExtended
-50
View File
@@ -1,11 +1,8 @@
#include "Page.h"
#include <GfxRenderer.h>
#include <Logging.h>
#include <Serialization.h>
#include <new>
void PageLine::render(GfxRenderer& renderer, const int fontId, const int xOffset, const int yOffset) {
block->render(renderer, fontId, xPos + xOffset, yPos + yOffset);
}
@@ -51,47 +48,6 @@ std::unique_ptr<PageImage> PageImage::deserialize(FsFile& file) {
return std::unique_ptr<PageImage>(new PageImage(std::move(ib), xPos, yPos));
}
void PageHorizontalRule::render(GfxRenderer& renderer, const int fontId, const int xOffset, const int yOffset) {
(void)fontId;
if (width == 0 || thickness == 0) {
return;
}
renderer.drawLine(xPos + xOffset, yPos + yOffset, xPos + xOffset + width - 1, yPos + yOffset, thickness, true);
}
bool PageHorizontalRule::serialize(FsFile& file) {
serialization::writePod(file, xPos);
serialization::writePod(file, yPos);
serialization::writePod(file, width);
serialization::writePod(file, thickness);
return true;
}
std::unique_ptr<PageHorizontalRule> PageHorizontalRule::deserialize(FsFile& file) {
int16_t xPos = 0;
int16_t yPos = 0;
uint16_t width = 0;
uint8_t thickness = 0;
serialization::readPod(file, xPos);
serialization::readPod(file, yPos);
serialization::readPod(file, width);
serialization::readPod(file, thickness);
if (width == 0 || thickness == 0) {
LOG_ERR("PGE", "Deserialization failed: invalid horizontal rule metadata (width=%u thickness=%u)", width,
thickness);
return nullptr;
}
auto* rule = new (std::nothrow) PageHorizontalRule(width, thickness, xPos, yPos);
if (!rule) {
LOG_ERR("PGE", "Deserialization failed: could not allocate PageHorizontalRule");
return nullptr;
}
return std::unique_ptr<PageHorizontalRule>(rule);
}
void Page::render(GfxRenderer& renderer, const int fontId, const int xOffset, const int yOffset) const {
for (auto& element : elements) {
element->render(renderer, fontId, xOffset, yOffset);
@@ -142,12 +98,6 @@ std::unique_ptr<Page> Page::deserialize(FsFile& file) {
} else if (tag == TAG_PageImage) {
auto pi = PageImage::deserialize(file);
page->elements.push_back(std::move(pi));
} else if (tag == TAG_PageHorizontalRule) {
auto rule = PageHorizontalRule::deserialize(file);
if (!rule) {
return nullptr;
}
page->elements.push_back(std::move(rule));
} else {
LOG_ERR("PGE", "Deserialization failed: Unknown tag %u", tag);
return nullptr;
+1 -16
View File
@@ -12,8 +12,7 @@
enum PageElementTag : uint8_t {
TAG_PageLine = 1,
TAG_PageImage = 2,
TAG_PageHorizontalRule = 3,
TAG_PageImage = 2, // New tag
};
// represents something that has been added to a page
@@ -56,20 +55,6 @@ class PageImage final : public PageElement {
const ImageBlock& getImageBlock() const { return *imageBlock; }
};
class PageHorizontalRule final : public PageElement {
uint16_t width;
uint8_t thickness;
public:
PageHorizontalRule(uint16_t width, uint8_t thickness, const int16_t xPos, const int16_t yPos)
: PageElement(xPos, yPos), width(width), thickness(thickness) {}
void render(GfxRenderer& renderer, int fontId, int xOffset, int yOffset) override;
bool serialize(FsFile& file) override;
PageElementTag getTag() const override { return TAG_PageHorizontalRule; }
static std::unique_ptr<PageHorizontalRule> deserialize(FsFile& file);
};
class Page {
public:
// the list of block index and line numbers on this page
+1 -1
View File
@@ -10,7 +10,7 @@
#include "parsers/ChapterHtmlSlimParser.h"
namespace {
constexpr uint8_t SECTION_FILE_VERSION = 24;
constexpr uint8_t SECTION_FILE_VERSION = 23;
constexpr uint32_t HEADER_SIZE = sizeof(uint8_t) + sizeof(int) + sizeof(float) + sizeof(bool) + sizeof(uint8_t) +
sizeof(uint16_t) + sizeof(uint16_t) + sizeof(uint16_t) + sizeof(bool) + sizeof(bool) +
sizeof(uint8_t) + sizeof(bool) + sizeof(uint32_t) + sizeof(uint32_t) +
+2 -2
View File
@@ -4,8 +4,8 @@
#include <Logging.h>
#include <Serialization.h>
#include "Epub/converters/DirectPixelWriter.h"
#include "Epub/converters/ImageDecoderFactory.h"
#include "../converters/DirectPixelWriter.h"
#include "../converters/ImageDecoderFactory.h"
// Cache file format:
// - uint16_t width
@@ -5,10 +5,8 @@
#include <HalStorage.h>
#include <JPEGDEC.h>
#include <Logging.h>
#include <Memory.h>
#include <cstdlib>
#include <memory>
#include <new>
#include "DirectPixelWriter.h"
@@ -349,16 +347,16 @@ bool JpegToFramebufferConverter::getDimensionsStatic(const std::string& imagePat
return false;
}
std::unique_ptr<JPEGDEC> jpeg(new (std::nothrow) JPEGDEC());
JPEGDEC* jpeg = new (std::nothrow) JPEGDEC();
if (!jpeg) {
LOG_ERR("JPG", "Failed to allocate JPEG decoder for dimensions");
return false;
}
int rc = jpeg->open(imagePath.c_str(), jpegOpen, jpegClose, jpegRead, jpegSeek, nullptr);
const ScopedCleanup cleanup{[&jpeg]() { jpeg->close(); }};
if (rc != 1) {
LOG_ERR("JPG", "Failed to open JPEG for dimensions (err=%d): %s", jpeg->getLastError(), imagePath.c_str());
delete jpeg;
return false;
}
@@ -366,6 +364,8 @@ bool JpegToFramebufferConverter::getDimensionsStatic(const std::string& imagePat
out.height = jpeg->getHeight();
LOG_DBG("JPG", "Image dimensions: %dx%d", out.width, out.height);
jpeg->close();
delete jpeg;
return true;
}
@@ -379,7 +379,7 @@ bool JpegToFramebufferConverter::decodeToFramebuffer(const std::string& imagePat
return false;
}
std::unique_ptr<JPEGDEC> jpeg(new (std::nothrow) JPEGDEC());
JPEGDEC* jpeg = new (std::nothrow) JPEGDEC();
if (!jpeg) {
LOG_ERR("JPG", "Failed to allocate JPEG decoder");
return false;
@@ -392,9 +392,9 @@ bool JpegToFramebufferConverter::decodeToFramebuffer(const std::string& imagePat
ctx.screenHeight = renderer.getScreenHeight();
int rc = jpeg->open(imagePath.c_str(), jpegOpen, jpegClose, jpegRead, jpegSeek, jpegDrawCallback);
const ScopedCleanup cleanup{[&jpeg]() { jpeg->close(); }};
if (rc != 1) {
LOG_ERR("JPG", "Failed to open JPEG (err=%d): %s", jpeg->getLastError(), imagePath.c_str());
delete jpeg;
return false;
}
@@ -403,10 +403,14 @@ bool JpegToFramebufferConverter::decodeToFramebuffer(const std::string& imagePat
if (srcWidth <= 0 || srcHeight <= 0) {
LOG_ERR("JPG", "Invalid JPEG dimensions: %dx%d", srcWidth, srcHeight);
jpeg->close();
delete jpeg;
return false;
}
if (!validateImageDimensions(srcWidth, srcHeight, "JPEG")) {
jpeg->close();
delete jpeg;
return false;
}
@@ -449,6 +453,8 @@ bool JpegToFramebufferConverter::decodeToFramebuffer(const std::string& imagePat
if (destWidth <= 0 || destHeight <= 0) {
LOG_ERR("JPG", "Degenerate output dimensions %dx%d for %s, skipping render", destWidth, destHeight,
imagePath.c_str());
jpeg->close();
delete jpeg;
return false;
}
@@ -484,9 +490,13 @@ bool JpegToFramebufferConverter::decodeToFramebuffer(const std::string& imagePat
if (rc != 1) {
LOG_ERR("JPG", "Decode failed (rc=%d, lastError=%d)", rc, jpeg->getLastError());
jpeg->close();
delete jpeg;
return false;
}
jpeg->close();
delete jpeg;
LOG_DBG("JPG", "JPEG decoding complete - render time: %lu ms", decodeTime);
// Write cache file if caching was enabled
@@ -4,11 +4,9 @@
#include <GfxRenderer.h>
#include <HalStorage.h>
#include <Logging.h>
#include <Memory.h>
#include <PNGdec.h>
#include <cstdlib>
#include <memory>
#include <new>
#include "DirectPixelWriter.h"
@@ -246,7 +244,7 @@ bool PngToFramebufferConverter::getDimensionsStatic(const std::string& imagePath
return false;
}
std::unique_ptr<PNG> png(new (std::nothrow) PNG());
PNG* png = new (std::nothrow) PNG();
if (!png) {
LOG_ERR("PNG", "Failed to allocate PNG decoder for dimensions");
return false;
@@ -254,16 +252,18 @@ bool PngToFramebufferConverter::getDimensionsStatic(const std::string& imagePath
int rc = png->open(imagePath.c_str(), pngOpenWithHandle, pngCloseWithHandle, pngReadWithHandle, pngSeekWithHandle,
nullptr);
const ScopedCleanup cleanup{[&png]() { png->close(); }};
if (rc != 0) {
LOG_ERR("PNG", "Failed to open PNG for dimensions: %d", rc);
delete png;
return false;
}
out.width = png->getWidth();
out.height = png->getHeight();
png->close();
delete png;
return true;
}
@@ -278,7 +278,7 @@ bool PngToFramebufferConverter::decodeToFramebuffer(const std::string& imagePath
}
// Heap-allocate PNG decoder (~42 KB) - freed at end of function
std::unique_ptr<PNG> png(new (std::nothrow) PNG());
PNG* png = new (std::nothrow) PNG();
if (!png) {
LOG_ERR("PNG", "Failed to allocate PNG decoder");
return false;
@@ -292,13 +292,15 @@ bool PngToFramebufferConverter::decodeToFramebuffer(const std::string& imagePath
int rc = png->open(imagePath.c_str(), pngOpenWithHandle, pngCloseWithHandle, pngReadWithHandle, pngSeekWithHandle,
pngDrawCallback);
const ScopedCleanup cleanup{[&png]() { png->close(); }};
if (rc != PNG_SUCCESS) {
LOG_ERR("PNG", "Failed to open PNG: %d", rc);
delete png;
return false;
}
if (!validateImageDimensions(png->getWidth(), png->getHeight(), "PNG")) {
png->close();
delete png;
return false;
}
@@ -333,6 +335,8 @@ bool PngToFramebufferConverter::decodeToFramebuffer(const std::string& imagePath
"PNG row buffer too small: need %d bytes for width=%d type=%d, configured PNG_MAX_BUFFERED_PIXELS=%d",
requiredInternal, ctx.srcWidth, pixelType, PNG_MAX_BUFFERED_PIXELS);
LOG_ERR("PNG", "Aborting decode to avoid PNGdec internal buffer overflow");
png->close();
delete png;
return false;
}
@@ -345,6 +349,8 @@ bool PngToFramebufferConverter::decodeToFramebuffer(const std::string& imagePath
ctx.grayLineBuffer = static_cast<uint8_t*>(malloc(grayBufSize));
if (!ctx.grayLineBuffer) {
LOG_ERR("PNG", "Failed to allocate gray line buffer");
png->close();
delete png;
return false;
}
@@ -374,9 +380,13 @@ bool PngToFramebufferConverter::decodeToFramebuffer(const std::string& imagePath
if (rc != PNG_SUCCESS) {
LOG_ERR("PNG", "Decode failed: %d", rc);
png->close();
delete png;
return false;
}
png->close();
delete png;
LOG_DBG("PNG", "PNG decoding complete - render time: %lu ms", decodeTime);
// Write cache file if caching was enabled and buffer was allocated
@@ -13,19 +13,9 @@ uint32_t toLowerLatinImpl(const uint32_t cp) {
return cp + 0x20;
}
// Latin Extended-A (U+0100..U+017E): uppercase letters are paired with
// lowercase at cp+1. Two sub-ranges have different alignment:
// U+0100..U+0137: uppercase on EVEN codepoints
// U+0139..U+0148: uppercase on ODD codepoints
// U+014A..U+0177: uppercase on EVEN codepoints
// U+0179..U+017E: uppercase on ODD codepoints
// Covers Polish (Ą/ą, Ć/ć, Ę/ę, Ł/ł, Ń/ń, Ś/ś, Ź/ź, Ż/ż), Czech, Hungarian, Turkish, etc.
if ((cp >= 0x0100 && cp <= 0x0137 && (cp % 2 == 0)) || (cp >= 0x0139 && cp <= 0x0148 && (cp % 2 == 1)) ||
(cp >= 0x014A && cp <= 0x0177 && (cp % 2 == 0)) || (cp >= 0x0179 && cp <= 0x017E && (cp % 2 == 1))) {
return cp + 1;
}
switch (cp) {
case 0x0152: // Œ
return 0x0153; // œ
case 0x0178: // Ÿ
return 0x00FF; // ÿ
case 0x1E9E: // ẞ
@@ -64,11 +54,6 @@ bool isLatinLetter(const uint32_t cp) {
return true;
}
// Latin Extended-A (U+0100..U+017F): Polish, Czech, Hungarian, Turkish, etc.
if (cp >= 0x0100 && cp <= 0x017F) {
return true;
}
switch (cp) {
case 0x0152: // Œ
case 0x0153: // œ
@@ -288,30 +273,6 @@ std::vector<CodepointInfo> collectCodepoints(const std::string& word) {
case 0x0079:
composed = 0x00FD;
break; // y -> ý
case 0x0043:
composed = 0x0106;
break; // C -> Ć
case 0x0063:
composed = 0x0107;
break; // c -> ć
case 0x004E:
composed = 0x0143;
break; // N -> Ń
case 0x006E:
composed = 0x0144;
break; // n -> ń
case 0x0053:
composed = 0x015A;
break; // S -> Ś
case 0x0073:
composed = 0x015B;
break; // s -> ś
case 0x005A:
composed = 0x0179;
break; // Z -> Ź
case 0x007A:
composed = 0x017A;
break; // z -> ź
default:
break;
}
@@ -412,18 +373,6 @@ std::vector<CodepointInfo> collectCodepoints(const std::string& word) {
break;
}
break;
case 0x0307: // dot above (Polish Ż)
switch (prev) {
case 0x005A:
composed = 0x017B;
break; // Z -> Ż
case 0x007A:
composed = 0x017C;
break; // z -> ż
default:
break;
}
break;
case 0x0327: // cedilla
switch (prev) {
case 0x0043:
@@ -436,24 +385,6 @@ std::vector<CodepointInfo> collectCodepoints(const std::string& word) {
break;
}
break;
case 0x0328: // ogonek (Polish Ą, Ę)
switch (prev) {
case 0x0041:
composed = 0x0104;
break; // A -> Ą
case 0x0061:
composed = 0x0105;
break; // a -> ą
case 0x0045:
composed = 0x0118;
break; // E -> Ę
case 0x0065:
composed = 0x0119;
break; // e -> ę
default:
break;
}
break;
default:
break;
}
+4 -3
View File
@@ -22,9 +22,10 @@ struct Iso639Mapping {
const char* iso639_2;
const char* iso639_1;
};
static constexpr Iso639Mapping kIso639Mappings[] = {{"eng", "en"}, {"fra", "fr"}, {"fre", "fr"}, {"deu", "de"},
{"ger", "de"}, {"rus", "ru"}, {"spa", "es"}, {"ita", "it"},
{"ukr", "uk"}, {"swe", "sv"}};
static constexpr Iso639Mapping kIso639Mappings[] = {
{"eng", "en"}, {"fra", "fr"}, {"fre", "fr"}, {"deu", "de"}, {"ger", "de"},
{"rus", "ru"}, {"spa", "es"}, {"ita", "it"}, {"ukr", "uk"},
};
// Maps a BCP-47 or ISO 639-2 language tag to a language-specific hyphenator.
const LanguageHyphenator* hyphenatorForLanguage(const std::string& langTag) {
@@ -9,9 +9,7 @@
#include "generated/hyph-es.trie.h"
#include "generated/hyph-fr.trie.h"
#include "generated/hyph-it.trie.h"
#include "generated/hyph-pl.trie.h"
#include "generated/hyph-ru.trie.h"
#include "generated/hyph-sv.trie.h"
#include "generated/hyph-uk.trie.h"
namespace {
@@ -23,11 +21,9 @@ LanguageHyphenator germanHyphenator(de_patterns, isLatinLetter, toLowerLatin);
LanguageHyphenator russianHyphenator(ru_patterns, isCyrillicLetter, toLowerCyrillic);
LanguageHyphenator spanishHyphenator(es_patterns, isLatinLetter, toLowerLatin);
LanguageHyphenator italianHyphenator(it_patterns, isLatinLetter, toLowerLatin);
LanguageHyphenator swedishHyphenator(sv_patterns, isLatinLetter, toLowerLatin);
LanguageHyphenator ukrainianHyphenator(uk_patterns, isCyrillicLetter, toLowerCyrillic);
LanguageHyphenator polishHyphenator(pl_patterns, isLatinLetter, toLowerLatin);
using EntryArray = std::array<LanguageEntry, 9>;
using EntryArray = std::array<LanguageEntry, 7>;
const EntryArray& entries() {
static const EntryArray kEntries = {{{"english", "en", &englishHyphenator},
@@ -36,8 +32,6 @@ const EntryArray& entries() {
{"russian", "ru", &russianHyphenator},
{"spanish", "es", &spanishHyphenator},
{"italian", "it", &italianHyphenator},
{"polish", "pl", &polishHyphenator},
{"swedish", "sv", &swedishHyphenator},
{"ukrainian", "uk", &ukrainianHyphenator}}};
return kEntries;
}
@@ -3,7 +3,7 @@
#include <cstddef>
#include <cstdint>
#include "Epub/hyphenation/SerializedHyphenationTrie.h"
#include "../SerializedHyphenationTrie.h"
// Auto-generated by generate_hyphenation_trie.py. Do not edit manually.
alignas(4) constexpr uint8_t de_trie_data[] = {
@@ -3,7 +3,7 @@
#include <cstddef>
#include <cstdint>
#include "Epub/hyphenation/SerializedHyphenationTrie.h"
#include "../SerializedHyphenationTrie.h"
// Auto-generated by generate_hyphenation_trie.py. Do not edit manually.
alignas(4) constexpr uint8_t en_trie_data[] = {
Loaded 20 of 177 files, more files were not shown because too many files have changed in this diff. Show more