Compare commits
167 Commits
master
...
255b98bda0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
255b98bda0
|
||
|
|
0d8a3fdbdd
|
||
|
|
5ca72ef231
|
||
|
|
422cad7bc5
|
||
|
|
1b628a9223
|
||
|
|
633d56195a
|
||
|
|
83aa38d1a2
|
||
|
|
7fe093b57a
|
||
|
|
867faad916
|
||
|
|
f2a2b03074
|
||
|
|
e43f763201
|
||
|
|
ad843d8edc
|
||
|
|
0d828ba986
|
||
|
|
cc90d7c755
|
||
|
|
dac087f391
|
||
|
|
022f5197d7
|
||
|
|
a5ca15df4f
|
||
|
|
22c189281c
|
||
|
|
0493f300be
|
||
|
|
f44657aeba
|
||
|
|
4627ec95f9
|
||
|
|
9464df1727
|
||
|
|
60a3e21c0e
|
||
|
|
30473c27d3
|
||
|
|
d1ee45592e
|
||
|
|
bd2cea8b8d
|
||
|
|
66a754dabd
|
||
|
|
dfbc931c14
|
||
|
|
bf604add85
|
||
|
|
170cc25774 | ||
|
|
c40e92e4d1 | ||
|
|
4d22256745 | ||
|
|
18b36efbae | ||
|
|
a35f372e1b | ||
|
|
4ef433e373 | ||
|
|
a5d7e03f54 | ||
|
|
047b0029c9 | ||
|
|
c3f1dbfa09 | ||
|
|
ea88797c8e | ||
|
|
218201bd1f | ||
|
|
6ee05b08a1 | ||
|
|
a826569a0f | ||
|
|
88594077aa | ||
|
|
ce0b439aa3 | ||
|
|
019587bb77 | ||
|
|
4388bf8cc7 | ||
|
|
6de8b7a666 | ||
|
|
c09f7b4a22
|
||
|
|
7eaced602f
|
||
|
|
f955cf2fb4
|
||
|
|
307a6608f0 | ||
|
|
a350492571 | ||
|
|
ef02737c89 | ||
|
|
aff93f1dc0 | ||
|
|
3628d8eb37
|
||
|
|
42011d5977
|
||
|
|
2aa13ea2de
|
||
|
|
19b6ad047b
|
||
|
|
2eae521b6a
|
||
|
|
9d9bc019a2
|
||
|
|
ff33b2b3be
|
||
|
|
0e2440aea8
|
||
|
|
39ef1e6d78
|
||
|
|
3cc127d658
|
||
|
|
98146f2545
|
||
|
|
f5b708424d
|
||
|
|
1c19899aa3
|
||
|
|
390f10f30d
|
||
|
|
49471e36f1
|
||
|
|
c44ac0272a
|
||
|
|
29954a3683
|
||
|
|
3eddb07a1a
|
||
|
|
f443f5dde0
|
||
|
|
3d51dfeeb7
|
||
|
|
4dadea1a03
|
||
|
|
0d9a1f4f89
|
||
|
|
1b350656a5
|
||
|
|
51dc498768
|
||
|
|
406c3aeace
|
||
|
|
55a1fef01a
|
||
|
|
18be265a4a
|
||
|
|
3a0641889f
|
||
|
|
ad282cadfe
|
||
|
|
c8ba4fe973
|
||
|
|
c1b8e53138
|
||
|
|
0fda9031fd
|
||
|
|
013a738144
|
||
|
|
c1d1e98909
|
||
|
|
6403ce6309
|
||
|
|
109f95df78
|
||
|
|
de981f5072
|
||
|
|
2bcc1c1495
|
||
|
|
aa7c0a882a
|
||
|
|
950faf4cd2
|
||
|
|
e5d574a07a
|
||
|
|
c8ddb6b61d
|
||
|
|
ef52af1a52
|
||
|
|
8a28755c69
|
||
|
|
4b713f40f1
|
||
|
|
ab5e18aca3
|
||
|
|
a8f0d63693
|
||
|
|
a8a89e35b8
|
||
|
|
724c1969b9
|
||
|
|
21b81bd177
|
||
|
|
b5c48af3b2
|
||
|
|
426a978e44
|
||
|
|
a1ac11ab51
|
||
|
|
7819cf0f77
|
||
|
|
3d7340ca6f
|
||
|
|
966fbef3d1
|
||
|
|
38a87298f3
|
||
|
|
ab4540b26f
|
||
|
|
7e15c9835f
|
||
|
|
7b3de29c59
|
||
|
|
1d7971ae60
|
||
|
|
61fb11cae3
|
||
|
|
424e332c75
|
||
|
|
f21720dc79
|
||
|
|
a9f5149444
|
||
|
|
0222cbf19b
|
||
|
|
02f2474e3b
|
||
|
|
f06e3a0a82
|
||
|
|
a585f219f4
|
||
|
|
df6cc637ec
|
||
|
|
4cfe155488
|
||
|
|
f1966f1e26
|
||
|
|
ebcd3a8b94
|
||
|
|
ed8a0feac1
|
||
|
|
12cc7de49e
|
||
|
|
f622e87c10
|
||
|
|
24c1df0308
|
||
|
|
6cc68e828a
|
||
|
|
6097ee03df
|
||
|
|
d11ad45e59
|
||
|
|
b965ce9fb7
|
||
|
|
744d6160e8
|
||
|
|
66f703df69
|
||
|
|
19004eefaa
|
||
|
|
f90aebc891
|
||
|
|
3096d6066b
|
||
|
|
1383d75c84
|
||
|
|
632b76c9ed
|
||
|
|
5dc9d21bdb
|
||
|
|
c1dfe92ea3
|
||
|
|
82bfbd8fa6
|
||
|
|
6aa0b865c2
|
||
|
|
0c71e0b13f
|
||
|
|
ea11d2f7d3
|
||
|
|
31878a77bc
|
||
|
|
21a75c624d
|
||
|
|
8d4bbf284d
|
||
|
|
905f694576
|
||
|
|
e798065a5c
|
||
|
|
5e269f912f
|
||
|
|
182c236050
|
||
|
|
73cd05827a | ||
|
|
ea32ba0f8d | ||
|
|
f7b1113819 | ||
|
|
228a1cb511 | ||
|
|
b72283d304 | ||
|
|
8cf226613b | ||
|
|
d4f25c44bf | ||
|
|
bc12556da1 | ||
|
|
4e7bb8979c | ||
|
|
4edb14bdd9
|
||
|
|
eb79b98f2b | ||
|
|
a85d5e627b
|
171
.cursor/plans/ttf_font_investigation_61ba7279.plan.md
Normal file
171
.cursor/plans/ttf_font_investigation_61ba7279.plan.md
Normal file
@@ -0,0 +1,171 @@
|
||||
---
|
||||
name: TTF Font Investigation
|
||||
overview: Investigate replacing compile-time bitmap fonts with runtime TTF rendering using stb_truetype (the core of lvgl-ttf-esp32), integrated into the existing custom GfxRenderer pipeline for the ESP32-C3 e-ink reader.
|
||||
todos:
|
||||
- id: poc-stb
|
||||
content: "Phase 1: Add stb_truetype.h and build a minimal proof-of-concept that loads a TTF from SD, rasterizes glyphs, and draws them via GfxRenderer"
|
||||
status: pending
|
||||
- id: measure-ram
|
||||
content: "Phase 1: Measure actual RAM consumption and render performance of stb_truetype on ESP32-C3"
|
||||
status: pending
|
||||
- id: spiffs-mmap
|
||||
content: "Phase 3: Test SPIFFS memory-mapping of TTF files using esp_partition_mmap() to avoid loading into RAM"
|
||||
status: pending
|
||||
- id: font-provider
|
||||
content: "Phase 2: Create FontProvider abstraction layer and TtfFontProvider with glyph caching"
|
||||
status: pending
|
||||
- id: renderer-refactor
|
||||
content: "Phase 2: Refactor GfxRenderer to use FontProvider interface instead of direct EpdFontFamily"
|
||||
status: pending
|
||||
- id: settings-integration
|
||||
content: "Phase 4: Update settings to support arbitrary font sizes and custom font selection"
|
||||
status: pending
|
||||
- id: remove-bitmap-fonts
|
||||
content: "Phase 5: Remove compiled bitmap reader fonts, keep only small UI bitmap fonts"
|
||||
status: pending
|
||||
isProject: false
|
||||
---
|
||||
|
||||
# TTF Font Rendering Investigation
|
||||
|
||||
## Current State
|
||||
|
||||
The project uses **no LVGL** -- it has a custom `GfxRenderer` that draws directly into an e-ink framebuffer. Fonts are pre-rasterized offline (TTF -> Python FreeType script -> C header bitmaps) and embedded at compile time.
|
||||
|
||||
**Cost of current approach:**
|
||||
|
||||
- **~2.7 MB flash** (mod build, Bookerly + NotoSans + Ubuntu) up to **~7 MB** (full build with OpenDyslexic)
|
||||
- **Only 4 discrete sizes** per family (12/14/16/18 pt) -- no runtime scaling
|
||||
- Each size x style (regular/bold/italic/bold-italic) is a separate ~80-200 KB bitmap blob
|
||||
- App partition is only **6.25 MB** -- fonts consume 43-100%+ of available space
|
||||
|
||||
## Why lvgl-ttf-esp32 Is Relevant (and What Isn't)
|
||||
|
||||
The [lvgl-ttf-esp32](https://github.com/huming2207/lvgl-ttf-esp32) repo wraps **stb_truetype** (a single-header C library) with an LVGL font driver. Since this project does not use LVGL, the wrapper is irrelevant, but the **stb_truetype library itself** is exactly what's needed -- a lightweight, zero-dependency TTF rasterizer that runs on ESP32.
|
||||
|
||||
## Proposed Architecture
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
subgraph current [Current Pipeline]
|
||||
TTF_Offline["TTF files (offline)"] --> fontconvert["fontconvert.py (FreeType)"]
|
||||
fontconvert --> headers["56 .h files (~2.7-7 MB flash)"]
|
||||
headers --> EpdFont["EpdFont / EpdFontFamily"]
|
||||
EpdFont --> GfxRenderer["GfxRenderer::renderChar()"]
|
||||
end
|
||||
|
||||
subgraph proposed [Proposed Pipeline]
|
||||
TTF_SD["TTF files on SD card (~100-500 KB each)"] --> stb["stb_truetype.h (runtime)"]
|
||||
stb --> cache["Glyph cache (RAM + SD)"]
|
||||
cache --> TtfFont["TtfFont (new class)"]
|
||||
TtfFont --> FontProvider["FontProvider interface"]
|
||||
FontProvider --> GfxRenderer2["GfxRenderer::renderChar()"]
|
||||
end
|
||||
```
|
||||
|
||||
|
||||
|
||||
### Core Idea
|
||||
|
||||
1. **stb_truetype.h** -- add as a single header file in `lib/`. It rasterizes individual glyphs from TTF data on demand.
|
||||
2. **TTF files on SD card** -- load at runtime from `.crosspoint/fonts/`. A typical TTF family (4 styles) is ~400-800 KB total vs 2.7 MB+ as bitmaps.
|
||||
3. **Glyph cache** -- since e-ink pages are static, cache rasterized glyphs in RAM (LRU, ~20-50 KB) and optionally persist to SD card to avoid re-rasterizing across page turns.
|
||||
4. `**FontProvider` abstraction** -- interface over both `EpdFont` (bitmap, for UI fonts) and new `TtfFont` (runtime, for reader fonts), so both can coexist.
|
||||
|
||||
## Integration Points
|
||||
|
||||
These are the key files/interfaces that would need changes:
|
||||
|
||||
|
||||
| Component | File | Change |
|
||||
| ----------------- | -------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------- |
|
||||
| Font abstraction | New `lib/FontProvider/` | `FontProvider` interface with `getGlyph()`, `getMetrics()` |
|
||||
| TTF renderer | New `lib/TtfFont/` | Wraps stb_truetype, manages TTF loading + glyph cache |
|
||||
| GfxRenderer | [lib/GfxRenderer/GfxRenderer.h](lib/GfxRenderer/GfxRenderer.h) | Change `fontMap` from `EpdFontFamily` to `FontProvider*`; update `renderChar`, `getTextWidth`, `getSpaceWidth` |
|
||||
| Font registration | [src/main.cpp](src/main.cpp) | Register TTF fonts from SD instead of (or alongside) bitmap fonts |
|
||||
| Settings | [src/CrossPointSettings.cpp](src/CrossPointSettings.cpp) | `getReaderFontId()` supports arbitrary sizes, not just 4 discrete ones |
|
||||
| PlaceholderCover | [lib/PlaceholderCover/PlaceholderCoverGenerator.cpp](lib/PlaceholderCover/PlaceholderCoverGenerator.cpp) | Uses own `renderGlyph()` -- needs similar adaptation |
|
||||
| Text layout | [lib/Epub/Epub/ParsedText.cpp](lib/Epub/Epub/ParsedText.cpp) | Uses `getTextWidth()` / `getSpaceWidth()` for line breaking -- works unchanged if FontProvider is transparent |
|
||||
|
||||
|
||||
## Feasibility Analysis
|
||||
|
||||
### Memory (ESP32-C3, ~380 KB RAM)
|
||||
|
||||
- **stb_truetype itself**: ~15-20 KB code in flash, minimal RAM overhead
|
||||
- **TTF file in memory**: requires the full TTF loaded into RAM for glyph access. Options:
|
||||
- **Memory-mapped from flash (SPIFFS)**: store TTF in SPIFFS (3.4 MB available, currently unused), memory-map via `mmap()` on ESP-IDF -- zero RAM cost
|
||||
- **Partial loading from SD**: read only needed tables on demand (stb_truetype supports custom `stbtt_read()` but the default API expects full file in memory)
|
||||
- **Load into PSRAM**: ESP32-C3 has no PSRAM, so this is not an option
|
||||
- **Glyph cache**: ~50 bytes metadata + bitmap per glyph. At 18pt, a glyph bitmap is ~20x25 pixels = ~63 bytes (1-bit). Caching 256 glyphs = ~30 KB RAM.
|
||||
- **Rasterization temp buffer**: stb_truetype allocates ~10-20 KB temporarily per glyph render (uses `malloc`)
|
||||
|
||||
**Verdict**: The biggest constraint is holding the TTF file in RAM. A typical Bookerly-Regular.ttf is ~150 KB. With 4 styles loaded, that's ~600 KB -- **too much for 380 KB RAM**. The viable path is using **SPIFFS** to store TTFs and memory-map them, or implementing a chunked reader that loads TTF table data on demand from SD.
|
||||
|
||||
### Flash Savings
|
||||
|
||||
- **Remove**: 2.7-7 MB of bitmap font headers from firmware
|
||||
- **Add**: ~40 KB for stb_truetype + TtfFont code
|
||||
- **Net savings**: **2.6-6.9 MB flash freed**
|
||||
- TTF files move to SD card or SPIFFS (not in firmware)
|
||||
|
||||
### Performance
|
||||
|
||||
- stb_truetype rasterizes a glyph in **~0.5-2 ms** on ESP32 (160 MHz)
|
||||
- A typical page has ~~200-300 glyphs, but with caching, only unique glyphs need rasterizing (~~60-80 per page)
|
||||
- **First page render**: ~60-160 ms extra for cache warmup
|
||||
- **Subsequent pages**: mostly cache hits, negligible overhead
|
||||
- E-ink refresh takes ~300-1000 ms anyway, so TTF rasterization cost is acceptable
|
||||
|
||||
### Anti-aliasing for E-ink
|
||||
|
||||
stb_truetype produces 8-bit alpha bitmaps (256 levels). The current system uses 1-bit or 2-bit glyphs. The adapter would:
|
||||
|
||||
- **1-bit mode**: threshold the alpha (e.g., alpha > 128 = black)
|
||||
- **2-bit mode**: quantize to 4 levels (0, 85, 170, 255) for e-ink grayscale
|
||||
|
||||
This should actually produce **better quality** than the offline FreeType conversion since stb_truetype does sub-pixel hinting.
|
||||
|
||||
## Recommended Implementation Phases
|
||||
|
||||
### Phase 1: Proof of Concept (stb_truetype standalone)
|
||||
|
||||
- Add stb_truetype.h to the project
|
||||
- Write a minimal test that loads a TTF from SD, rasterizes a few glyphs, and draws them via `GfxRenderer::drawPixel()`
|
||||
- Measure RAM usage and render time
|
||||
- Validate glyph quality on e-ink
|
||||
|
||||
### Phase 2: FontProvider Abstraction
|
||||
|
||||
- Create `FontProvider` interface matching `EpdFontFamily`'s public API
|
||||
- Wrap existing `EpdFontFamily` in a `BitmapFontProvider`
|
||||
- Create `TtfFontProvider` backed by stb_truetype + glyph cache
|
||||
- Refactor `GfxRenderer::fontMap` to use `FontProvider*`
|
||||
|
||||
### Phase 3: TTF Storage Strategy
|
||||
|
||||
- Evaluate SPIFFS memory mapping vs. SD-card chunked loading
|
||||
- Implement the chosen strategy
|
||||
- Handle font discovery (scan SD card for `.ttf` files)
|
||||
|
||||
### Phase 4: Settings and UI Integration
|
||||
|
||||
- Replace discrete font-size enum with a continuous size setting (or finer granularity)
|
||||
- Add "Custom Font" option in settings
|
||||
- Update section cache invalidation when font/size changes
|
||||
|
||||
### Phase 5: Remove Bitmap Reader Fonts
|
||||
|
||||
- Keep bitmap fonts only for UI (Ubuntu 10/12, NotoSans 8) which are small (~62 KB)
|
||||
- Remove Bookerly, NotoSans, OpenDyslexic bitmap headers
|
||||
- Ship TTF files on SD card (or downloadable)
|
||||
|
||||
## Key Risk: TTF-in-RAM on ESP32-C3
|
||||
|
||||
The critical question is whether TTF file data can be accessed without loading the full file into RAM. Three mitigation strategies:
|
||||
|
||||
1. **SPIFFS + mmap**: Store TTFs in the 3.4 MB SPIFFS partition and use ESP-IDF's `esp_partition_mmap()` to map them into the address space. Zero RAM cost, but SPIFFS is read-only after flashing (unless written at runtime).
|
||||
2. **SD card + custom I/O**: Implement `stbtt_GetFontOffsetForIndex` and glyph extraction using buffered SD reads. stb_truetype's API assumes a contiguous byte array, so this would require a patched or wrapper approach.
|
||||
3. **Load one style at a time**: Only keep the active style's TTF in RAM (~150 KB). Switch styles by unloading/reloading. Feasible but adds latency on style changes (bold/italic).
|
||||
|
||||
Strategy 1 (SPIFFS mmap) is the most promising since the SPIFFS partition is already allocated but unused.
|
||||
@@ -110,7 +110,7 @@ These flags in `platformio.ini` fundamentally affect firmware behavior:
|
||||
- Only ONE framebuffer exists (not double-buffered)
|
||||
- Grayscale rendering requires temporary buffer allocation (`renderer.storeBwBuffer()`)
|
||||
- Must call `renderer.restoreBwBuffer()` to free temporary buffers
|
||||
- See [lib/GfxRenderer/GfxRenderer.cpp:439-440](lib/GfxRenderer/GfxRenderer.cpp) for malloc usage
|
||||
- See [lib/GfxRenderer/GfxRenderer.cpp:439-440](../lib/GfxRenderer/GfxRenderer.cpp) for malloc usage
|
||||
|
||||
### Directory Structure
|
||||
* lib/: Internal libraries (Epub engine, GfxRenderer, UITheme, I18n)
|
||||
@@ -130,7 +130,7 @@ These flags in `platformio.ini` fundamentally affect firmware behavior:
|
||||
| `HalGPIO` | `InputManager` | Button input handling | *(none)* |
|
||||
| `HalStorage` | `SDCardManager` | SD card file I/O | `Storage` |
|
||||
|
||||
**Location**: [lib/hal/](lib/hal/)
|
||||
**Location**: [lib/hal/](../lib/hal/)
|
||||
|
||||
**Why HAL?**
|
||||
- Provides consistent error logging per module
|
||||
@@ -247,7 +247,7 @@ When a template is necessary, limit instantiations: use explicit template instan
|
||||
|
||||
### Error Handling Philosophy
|
||||
|
||||
**Source**: [src/main.cpp:132-143](src/main.cpp), [lib/GfxRenderer/GfxRenderer.cpp:10](lib/GfxRenderer/GfxRenderer.cpp)
|
||||
**Source**: [src/main.cpp:132-143](../src/main.cpp), [lib/GfxRenderer/GfxRenderer.cpp:10](../lib/GfxRenderer/GfxRenderer.cpp)
|
||||
|
||||
**Pattern Hierarchy**:
|
||||
1. **LOG_ERR + return false** (90%): `LOG_ERR("MOD", "Failed: %s", reason); return false;`
|
||||
@@ -259,7 +259,7 @@ When a template is necessary, limit instantiations: use explicit template instan
|
||||
|
||||
### Acceptable malloc/free Patterns
|
||||
|
||||
**Source**: [src/activities/home/HomeActivity.cpp:166](src/activities/home/HomeActivity.cpp), [lib/GfxRenderer/GfxRenderer.cpp:439-440](lib/GfxRenderer/GfxRenderer.cpp)
|
||||
**Source**: [src/activities/home/HomeActivity.cpp:166](../src/activities/home/HomeActivity.cpp), [lib/GfxRenderer/GfxRenderer.cpp:439-440](../lib/GfxRenderer/GfxRenderer.cpp)
|
||||
|
||||
Despite "prefer stack allocation," malloc is acceptable for:
|
||||
1. **Large temporary buffers** (> 256 bytes, won't fit on stack)
|
||||
@@ -290,10 +290,10 @@ buffer = nullptr;
|
||||
- **Document size**: Comment why stack allocation was rejected
|
||||
|
||||
**Examples in codebase**:
|
||||
- Cover image buffers: [HomeActivity.cpp:166](src/activities/home/HomeActivity.cpp#L166)
|
||||
- Text chunk buffers: [TxtReaderActivity.cpp:259](src/activities/reader/TxtReaderActivity.cpp#L259)
|
||||
- Bitmap rendering: [GfxRenderer.cpp:439-440](lib/GfxRenderer/GfxRenderer.cpp#L439-L440)
|
||||
- OTA update buffer: [OtaUpdater.cpp:40](src/network/OtaUpdater.cpp#L40)
|
||||
- Cover image buffers: [HomeActivity.cpp:166](../src/activities/home/HomeActivity.cpp)
|
||||
- Text chunk buffers: [TxtReaderActivity.cpp:259](../src/activities/reader/TxtReaderActivity.cpp)
|
||||
- Bitmap rendering: [GfxRenderer.cpp:439-440](../lib/GfxRenderer/GfxRenderer.cpp)
|
||||
- OTA update buffer: [OtaUpdater.cpp:40](../src/network/OtaUpdater.cpp)
|
||||
|
||||
---
|
||||
|
||||
@@ -305,7 +305,7 @@ buffer = nullptr;
|
||||
|
||||
### Logical Button Mapping
|
||||
|
||||
**Source**: [src/MappedInputManager.cpp:20-55](src/MappedInputManager.cpp)
|
||||
**Source**: [src/MappedInputManager.cpp:20-55](../src/MappedInputManager.cpp)
|
||||
|
||||
Constraint: Physical button positions are fixed on hardware, but their logical functions change based on user settings and screen orientation.
|
||||
|
||||
@@ -352,7 +352,7 @@ Constraint: Physical button positions are fixed on hardware, but their logical f
|
||||
|
||||
### Activity Lifecycle and Memory Management
|
||||
|
||||
**Source**: [src/main.cpp:132-143](src/main.cpp)
|
||||
**Source**: [src/main.cpp:132-143](../src/main.cpp)
|
||||
|
||||
**CRITICAL**: Activities are **heap-allocated** and **deleted on exit**.
|
||||
|
||||
@@ -389,7 +389,7 @@ void onExit() { /* free: vTaskDelete, free buffer, close files */ Activity::on
|
||||
|
||||
### FreeRTOS Task Guidelines
|
||||
|
||||
**Source**: [src/activities/util/KeyboardEntryActivity.cpp:45-50](src/activities/util/KeyboardEntryActivity.cpp)
|
||||
**Source**: [src/activities/util/KeyboardEntryActivity.cpp:45-50](../src/activities/util/KeyboardEntryActivity.cpp)
|
||||
|
||||
**Pattern**: See Activity Lifecycle above. `xTaskCreate(&taskTrampoline, "Name", stackSize, this, 1, &handle)`
|
||||
|
||||
@@ -402,7 +402,7 @@ void onExit() { /* free: vTaskDelete, free buffer, close files */ Activity::on
|
||||
|
||||
### Global Font Loading
|
||||
|
||||
**Source**: [src/main.cpp:40-115](src/main.cpp)
|
||||
**Source**: [src/main.cpp:40-115](../src/main.cpp)
|
||||
|
||||
**All fonts are loaded as global static objects** at firmware startup:
|
||||
- Bookerly: 12, 14, 16, 18pt (4 styles each: regular, bold, italic, bold-italic)
|
||||
@@ -423,7 +423,7 @@ void onExit() { /* free: vTaskDelete, free buffer, close files */ Activity::on
|
||||
- Fonts stored in **Flash** (marked as `static const` in `lib/EpdFont/builtinFonts/`)
|
||||
- Font rendering data cached in **DRAM** when first used
|
||||
- `OMIT_FONTS` can reduce binary size for minimal builds
|
||||
- Font IDs defined in [src/fontIds.h](src/fontIds.h)
|
||||
- Font IDs defined in [src/fontIds.h](../src/fontIds.h)
|
||||
|
||||
**Usage**:
|
||||
```cpp
|
||||
@@ -517,7 +517,7 @@ clang-format -i src/**/*.cpp src/**/*.h
|
||||
4. **Corrupt Cache Files**:
|
||||
- Delete `.crosspoint/` directory on SD card
|
||||
- Forces clean re-parse of all EPUBs
|
||||
- Check file format versions in [docs/file-formats.md](docs/file-formats.md)
|
||||
- Check file format versions in [docs/file-formats.md](../docs/file-formats.md)
|
||||
|
||||
5. **Watchdog Timeout**:
|
||||
- Loop/task blocked for >5 seconds
|
||||
|
||||
8
SCOPE.md
8
SCOPE.md
@@ -39,6 +39,14 @@ usability over "swiss-army-knife" functionality.
|
||||
* **Complex Annotation:** No typed out notes. These features are better suited for devices with better input
|
||||
capabilities and more powerful chips.
|
||||
|
||||
### In-scope — Technically Unsupported
|
||||
|
||||
*These features align with CrossPoint's goals but are impractical on the current hardware or produce poor UX.*
|
||||
|
||||
* **Clock Display:** The ESP32-C3's RTC drifts significantly during deep sleep; making the clock untrustworthy after any sleep cycle. NTP sync could help, but CrossPoint doesn't connect to the internet on every boot.
|
||||
|
||||
* **PDF Rendering:** PDFs are fixed-layout documents, so rendering them requires displaying pages as images rather than reflowable text — resulting in constant panning and zooming that makes for a poor reading experience on e-ink.
|
||||
|
||||
## 3. Idea Evaluation
|
||||
|
||||
While I appreciate the desire to add new and exciting features to CrossPoint Reader, CrossPoint Reader is designed to be
|
||||
|
||||
@@ -308,13 +308,30 @@ If you use the HTTPS listener, use `https://<server-ip>:7200` (`curl -k` only fo
|
||||
|
||||
### 3.7 Sleep Screen
|
||||
|
||||
You can customize the sleep screen by placing custom images in specific locations on the SD card:
|
||||
The **Sleep Screen** setting controls what is displayed when the device goes to sleep:
|
||||
|
||||
- **Single Image:** Place a file named `sleep.bmp` in the root directory.
|
||||
- **Multiple Images:** Create a `sleep` directory in the root of the SD card and place any number of `.bmp` images inside. If images are found in this directory, they will take priority over the `sleep.bmp` file, and one will be randomly selected each time the device sleeps.
|
||||
| Mode | Behavior |
|
||||
|------|----------|
|
||||
| **Dark** (default) | The CrossPoint logo on a dark background. |
|
||||
| **Light** | The CrossPoint logo on a white background. |
|
||||
| **Custom** | A custom image from the SD card (see below). Falls back to **Dark** if no custom image is found. |
|
||||
| **Cover** | The cover of the currently open book. Falls back to **Dark** if no book is open. |
|
||||
| **Cover + Custom** | The cover of the currently open book. Falls back to **Custom** behavior if no book is open. |
|
||||
| **None** | A blank screen. |
|
||||
|
||||
> [!NOTE]
|
||||
> You'll need to set the **Sleep Screen** setting to **Custom** in order to use these images.
|
||||
#### Cover settings
|
||||
|
||||
When using **Cover** or **Cover + Custom**, two additional settings apply:
|
||||
|
||||
- **Sleep Screen Cover Mode**: **Fit** (scale to fit, white borders) or **Crop** (scale and crop to fill the screen).
|
||||
- **Sleep Screen Cover Filter**: **None** (grayscale), **Contrast** (black & white), or **Inverted** (inverted black & white).
|
||||
|
||||
#### Custom images
|
||||
|
||||
To use custom sleep images, set the sleep screen mode to **Custom** or **Cover + Custom**, then place images on the SD card:
|
||||
|
||||
- **Multiple Images (recommended):** Create a `.sleep` directory in the root of the SD card and place any number of `.bmp` images inside. One will be randomly selected each time the device sleeps. (A directory named `sleep` is also accepted as a fallback.)
|
||||
- **Single Image:** Place a file named `sleep.bmp` in the root directory. This is used as a fallback if no valid images are found in the `.sleep`/`sleep` directory.
|
||||
|
||||
> [!TIP]
|
||||
> For best results:
|
||||
|
||||
21
chat-summaries/2026-02-09_00-00-summary.md
Normal file
21
chat-summaries/2026-02-09_00-00-summary.md
Normal file
@@ -0,0 +1,21 @@
|
||||
# Project Documentation for CrossPoint Reader Firmware
|
||||
|
||||
**Date:** 2026-02-09
|
||||
|
||||
## Task
|
||||
|
||||
Create three documentation files in `/mod/docs/` covering the Xteink X4 hardware capabilities, project file structure, and CI/build/code style for the CrossPoint Reader firmware.
|
||||
|
||||
## Changes Made
|
||||
|
||||
### New Files
|
||||
|
||||
1. **`mod/docs/hardware.md`** -- Device hardware capabilities documentation covering CPU (ESP32-C3), 16MB flash with partition layout, 480x800 eink display (2-bit grayscale, 3 refresh modes), 7 physical buttons, SD card storage, battery monitoring, WiFi networking, USB-C, and power management (deep sleep with GPIO wakeup).
|
||||
|
||||
2. **`mod/docs/file-structure.md`** -- Complete project file structure map documenting `src/` (main app with Activity-based UI system), `lib/` (16 libraries including EPUB engine, HAL, fonts, XML/ZIP/JPEG support), `open-x4-sdk/` (hardware driver submodule), `scripts/` (build helpers), `test/` (hyphenation tests), `docs/` (upstream docs), and `.github/` (CI/templates).
|
||||
|
||||
3. **`mod/docs/ci-build-and-code-style.md`** -- CI pipeline documentation covering 4 GitHub Actions workflows (CI with format/cppcheck/build/gate jobs, release, RC, PR title check), PlatformIO build system with 3 environments, clang-format 21 rules (2-space indent, 120-col limit, K&R braces), cppcheck static analysis config, and contribution guidelines (semantic PR titles, AI disclosure, scope alignment).
|
||||
|
||||
## Follow-up Items
|
||||
|
||||
- None. All three documents are complete and ready for review.
|
||||
59
chat-summaries/2026-02-09_04-43-summary.md
Normal file
59
chat-summaries/2026-02-09_04-43-summary.md
Normal file
@@ -0,0 +1,59 @@
|
||||
# Sleep Screen Tweaks Implementation
|
||||
|
||||
## Task Description
|
||||
Implemented the two "Sleep Screen tweaks" from the plan:
|
||||
1. **Gradient fill for letterboxed areas** - When a sleep screen image doesn't match the display's aspect ratio, the void (letterbox) areas are now filled with a dithered gradient sampled from the nearest ~20 pixels of the image's edge, fading toward white.
|
||||
2. **Fix "Fit" mode for small images** - Images smaller than the 480x800 display are now scaled up (nearest-neighbor) to fit while preserving aspect ratio, instead of being displayed at native size with wasted screen space.
|
||||
|
||||
## Changes Made
|
||||
|
||||
### `lib/GfxRenderer/GfxRenderer.cpp`
|
||||
- Modified `drawBitmap()` scaling logic: when both `maxWidth` and `maxHeight` are provided, always computes an aspect-ratio-preserving scale factor (supports both upscaling and downscaling)
|
||||
- Modified `drawBitmap()` rendering loop: uses block-fill approach where each source pixel maps to a screen rectangle (handles both upscaling blocks and 1:1/downscaling single pixels via a unified loop)
|
||||
- Applied same changes to `drawBitmap1Bit()` for 1-bit bitmap consistency
|
||||
- Added `drawPixelGray()` method: draws a pixel using its 2-bit grayscale value, dispatching correctly based on the current render mode (BW, GRAYSCALE_LSB, GRAYSCALE_MSB)
|
||||
|
||||
### `lib/GfxRenderer/GfxRenderer.h`
|
||||
- Added `drawPixelGray(int x, int y, uint8_t val2bit)` declaration
|
||||
|
||||
### `lib/GfxRenderer/BitmapHelpers.cpp`
|
||||
- Added `quantizeNoiseDither()`: hash-based noise dithering that always uses noise (unlike `quantize()` which is controlled by a compile-time flag), used for smooth gradient rendering on the 4-level display
|
||||
|
||||
### `lib/GfxRenderer/BitmapHelpers.h`
|
||||
- Added `quantizeNoiseDither()` declaration
|
||||
|
||||
### `src/activities/boot_sleep/SleepActivity.cpp`
|
||||
- Removed the `if (bitmap.getWidth() > pageWidth || bitmap.getHeight() > pageHeight)` gate in `renderBitmapSleepScreen()` — position/scale is now always computed regardless of whether the image is larger or smaller than the screen
|
||||
- Added anonymous namespace with:
|
||||
- `LetterboxGradientData` struct for edge sample storage
|
||||
- `sampleBitmapEdges()`: reads bitmap row-by-row to sample per-column (or per-row) average gray values from the first/last 20 pixels of the image edges
|
||||
- `drawLetterboxGradients()`: draws dithered gradients in letterbox areas using sampled edge colors, interpolating toward white
|
||||
- Integrated gradient rendering into the sleep screen flow: edge sampling (first pass), then gradient + bitmap rendering in BW pass, and again in each grayscale pass (LSB, MSB)
|
||||
|
||||
## Follow-up: Letterbox Fill Settings
|
||||
|
||||
Added three letterbox fill options and two new persisted settings:
|
||||
|
||||
### `src/CrossPointSettings.h`
|
||||
- Added `SLEEP_SCREEN_LETTERBOX_FILL` enum: `LETTERBOX_NONE` (plain white), `LETTERBOX_GRADIENT` (default), `LETTERBOX_SOLID`
|
||||
- Added `SLEEP_SCREEN_GRADIENT_DIR` enum: `GRADIENT_TO_WHITE` (default), `GRADIENT_TO_BLACK`
|
||||
- Added `sleepScreenLetterboxFill` and `sleepScreenGradientDir` member fields
|
||||
|
||||
### `src/CrossPointSettings.cpp`
|
||||
- Incremented `SETTINGS_COUNT` to 32
|
||||
- Added serialization (read/write) for the two new fields at the end for backward compatibility
|
||||
|
||||
### `src/SettingsList.h`
|
||||
- Added "Letterbox Fill" menu entry (None / Gradient / Solid) in Display category
|
||||
- Added "Gradient Direction" menu entry (To White / To Black) in Display category
|
||||
|
||||
### `src/activities/boot_sleep/SleepActivity.cpp`
|
||||
- Renamed `drawLetterboxGradients` → `drawLetterboxFill` with added `solidFill` and `targetColor` parameters
|
||||
- Solid mode: uses edge color directly (no distance-based interpolation), quantized with noise dithering
|
||||
- Gradient direction: interpolates from edge color toward `targetColor` (255 for white, 0 for black)
|
||||
- `renderBitmapSleepScreen` reads the settings and skips edge sampling entirely when fill mode is "None"
|
||||
|
||||
## Follow-up Items
|
||||
- Test with various cover image sizes and aspect ratios on actual hardware
|
||||
- Test custom images from `/sleep/` directory (1-bit and multi-bit)
|
||||
- Monitor RAM usage via serial during gradient rendering
|
||||
25
chat-summaries/2026-02-09_16-29-summary.md
Normal file
25
chat-summaries/2026-02-09_16-29-summary.md
Normal file
@@ -0,0 +1,25 @@
|
||||
# Edge Data Caching for Sleep Screen Letterbox Fill
|
||||
|
||||
## Task
|
||||
Cache the letterbox edge-sampling calculations so they are only computed once per cover image (on first sleep) and reused from a binary cache file on subsequent sleeps.
|
||||
|
||||
## Changes Made
|
||||
|
||||
### `src/activities/boot_sleep/SleepActivity.h`
|
||||
- Added `#include <string>` and updated `renderBitmapSleepScreen` signature to accept an optional `edgeCachePath` parameter (defaults to empty string for no caching).
|
||||
|
||||
### `src/activities/boot_sleep/SleepActivity.cpp`
|
||||
- Added `#include <Serialization.h>` for binary read/write helpers.
|
||||
- Added `loadEdgeCache()` function in the anonymous namespace: loads edge data from a binary cache file, validates the cache version and screen dimensions against current values to detect stale data.
|
||||
- Added `saveEdgeCache()` function: writes edge data (edgeA, edgeB arrays + metadata) to a compact binary file (~10 bytes header + 2 * edgeCount bytes data, typically under 1KB).
|
||||
- Updated `renderBitmapSleepScreen()`: tries loading from cache before sampling. On cache miss, samples edges from bitmap and saves to cache. On cache hit, skips the bitmap sampling pass entirely.
|
||||
- Updated `renderCoverSleepScreen()`: derives the edge cache path from the cover BMP path (e.g. `cover.bmp` -> `cover_edges.bin`) and passes it to `renderBitmapSleepScreen`. The cache is stored alongside the cover BMP in the book's `.crosspoint` directory.
|
||||
- Custom sleep images (`renderCustomSleepScreen`) do not use caching since the image may change randomly each sleep.
|
||||
|
||||
### Cache Design
|
||||
- **Format**: Binary file with version byte, screen dimensions, horizontal flag, edge count, letterbox sizes, and two edge color arrays.
|
||||
- **Invalidation**: Validated by cache version and screen dimensions. Naturally scoped per-book (lives in `/.crosspoint/epub_<hash>/`). Different cover modes (FIT vs CROP) produce different BMP files and thus different cache paths.
|
||||
- **Size**: ~970 bytes for a 480-wide image.
|
||||
|
||||
## Follow-up Items
|
||||
- None
|
||||
26
chat-summaries/2026-02-09_16-35-summary.md
Normal file
26
chat-summaries/2026-02-09_16-35-summary.md
Normal file
@@ -0,0 +1,26 @@
|
||||
# Letterbox Fill: 4-Mode Restructure (Solid, Blended, Gradient, None)
|
||||
|
||||
## Task
|
||||
Restructure the letterbox fill modes from 3 (None, Gradient, Solid) to 4 distinct modes with clearer semantics.
|
||||
|
||||
## New Modes
|
||||
1. **Solid** (new) - Picks the dominant (average) shade from the edge and fills the entire letterbox area with that single dithered color.
|
||||
2. **Blended** (renamed from old "Solid") - Uses per-pixel sampled edge colors with noise dithering, no distance-based interpolation.
|
||||
3. **Gradient** - Existing gradient behavior (interpolates per-pixel edge color toward a target color).
|
||||
4. **None** - No fill.
|
||||
|
||||
## Changes Made
|
||||
|
||||
### `src/CrossPointSettings.h`
|
||||
- Updated `SLEEP_SCREEN_LETTERBOX_FILL` enum: `LETTERBOX_NONE=0`, `LETTERBOX_SOLID=1`, `LETTERBOX_BLENDED=2`, `LETTERBOX_GRADIENT=3`.
|
||||
- Note: enum values changed from the old 3-value layout. Existing saved settings may need reconfiguring.
|
||||
|
||||
### `src/SettingsList.h`
|
||||
- Updated "Letterbox Fill" option labels to: "None", "Solid", "Blended", "Gradient".
|
||||
|
||||
### `src/activities/boot_sleep/SleepActivity.cpp`
|
||||
- `drawLetterboxFill()`: Changed signature from `bool solidFill` to `uint8_t fillMode`. Added dominant shade computation for SOLID mode (averages all edge samples into one value per side). BLENDED and SOLID both skip gradient interpolation; SOLID additionally skips per-pixel edge lookups.
|
||||
- `renderBitmapSleepScreen()`: Removed `solidFill` bool, passes `fillMode` directly to `drawLetterboxFill`. Updated log message to show the mode name.
|
||||
|
||||
## Follow-up Items
|
||||
- None
|
||||
46
chat-summaries/2026-02-12_17-08-summary.md
Normal file
46
chat-summaries/2026-02-12_17-08-summary.md
Normal file
@@ -0,0 +1,46 @@
|
||||
# Dictionary Feature Bug Fixes
|
||||
|
||||
**Date:** 2026-02-12
|
||||
**Branch:** mod/add-dictionary
|
||||
|
||||
## Task
|
||||
|
||||
Fix three bugs reported after initial implementation of PR #857 dictionary word lookup feature.
|
||||
|
||||
## Changes Made
|
||||
|
||||
### 1. Fix: Lookup fails after first successful lookup (Dictionary.cpp)
|
||||
|
||||
**Root cause:** `cleanWord()` lowercases the search term, but the dictionary index is sorted case-sensitively (uppercase entries sort before lowercase). The binary search lands in the wrong segment on the second pass because the index is already loaded and the sparse offset table was built for a case-sensitive sort order.
|
||||
|
||||
**Fix:** Extracted the binary-search + linear-scan logic into a new `searchIndex()` private helper. The `lookup()` method now performs a **two-pass search**: first with the lowercased word, then with the first-letter-capitalized variant. This handles dictionaries that store headwords as "Hello" instead of "hello". Also removed `stripHtml` from the header since HTML is now rendered, not stripped.
|
||||
|
||||
**Files:** `src/util/Dictionary.h`, `src/util/Dictionary.cpp`
|
||||
|
||||
### 2. Fix: Raw HTML displayed in definitions (DictionaryDefinitionActivity)
|
||||
|
||||
**Root cause:** Dictionary uses `sametypesequence=h` (HTML format). The original activity rendered definitions as plain text.
|
||||
|
||||
**Fix:** Complete rewrite of the definition activity to **render HTML with styled text**:
|
||||
- New `parseHtml()` method tokenizes HTML into `TextAtom` structs (word + style + newline directives)
|
||||
- Supports bold (`<b>`, `<strong>`, headings), italic (`<i>`, `<em>`), and mixed bold-italic via `EpdFontFamily::Style`
|
||||
- Handles `<ol>` (numbered with digit/alpha support), `<ul>` (bullet points), nested lists with indentation
|
||||
- Decodes HTML entities (named + numeric/hex → UTF-8)
|
||||
- Skips `<svg>` content, treats `</html>` as section separators
|
||||
- `wrapText()` flows styled atoms into positioned line segments (`Segment` struct with x-offset and style)
|
||||
- `renderScreen()` draws each segment with correct position and style via `renderer.drawText(fontId, x, y, text, true, style)`
|
||||
|
||||
**Files:** `src/activities/reader/DictionaryDefinitionActivity.h`, `src/activities/reader/DictionaryDefinitionActivity.cpp`
|
||||
|
||||
### 3. Fix: No button hints on word selection screen (DictionaryWordSelectActivity)
|
||||
|
||||
**Fix:** Added `GUI.drawButtonHints()` call at the end of `renderScreen()` with labels: "« Back", "✓ Lookup", "↕ Row", "↔ Word".
|
||||
|
||||
**Files:** `src/activities/reader/DictionaryWordSelectActivity.cpp`
|
||||
|
||||
## Follow-up Items
|
||||
|
||||
- If the reader font doesn't include bold/italic variants, styled text gracefully falls back to regular style
|
||||
- Nested list indentation uses 15px per level after the first
|
||||
- Alpha list numbering (`list-style-type: lower-alpha`) is supported; other custom list styles fall back to numeric
|
||||
- Button hint labels may need tuning once tested on device (especially in landscape orientation)
|
||||
53
chat-summaries/2026-02-12_17-34-summary.md
Normal file
53
chat-summaries/2026-02-12_17-34-summary.md
Normal file
@@ -0,0 +1,53 @@
|
||||
# Dictionary Feature Bug Fixes (Round 2)
|
||||
|
||||
**Date:** 2026-02-12
|
||||
**Branch:** mod/add-dictionary
|
||||
|
||||
## Task
|
||||
|
||||
Fix three remaining bugs with the dictionary word lookup feature after initial implementation and first round of fixes.
|
||||
|
||||
## Changes Made
|
||||
|
||||
### 1. Fix: Lookup only works for first word searched (Dictionary.cpp)
|
||||
|
||||
**Root cause:** The binary search used C++ `operator<=` (case-sensitive, pure `strcmp` order) for comparing index entries, but StarDict `.idx` files are sorted using `stardict_strcmp` — a two-level comparison that sorts case-*insensitively* first, then uses case-sensitive `strcmp` as a tiebreaker. This means entries like "Simple" and "simple" are adjacent, and uppercase/lowercase entries for the same letter are interleaved (e.g., "Silver", "silver", "Simple", "simple", "Simpson", "simpson").
|
||||
|
||||
With the wrong sort order, the binary search overshoots for many words: uppercase entries like "Simpson" are case-sensitively < "simple" (because 'S' < 's'), so `lo` moves past the segment actually containing "simple". The linear scan then starts too late and doesn't find the word. By coincidence, some words (like "professor") happen to land in correct segments while others (like "simple") don't.
|
||||
|
||||
**Fix:**
|
||||
- Added `stardictCmp()` (case-insensitive first, then case-sensitive tiebreaker) and `asciiCaseCmp()` helper functions
|
||||
- Binary search now uses `stardictCmp(key, word) <= 0` instead of `key <= word`
|
||||
- Linear scan early termination now uses `stardictCmp(key, word) > 0` instead of `key > word`
|
||||
- Exact match now uses `asciiCaseCmp(key, word) == 0` (case-insensitive) since `cleanWord` lowercases the search term but the dictionary may store entries in any case
|
||||
- Removed the two-pass search approach (no longer needed — single pass handles all casing)
|
||||
|
||||
**Files:** `src/util/Dictionary.cpp`
|
||||
|
||||
### 2. Fix: Unrendered glyphs in pronunciation guide (DictionaryDefinitionActivity.cpp)
|
||||
|
||||
**Root cause:** The dictionary stores IPA pronunciation as raw text between entries, e.g., `/əmˈsɛlvz/` appearing between `</html>` and the next `<p>` tag. These IPA Extension Unicode characters (U+0250–U+02FF) are not in the e-ink display's bitmap font, rendering as empty boxes.
|
||||
|
||||
**Fix:** Added IPA detection in `parseHtml()`: when encountering `/` or `[` delimiters, the parser looks ahead for a matching close delimiter within 80 characters. If the enclosed content contains any non-ASCII byte (> 0x7F), the entire section (including delimiters) is skipped. This removes IPA transcriptions like `/ˈsɪmpəl/` and `[hɜː(ɹ)b]` while preserving legitimate ASCII bracket content like "[citation needed]" or "and/or".
|
||||
|
||||
**Files:** `src/activities/reader/DictionaryDefinitionActivity.cpp`
|
||||
|
||||
### 3. Fix: Thinner button hints with overlap detection (DictionaryWordSelectActivity)
|
||||
|
||||
**Root cause:** Button hints used `GUI.drawButtonHints()` which draws 40px-tall buttons, taking excessive space over the book page content. No overlap detection meant hints could obscure the selected word at the bottom of the screen.
|
||||
|
||||
**Fix:**
|
||||
- Replaced `GUI.drawButtonHints()` with a custom `drawHints()` method
|
||||
- Draws thinner hints (22px instead of 40px) using `drawRect` + small text
|
||||
- Converts the selected word's bounding box from the current orientation to portrait coordinates (handles portrait, inverted, landscape CW/CCW)
|
||||
- Checks vertical and horizontal overlap between each of the 4 button hint areas and the selected word (including hyphenation continuations)
|
||||
- Individual hints that overlap the cursor are hidden (white area cleared, no button drawn)
|
||||
- Uses the theme's button positions `[58, 146, 254, 342]` to match the physical button layout
|
||||
|
||||
**Files:** `src/activities/reader/DictionaryWordSelectActivity.h`, `src/activities/reader/DictionaryWordSelectActivity.cpp`
|
||||
|
||||
## Follow-up Items
|
||||
|
||||
- The landscape coordinate conversions for overlap detection are best-guess transforms; if they're wrong for the specific device rotation mapping, they may need tuning after testing
|
||||
- The IPA skip heuristic is conservative (only skips content with non-ASCII in `/`/`[` delimiters); some edge-case IPA content outside these delimiters would still show
|
||||
- `SMALL_FONT_ID` is now included via `fontIds.h` in the word select activity
|
||||
47
chat-summaries/2026-02-12_17-52-summary.md
Normal file
47
chat-summaries/2026-02-12_17-52-summary.md
Normal file
@@ -0,0 +1,47 @@
|
||||
# Dictionary Feature Bug Fixes (Round 3)
|
||||
|
||||
**Date:** 2026-02-12
|
||||
**Branch:** mod/add-dictionary
|
||||
|
||||
## Task
|
||||
|
||||
Fix three issues reported after round 2 of dictionary fixes.
|
||||
|
||||
## Changes Made
|
||||
|
||||
### 1. Fix: Definitions truncated for some words (Dictionary.cpp)
|
||||
|
||||
**Root cause:** The `asciiCaseCmp` case-insensitive match introduced in round 2 returns the *first* case variant found in the index. In StarDict order, "Professor" (capitalized) sorts before "professor" (lowercase). If the dictionary has separate entries for each — e.g., "Professor" as a title (short definition) and "professor" as the common noun (full multi-page definition) — the shorter entry is returned.
|
||||
|
||||
**Fix:** The linear scan in `searchIndex` now remembers the first case-insensitive match as a fallback, but continues scanning adjacent entries (case variants are always adjacent in StarDict order). If an exact case-sensitive match is found, it's used immediately. Otherwise, the first case-insensitive match is used. This ensures `cleanWord("professor")` → `"professor"` finds the full lowercase entry, not the shorter capitalized one.
|
||||
|
||||
**Files:** `src/util/Dictionary.cpp`
|
||||
|
||||
### 2. Fix: Non-renderable foreign script characters in definitions (DictionaryDefinitionActivity)
|
||||
|
||||
**Root cause:** Dictionary definitions include text from other languages (Chinese, Greek, Arabic, Cyrillic, etc.) as etymological references or examples. These characters aren't in the e-ink bitmap font and render as empty boxes. This is the same class of issue as the IPA pronunciation fix from round 2, but affecting inline content within definitions.
|
||||
|
||||
**Fix:**
|
||||
- Added `isRenderableCodepoint(uint32_t cp)` static helper that whitelists character ranges the e-ink font supports:
|
||||
- U+0000–U+024F: Basic Latin through Latin Extended-B (ASCII + accented chars)
|
||||
- U+0300–U+036F: Combining Diacritical Marks
|
||||
- U+2000–U+206F: General Punctuation (dashes, quotes, bullets, ellipsis)
|
||||
- U+20A0–U+20CF: Currency Symbols
|
||||
- U+2100–U+214F: Letterlike Symbols
|
||||
- U+2190–U+21FF: Arrows
|
||||
- Replaced the byte-by-byte character append in `parseHtml()` with a UTF-8-aware decoder that reads multi-byte sequences, decodes the codepoint, and only appends renderable characters. Invalid or non-renderable characters are silently skipped.
|
||||
|
||||
**Files:** `src/activities/reader/DictionaryDefinitionActivity.h`, `src/activities/reader/DictionaryDefinitionActivity.cpp`
|
||||
|
||||
### 3. Fix: Revert to standard-height hints, keep overlap hiding (DictionaryWordSelectActivity)
|
||||
|
||||
**What changed:** Reverted from 22px thin custom hints back to the standard 40px theme-style buttons (rounded corners with `cornerRadius=6`, `SMALL_FONT_ID` text, matching `LyraTheme::drawButtonHints` exactly). The overlap detection is preserved.
|
||||
|
||||
**Key design choice:** Instead of calling `GUI.drawButtonHints()` (which always clears all 4 button areas, erasing page content even for hidden buttons), the method draws each button individually in portrait mode. Hidden buttons are skipped entirely (`continue`), so the page content and word highlight underneath remain visible. Non-hidden buttons get the full theme treatment: white fill + rounded rect border + centered text.
|
||||
|
||||
**Files:** `src/activities/reader/DictionaryWordSelectActivity.cpp`
|
||||
|
||||
## Follow-up Items
|
||||
|
||||
- The `isRenderableCodepoint` whitelist is conservative — if the font gains additional glyph coverage (e.g., Greek letters for math), the whitelist can be extended
|
||||
- Entity-decoded characters bypass the codepoint filter since they're appended as raw bytes; this is fine for the current entity set (all produce ASCII or General Punctuation characters)
|
||||
40
chat-summaries/2026-02-12_20-00-summary.md
Normal file
40
chat-summaries/2026-02-12_20-00-summary.md
Normal file
@@ -0,0 +1,40 @@
|
||||
# Bookmark Feature Implementation
|
||||
|
||||
## Task
|
||||
Implement bookmark functionality for the e-reader, replacing existing "Coming soon" stubs with full add/remove bookmark, visual page indicator, and bookmark navigation features.
|
||||
|
||||
## Changes Made
|
||||
|
||||
### New Files Created
|
||||
- **`src/util/BookmarkStore.h`** / **`src/util/BookmarkStore.cpp`** - Bookmark persistence utility. Stores bookmarks as binary data (`bookmarks.bin`) per-book in the epub cache directory on SD card. Each bookmark is a (spineIndex, pageNumber) pair (4 bytes). Provides static methods: `load`, `save`, `addBookmark`, `removeBookmark`, `hasBookmark`.
|
||||
|
||||
- **`src/activities/reader/EpubReaderBookmarkSelectionActivity.h`** / **`.cpp`** - New activity for the "Go to Bookmark" list UI, modeled on the existing chapter selection activity. Shows bookmark entries as "Chapter Title - Page N" with ButtonNavigator for scrolling. Selecting a bookmark navigates to that spine/page.
|
||||
|
||||
### Edited Files
|
||||
- **`src/activities/reader/EpubReaderMenuActivity.h`** - Added `REMOVE_BOOKMARK` to `MenuAction` enum. Changed `buildMenuItems()` to accept `isBookmarked` parameter; dynamically shows "Remove Bookmark" or "Add Bookmark" as the first menu item.
|
||||
|
||||
- **`src/activities/reader/EpubReaderActivity.cpp`** - Main integration point:
|
||||
- Added includes for `BookmarkStore.h` and `EpubReaderBookmarkSelectionActivity.h`
|
||||
- Menu creation now computes `isBookmarked` state and passes it through
|
||||
- `ADD_BOOKMARK` handler: calls `BookmarkStore::addBookmark()`, shows "Bookmark added" popup
|
||||
- `REMOVE_BOOKMARK` handler (new): calls `BookmarkStore::removeBookmark()`, shows "Bookmark removed" popup
|
||||
- `GO_TO_BOOKMARK` handler: loads bookmarks, opens `EpubReaderBookmarkSelectionActivity` if any exist, falls back to Table of Contents if no bookmarks but TOC exists, otherwise returns to reader
|
||||
- `renderContents()`: draws a small bookmark ribbon (fillPolygon, 5-point shape) in the top-right corner when the current page is bookmarked
|
||||
|
||||
## Follow-up Changes (same session)
|
||||
|
||||
### Force half refresh on menu exit
|
||||
- `onReaderMenuBack()`: sets `pagesUntilFullRefresh = 1` so the next render uses `HALF_REFRESH` to clear menu/popup ghosting artifacts from the e-ink display.
|
||||
- `ADD_BOOKMARK` / `REMOVE_BOOKMARK` handlers: also set `pagesUntilFullRefresh = 1` after their popups.
|
||||
|
||||
### Bookmark snippet (first sentence)
|
||||
- `Bookmark` struct now includes a `snippet` string field storing the first sentence from the bookmarked page.
|
||||
- `BookmarkStore` binary format upgraded to v2: version marker byte (0xFF) + count + entries with variable-length snippet. Backward-compatible: reads v1 files (no snippets) gracefully.
|
||||
- `addBookmark()` now accepts an optional `snippet` parameter (max 120 chars).
|
||||
- `EpubReaderActivity::onReaderMenuConfirm(ADD_BOOKMARK)`: extracts the first sentence from the page by iterating PageLine elements and their TextBlock words, stopping at sentence-ending punctuation (.!?:).
|
||||
- `EpubReaderBookmarkSelectionActivity::getBookmarkLabel()`: displays bookmark as "Chapter Title - First sentence here - Page N".
|
||||
|
||||
## Follow-up Items
|
||||
- Test on device to verify bookmark ribbon sizing/positioning looks good across orientations
|
||||
- Consider caching bookmark state in memory to avoid SD reads on every page render (currently `hasBookmark` reads from SD each time in `renderContents`)
|
||||
- The bookmark selection list could potentially support deleting bookmarks directly from the list in a future iteration
|
||||
32
chat-summaries/2026-02-12_21-30-summary.md
Normal file
32
chat-summaries/2026-02-12_21-30-summary.md
Normal file
@@ -0,0 +1,32 @@
|
||||
# Implement Dictionary Word Lookup Feature (PR #857)
|
||||
|
||||
## Task
|
||||
|
||||
Ported upstream PR #857 (dictionary word lookup feature) into the local codebase on `mod/add-dictionary` branch. Two adaptations were made:
|
||||
|
||||
1. **Vector compatibility**: PR #857 was written against upstream `master` which used `std::list` for word storage. Our codebase already has PR #802 applied (list-to-vector). The `TextBlock.h` getters added by PR #857 were changed to return `const std::vector<...>&` instead of `const std::list<...>&`.
|
||||
|
||||
2. **Dictionary path tweak**: Changed dictionary file lookup from SD card root (`/dictionary.idx`, `/dictionary.dict`) to a `/.dictionary/` subfolder (`/.dictionary/dictionary.idx`, `/.dictionary/dictionary.dict`).
|
||||
|
||||
## Changes Made
|
||||
|
||||
### New files (10)
|
||||
|
||||
- `src/util/Dictionary.h` / `.cpp` -- StarDict 3 format dictionary lookup (sparse index + binary search)
|
||||
- `src/util/LookupHistory.h` / `.cpp` -- Per-book lookup history stored in book cache dir
|
||||
- `src/activities/reader/DictionaryDefinitionActivity.h` / `.cpp` -- Definition display with pagination
|
||||
- `src/activities/reader/DictionaryWordSelectActivity.h` / `.cpp` -- Word selection from current page with orientation-aware navigation
|
||||
- `src/activities/reader/LookedUpWordsActivity.h` / `.cpp` -- Lookup history browser with long-press delete
|
||||
|
||||
### Modified files (5)
|
||||
|
||||
- `lib/Epub/Epub/blocks/TextBlock.h` -- Added `getWords()`, `getWordXpos()`, `getWordStyles()` accessors (returning `std::vector`)
|
||||
- `lib/Epub/Epub/Page.h` -- Added `getBlock()` accessor to `PageLine`
|
||||
- `src/activities/reader/EpubReaderMenuActivity.h` -- Added `LOOKUP`/`LOOKED_UP_WORDS` enum values, `hasDictionary` constructor param, dynamic `buildMenuItems()`
|
||||
- `src/activities/reader/EpubReaderActivity.h` -- Added includes for new activity headers
|
||||
- `src/activities/reader/EpubReaderActivity.cpp` -- Added includes, `Dictionary::exists()` check, `LOOKUP` and `LOOKED_UP_WORDS` case handling
|
||||
|
||||
## Follow-up Items
|
||||
|
||||
- Dictionary files (`dictionary.idx`, `dictionary.dict`, `dictionary.ifo`) must be placed in `/.dictionary/` folder on the SD card root
|
||||
- Menu items "Lookup" and "Lookup History" only appear when dictionary files are detected
|
||||
56
chat-summaries/2026-02-12_22-00-summary.md
Normal file
56
chat-summaries/2026-02-12_22-00-summary.md
Normal file
@@ -0,0 +1,56 @@
|
||||
# Dictionary Feature Polish & Menu Reorganization
|
||||
|
||||
**Date:** 2026-02-12
|
||||
|
||||
## Task Description
|
||||
|
||||
Continued polishing the dictionary word lookup feature across multiple iterations: fixing side button hint placement and orientation, adding CCW text rotation, fixing pronunciation line rendering, adding index caching, and reorganizing the reader menu.
|
||||
|
||||
## Changes Made
|
||||
|
||||
### Side button hint fixes (`DictionaryWordSelectActivity.cpp`)
|
||||
- Moved `drawSideButtonHints` call inside `drawHints()` where renderer is in portrait mode (fixes wrong placement)
|
||||
- Made all button hint labels orientation-aware (portrait, inverted, landscape CW/CCW each get correct labels matching their button-to-action mapping)
|
||||
- Replaced `GUI.drawSideButtonHints()` with custom drawing: solid background, overlap/cursor hiding, text truncation, and orientation-aware rotation
|
||||
- Changed word navigation labels from "Prev Word"/"Next Word" to "« Word"/"Word »"
|
||||
|
||||
### GfxRenderer CCW text rotation (`lib/GfxRenderer/`)
|
||||
- Added `drawTextRotated90CCW()` to `GfxRenderer.h` and `GfxRenderer.cpp`
|
||||
- Mirrors the existing CW rotation: text reads top-to-bottom instead of bottom-to-top
|
||||
- Used for side button hints in landscape CCW orientation
|
||||
|
||||
### Definition screen fixes (`DictionaryDefinitionActivity.cpp/.h`)
|
||||
- Fixed pronunciation commas: definitions start with `/ˈsɪm.pəl/, /ˈsɪmpəl/` before `<p>` — now skips all content before first `<` tag in `parseHtml()`
|
||||
- Added side button hints with proper CCW rotation and solid backgrounds
|
||||
- Updated bottom button labels: "« Back", "" (hidden stub), "« Page", "Page »"
|
||||
- Added half refresh on initial screen entry (`firstRender` flag)
|
||||
|
||||
### Dictionary index caching (`Dictionary.h/.cpp`)
|
||||
- New `loadCachedIndex()`: reads `/.dictionary/dictionary.cache` — validates magic + idx file size, loads sparse offsets directly (~7KB binary read vs 17MB scan)
|
||||
- New `saveCachedIndex()`: persists after first full scan
|
||||
- Cache format: `[magic 4B][idxFileSize 4B][totalWords 4B][count 4B][offsets N×4B]`
|
||||
- Auto-invalidates when `.idx` file size changes
|
||||
- New public methods: `cacheExists()`, `deleteCache()`
|
||||
|
||||
### Reader menu reorganization (`EpubReaderMenuActivity.h`, `EpubReaderActivity.cpp`)
|
||||
- New `MenuAction` enum values: `ADD_BOOKMARK`, `GO_TO_BOOKMARK`, `DELETE_DICT_CACHE`
|
||||
- Reordered menu: Add Bookmark, Lookup Word, Lookup Word History, Reading Orientation, Table of Contents, Go to Bookmark, Go to %, Close Book, Sync Progress, Delete Book Cache, Delete Dictionary Cache
|
||||
- Renamed: "Go to Chapter" → "Table of Contents", "Go Home" → "Close Book", "Lookup" → "Lookup Word"
|
||||
- Bookmark stubs show "Coming soon" popup
|
||||
- Delete Dictionary Cache checks existence and clears in-memory state
|
||||
|
||||
## Files Modified
|
||||
- `lib/GfxRenderer/GfxRenderer.h` — added `drawTextRotated90CCW` declaration
|
||||
- `lib/GfxRenderer/GfxRenderer.cpp` — added `drawTextRotated90CCW` implementation
|
||||
- `src/util/Dictionary.h` — added `cacheExists()`, `deleteCache()`, `loadCachedIndex()`, `saveCachedIndex()`
|
||||
- `src/util/Dictionary.cpp` — cache load/save implementation, delete cache
|
||||
- `src/activities/reader/DictionaryWordSelectActivity.cpp` — orientation-aware hints, custom side button drawing
|
||||
- `src/activities/reader/DictionaryDefinitionActivity.h` — added `firstRender` flag
|
||||
- `src/activities/reader/DictionaryDefinitionActivity.cpp` — pronunciation fix, side hints, half refresh, label changes
|
||||
- `src/activities/reader/EpubReaderMenuActivity.h` — new enum values, reordered menu
|
||||
- `src/activities/reader/EpubReaderActivity.cpp` — handlers for new menu actions
|
||||
|
||||
## Follow-up Items
|
||||
- Bookmark feature implementation (stubs are in place)
|
||||
- Test CCW text rotation rendering on device
|
||||
- Verify cache invalidation works when dictionary files are replaced
|
||||
28
chat-summaries/2026-02-12_23-00-summary.md
Normal file
28
chat-summaries/2026-02-12_23-00-summary.md
Normal file
@@ -0,0 +1,28 @@
|
||||
# Implement Letterbox Edge Row Copy for MATCHED Mode
|
||||
|
||||
## Task
|
||||
Implement the "FrameBuffer Edge Row Copy" plan for the MATCHED letterbox fill mode. Instead of computing letterbox colors from sampled edge data, the new approach copies the cover's rendered edge row directly from the frameBuffer into the letterbox area after each `drawBitmap` call.
|
||||
|
||||
## Changes Made
|
||||
|
||||
### `src/activities/boot_sleep/SleepActivity.cpp`
|
||||
- **Added `#include <cstring>`** for `memcpy` usage.
|
||||
- **Added `copyEdgeRowsToLetterbox()` helper** (anonymous namespace): Copies physical columns (horizontal letterbox) or physical rows (vertical letterbox) in the frameBuffer. For horizontal letterbox, iterates per-bit across 480 physical rows. For vertical letterbox, uses `memcpy` of 100-byte physical rows.
|
||||
- **Updated `renderBitmapSleepScreen()`**:
|
||||
- Added `scaledWidth`/`scaledHeight` computation matching `drawBitmap`'s floor logic.
|
||||
- Added `isMatched` flag.
|
||||
- MATCHED mode now skips edge sampling entirely (`sampleBitmapEdges` / cache load).
|
||||
- After each `drawBitmap` call (BW, LSB, MSB passes), calls `copyEdgeRowsToLetterbox` for MATCHED mode.
|
||||
- **Cleaned up dead code**:
|
||||
- Removed the entire MATCHED case from `drawLetterboxFill()` (no longer called for MATCHED).
|
||||
- Removed `grayToVal2bit` helper (was only used by the removed MATCHED case).
|
||||
- Removed `skipFillInGreyscale` flag (no longer needed — the edge copy participates in all passes naturally).
|
||||
|
||||
## Build
|
||||
Successfully compiled with `pio run` (0 errors, 0 warnings relevant to changes).
|
||||
|
||||
## Follow-up
|
||||
- Needs on-device testing to verify:
|
||||
1. The letterbox blends seamlessly with the cover edge (pixel-perfect 1:1 match).
|
||||
2. No scan coupling corruption (the scattered pixel distribution from dithering should cause less coupling than uniform blocks).
|
||||
3. If corruption is still unacceptable, the fallback is the previous flat-fill + greyscale-skip approach (revert this change).
|
||||
41
chat-summaries/2026-02-12_merge-master-into-mod-master.md
Normal file
41
chat-summaries/2026-02-12_merge-master-into-mod-master.md
Normal file
@@ -0,0 +1,41 @@
|
||||
# Merge master into mod/master
|
||||
|
||||
**Date:** 2026-02-12
|
||||
|
||||
## Task
|
||||
|
||||
Compare upstream `master` (14 new commits) with `mod/master` (2 mod commits) since their common ancestor (`6202bfd` — release/1.0.0 merge), assess merge risk, and perform the merge.
|
||||
|
||||
## Branch Summary
|
||||
|
||||
### Upstream (`master`) — 14 commits, 47 files, ~6000 lines
|
||||
- Unified navigation handling with ButtonNavigator utility
|
||||
- Italian hyphenation support
|
||||
- Natural sort in file browser
|
||||
- Auto WiFi reconnect to last network
|
||||
- Extended Python debugging monitor
|
||||
- More power saving on idle
|
||||
- OPDS fixes (absolute URLs, prevent sleep during download)
|
||||
- Uniform debug message formatting (millis() timestamps)
|
||||
- File browser Back/Home label fix, GPIO trigger fix
|
||||
- USER_GUIDE.md updates
|
||||
|
||||
### Mod (`mod/master`) — 2 commits, 10 files, ~588 lines
|
||||
- `.gitignore` tweaks for mod fork
|
||||
- Sleep screen letterbox fill and image upscaling feature
|
||||
|
||||
## Conflict Resolution
|
||||
|
||||
Single conflict in `src/activities/boot_sleep/SleepActivity.cpp`:
|
||||
- **Upstream** changed `Serial.println` → `Serial.printf("[%lu] [SLP] ...\n", millis())` for uniform debug logging
|
||||
- **Mod** had already adopted this format in new code, but the original lines it modified were the old format
|
||||
- **Resolution:** Kept mod's `renderBitmapSleepScreen(bitmap, edgeCachePath)` call with upstream's `millis()` log format
|
||||
|
||||
## Result
|
||||
|
||||
Merge commit: `182c236`
|
||||
|
||||
## Follow-up
|
||||
|
||||
- Test sleep screen behavior end-to-end (letterbox fill + upstream idle power saving changes)
|
||||
- Verify new upstream features (navigation, WiFi auto-connect) work alongside mod changes
|
||||
23
chat-summaries/2026-02-12_mod-version-env.md
Normal file
23
chat-summaries/2026-02-12_mod-version-env.md
Normal file
@@ -0,0 +1,23 @@
|
||||
# Add `env:mod` with version + git hash
|
||||
|
||||
## Task
|
||||
Add a PlatformIO environment that flashes firmware with a `-mod+<git_hash>` version suffix (e.g. `1.0.0-mod+a3f7c21`).
|
||||
|
||||
## Changes
|
||||
|
||||
### New file: `scripts/inject_mod_version.py`
|
||||
- PlatformIO pre-build script
|
||||
- Reads `version` from the `[crosspoint]` section of `platformio.ini`
|
||||
- Runs `git rev-parse --short HEAD` to get the current commit hash
|
||||
- Injects `-DCROSSPOINT_VERSION="{version}-mod+{hash}"` into build flags
|
||||
|
||||
### Modified: `platformio.ini`
|
||||
- Added `[env:mod]` section (lines 58-64) that extends `base`, includes the new script via `extra_scripts`, and inherits base build flags
|
||||
|
||||
## Usage
|
||||
```
|
||||
pio run -e mod -t upload
|
||||
```
|
||||
|
||||
## Follow-up
|
||||
- None
|
||||
39
chat-summaries/2026-02-12_prerender-book-covers-summary.md
Normal file
39
chat-summaries/2026-02-12_prerender-book-covers-summary.md
Normal file
@@ -0,0 +1,39 @@
|
||||
# Prerender Book Covers/Thumbnails on First Open
|
||||
|
||||
**Date:** 2026-02-12
|
||||
**Branch:** `mod/prerender-book-covers`
|
||||
|
||||
## Task
|
||||
|
||||
Implement todo item: "Process/render all covers/thumbs when opening book for first time" with a progress indicator so the reader doesn't appear frozen.
|
||||
|
||||
## Changes Made
|
||||
|
||||
### `src/components/UITheme.h`
|
||||
- Added `PRERENDER_THUMB_HEIGHTS[]` and `PRERENDER_THUMB_HEIGHTS_COUNT` constants (226, 400) representing all known theme `homeCoverHeight` values (Lyra and Base). This ensures thumbnails are pre-generated for all themes.
|
||||
|
||||
### `src/activities/reader/EpubReaderActivity.cpp`
|
||||
- Added prerender block in `onEnter()` after `setupCacheDir()` and before `addBook()`.
|
||||
- Checks whether `cover.bmp`, `cover_crop.bmp`, `thumb_226.bmp`, and `thumb_400.bmp` exist.
|
||||
- If any are missing, shows "Preparing book..." popup with a progress bar that updates after each generation step.
|
||||
- On subsequent opens, all files already exist and the popup is skipped entirely.
|
||||
|
||||
### `src/activities/reader/XtcReaderActivity.cpp`
|
||||
- Added same prerender pattern in `onEnter()`.
|
||||
- Generates `cover.bmp`, `thumb_226.bmp`, and `thumb_400.bmp` (XTC has no cropped cover variant).
|
||||
|
||||
### `src/activities/reader/TxtReaderActivity.cpp`
|
||||
- Added prerender for `cover.bmp` only (TXT has no thumbnail support).
|
||||
- Shows "Preparing book..." popup if the cover needs generating.
|
||||
|
||||
## Design Decisions
|
||||
|
||||
- **Letterbox edge data not prerendered:** The sleep screen's letterbox gradient fill (`cover_edges.bin`) depends on runtime settings (crop mode, screen dimensions) and is already efficiently cached after first sleep. The expensive part (JPEG-to-BMP conversion) is what this change addresses.
|
||||
- **TXT `addBook()` cover path unchanged:** The `coverBmpPath` field in `RecentBook` is used for home screen thumbnails, not sleep covers. Since TXT has no thumbnail support, passing `""` remains correct.
|
||||
- **HomeActivity::loadRecentCovers() kept as fallback:** Books opened before this change will still have thumbnails generated lazily on the Home screen. No code removal needed.
|
||||
|
||||
## Follow-up Items
|
||||
|
||||
- Mark todo item as complete in `mod/docs/todo.md`
|
||||
- Test on device with each book format (EPUB, XTC/XTCH, TXT)
|
||||
- If a new theme is added with a different `homeCoverHeight`, update `PRERENDER_THUMB_HEIGHTS`
|
||||
33
chat-summaries/2026-02-13_cleanup-summary.md
Normal file
33
chat-summaries/2026-02-13_cleanup-summary.md
Normal file
@@ -0,0 +1,33 @@
|
||||
# Letterbox Fill: Hash-Based Block Dithering Fix & Cleanup
|
||||
|
||||
**Date:** 2026-02-13
|
||||
|
||||
## Task Description
|
||||
|
||||
Resolved an e-ink display crosstalk issue where a specific book cover ("The World in a Grain") became completely washed out and invisible when using "Dithered" letterbox fill mode. The root cause was pixel-level Bayer dithering creating a high-frequency checkerboard pattern in the BW pass for gray values in the 171-254 range (level-2/level-3 boundary), which caused display crosstalk during HALF_REFRESH.
|
||||
|
||||
## Solution
|
||||
|
||||
Hash-based block dithering with 2x2 pixel blocks for gray values in the problematic BW-boundary range (171-254). Each 2x2 block gets a uniform level (2 or 3) determined by a spatial hash, with the proportion approximating the target gray. Standard Bayer dithering is used for all other gray ranges.
|
||||
|
||||
## Changes Made
|
||||
|
||||
### Modified Files
|
||||
- **`src/activities/boot_sleep/SleepActivity.cpp`** — Added `bayerCrossesBwBoundary()` and `hashBlockDither()` functions; `drawLetterboxFill()` uses hash-based block dithering for BW-boundary gray values, standard Bayer for everything else. Removed all debug instrumentation (H1-H20 logs, frame buffer checksums, edge histograms, rewind verification).
|
||||
- **`src/CrossPointSettings.h`** — Reordered letterbox fill enum to `DITHERED=0, SOLID=1, NONE=2` with `DITHERED` as default.
|
||||
- **`src/SettingsList.h`** — Updated settings UI labels to match new enum order.
|
||||
- **`lib/GfxRenderer/GfxRenderer.h`** — Removed `getRenderMode()` getter (was only needed by debug instrumentation).
|
||||
|
||||
### Deleted Files
|
||||
- 16 debug log files from `.cursor/` directory
|
||||
|
||||
## Key Findings
|
||||
|
||||
- Single-pixel alternation (block=1) causes display crosstalk regardless of pattern regularity (Bayer or hash).
|
||||
- 2x2 minimum pixel runs are sufficient to avoid crosstalk on this e-ink display.
|
||||
- The irregular hash pattern is less visually noticeable than a regular Bayer grid at the same block size.
|
||||
- The issue only affects gray values 171-254 where Bayer produces a level-2/level-3 mix (the BW pass boundary).
|
||||
|
||||
## Follow-up Items
|
||||
|
||||
- None. Feature is stable and working for all tested book covers.
|
||||
49
chat-summaries/2026-02-13_letterbox-extend-edges.md
Normal file
49
chat-summaries/2026-02-13_letterbox-extend-edges.md
Normal file
@@ -0,0 +1,49 @@
|
||||
# Letterbox Fill: Replace Dithering with Edge Replication ("Extend Edges")
|
||||
|
||||
**Date:** 2026-02-13
|
||||
|
||||
## Task Description
|
||||
|
||||
After implementing Bayer ordered dithering for the "Blended" letterbox fill mode (replacing earlier noise dithering), the user reported that the problematic cover (*The World in a Grain* by Vince Beiser) looked even worse. Investigation revealed that **any dithering technique** is fundamentally incompatible with the e-ink multi-pass rendering pipeline for large uniform fill areas:
|
||||
|
||||
- The BW HALF_REFRESH pass creates a visible dark/white pattern in the letterbox
|
||||
- The subsequent greyscale correction can't cleanly handle mixed-level patterns in large uniform areas
|
||||
- This causes crosstalk artifacts that can affect adjacent cover rendering
|
||||
- Uniform fills (all same level, like Solid mode) work fine because all pixels go through identical correction
|
||||
|
||||
The user's key insight: the display already renders the cover's grayscale correctly (including Atkinson-dithered mixed levels). Instead of re-dithering an averaged color, replicate the cover's actual boundary pixels into the letterbox -- letting the display render them the same way it renders the cover itself.
|
||||
|
||||
## Changes Made
|
||||
|
||||
### `src/CrossPointSettings.h`
|
||||
- Renamed `LETTERBOX_BLENDED` (value 2) → `LETTERBOX_EXTENDED`
|
||||
- Updated comment to reflect "None / Solid / Extend Edges"
|
||||
|
||||
### `src/SettingsList.h`
|
||||
- Changed UI label from `"Blended"` → `"Extend Edges"`
|
||||
|
||||
### `src/activities/boot_sleep/SleepActivity.cpp`
|
||||
- **Removed**: `BAYER_4X4` matrix, `quantizeBayerDither()` function (all Bayer dithering code)
|
||||
- **Added**: `getPackedPixel()`, `setPackedPixel()` helpers for packed 2-bit array access
|
||||
- **Rewrote** `LetterboxFillData` struct:
|
||||
- Added `edgeA`/`edgeB` (dynamically allocated packed 2-bit arrays) for per-pixel edge data
|
||||
- Added `edgePixelCount`, `scale` for coordinate mapping
|
||||
- Added `freeEdgeData()` cleanup method
|
||||
- **Renamed** `computeEdgeAverages()` → `computeEdgeData()`:
|
||||
- New `captureEdgePixels` parameter controls whether to allocate and fill edge arrays
|
||||
- For horizontal letterboxing: captures first/last visible BMP rows
|
||||
- For vertical letterboxing: captures leftmost/rightmost pixel per visible row
|
||||
- Still computes averages for SOLID mode in the same pass
|
||||
- **Rewrote** `drawLetterboxFill()`:
|
||||
- SOLID mode: unchanged (uniform fill with snapped level)
|
||||
- EXTENDED mode: maps screen coordinates → BMP coordinates via scale factor, looks up stored 2-bit pixel values from the cover's boundary row/column
|
||||
- **Updated** `renderBitmapSleepScreen()`: new log messages, `freeEdgeData()` call at end
|
||||
|
||||
## Backward Compatibility
|
||||
- Enum value 2 is unchanged (was `LETTERBOX_BLENDED`, now `LETTERBOX_EXTENDED`)
|
||||
- Serialized settings files continue to work without migration
|
||||
|
||||
## Follow-up Items
|
||||
- Test "Extend Edges" mode with both covers (Power Broker and World in a Grain)
|
||||
- Test vertical letterboxing (left/right) if applicable covers are available
|
||||
- Verify SOLID mode still works as expected
|
||||
42
chat-summaries/2026-02-13_letterbox-fill-redesign.md
Normal file
42
chat-summaries/2026-02-13_letterbox-fill-redesign.md
Normal file
@@ -0,0 +1,42 @@
|
||||
# Letterbox Fill Redesign
|
||||
|
||||
**Date:** 2026-02-13
|
||||
**Task:** Strip out the 5-mode letterbox edge fill system and replace with a simplified 3-mode design
|
||||
|
||||
## Changes Made
|
||||
|
||||
### Problem
|
||||
The existing letterbox fill feature had 5 modes (None, Solid, Blended, Gradient, Matched) with ~300 lines of complex code including per-pixel edge arrays, malloc'd buffers, binary edge caching, framebuffer-level column/row copying, and a gradient direction sub-setting. Several modes introduced visual corruption that couldn't be resolved.
|
||||
|
||||
### New Design
|
||||
Simplified to 3 modes:
|
||||
- **None** -- no fill
|
||||
- **Solid** -- computes dominant edge color, snaps to nearest of 4 e-ink levels (black/dark gray/light gray/white), fills uniformly
|
||||
- **Blended** -- computes dominant edge color, fills with exact gray value using noise dithering for smooth approximation
|
||||
|
||||
### Files Changed
|
||||
|
||||
1. **`src/CrossPointSettings.h`** -- Removed `LETTERBOX_GRADIENT`, `LETTERBOX_MATCHED` enum values; removed `SLEEP_SCREEN_GRADIENT_DIR` enum and `sleepScreenGradientDir` member; changed default to `LETTERBOX_NONE`
|
||||
|
||||
2. **`src/SettingsList.h`** -- Trimmed Letterbox Fill options to `{None, Solid, Blended}`; removed Gradient Direction setting entry
|
||||
|
||||
3. **`src/CrossPointSettings.cpp`** -- Removed `sleepScreenGradientDir` from write path; added dummy read for backward compatibility with old settings files; decremented `SETTINGS_COUNT` from 32 to 31
|
||||
|
||||
4. **`src/activities/boot_sleep/SleepActivity.cpp`** -- Major rewrite:
|
||||
- Removed: `LetterboxGradientData` struct, `loadEdgeCache()`/`saveEdgeCache()`, `sampleBitmapEdges()`, `copyEdgeRowsToLetterbox()`, old `drawLetterboxFill()`
|
||||
- Added: `LetterboxFillData` struct (2 bytes vs arrays), `snapToEinkLevel()`, `computeEdgeAverages()` (running sums only, no malloc), simplified `drawLetterboxFill()`
|
||||
- Cleaned `renderBitmapSleepScreen()`: removed matched/gradient logic, edge cache paths, unused `scaledWidth`/`scaledHeight`
|
||||
- Cleaned `renderCoverSleepScreen()`: removed edge cache path derivation
|
||||
- Removed unused includes (`<Serialization.h>`, `<cstring>`), added `<cmath>`
|
||||
|
||||
5. **`src/activities/boot_sleep/SleepActivity.h`** -- Removed `edgeCachePath` parameter from `renderBitmapSleepScreen()` signature; removed unused `<string>` include
|
||||
|
||||
### Backward Compatibility
|
||||
- Enum values 0/1/2 (None/Solid/Blended) unchanged -- existing settings preserved
|
||||
- Old Gradient (3) or Matched (4) values rejected by `readAndValidate`, falling back to default (None)
|
||||
- Old `sleepScreenGradientDir` byte consumed via dummy read during settings load
|
||||
- Orphaned `_edges.bin` cache files on SD cards are harmless
|
||||
|
||||
## Follow-up Items
|
||||
- Test all 3 fill modes on device with various cover aspect ratios
|
||||
- Consider cleaning up orphaned `_edges.bin` files (optional, low priority)
|
||||
41
chat-summaries/2026-02-13_merge-upstream-summary.md
Normal file
41
chat-summaries/2026-02-13_merge-upstream-summary.md
Normal file
@@ -0,0 +1,41 @@
|
||||
# Merge upstream/master into mod/master
|
||||
|
||||
**Date:** 2026-02-13
|
||||
**Branch:** `mod/merge-upstream` (from `mod/master`)
|
||||
**Commit:** `82bfbd8`
|
||||
|
||||
## Task
|
||||
|
||||
Merged 3 new upstream/master commits into the mod fork:
|
||||
1. `7a385d7` feat: Allow screenshot retrieval from device (#820)
|
||||
2. `cb24947` feat: Add central logging pragma (#843) — replaces Serial.printf with LOG_* macros across ~50 files, adds Logging library
|
||||
3. `6e51afb` fix: Account for nbsp character as non-breaking space (#757)
|
||||
|
||||
## Conflicts Resolved
|
||||
|
||||
### src/main.cpp (1 conflict region)
|
||||
- Kept mod's `HalPowerManager` (deep sleep, power saving) + upstream's `Logging.h` and `LOG_*` macros + screenshot serial handler (`logSerial`)
|
||||
|
||||
### src/activities/boot_sleep/SleepActivity.cpp (3 conflict regions)
|
||||
- Kept mod's entire letterbox fill rework (~330 lines of dithering, edge caching, etc.)
|
||||
- Replaced upstream's reverted positioning logic (size-gated) with mod's always-compute-scale approach
|
||||
- Applied upstream's `LOG_*` pattern to all mod `Serial.printf` calls
|
||||
|
||||
## Additional Changes (beyond conflict resolution)
|
||||
|
||||
- **lib/GfxRenderer/GfxRenderer.cpp** — Fixed one `Serial.printf` that auto-merge missed converting to `LOG_ERR` (caused linker error with the new Logging library's `#define Serial` macro)
|
||||
- **lib/hal/HalPowerManager.cpp** — Converted 4 `Serial.printf` calls to `LOG_DBG`/`LOG_ERR`, added `#include <Logging.h>`
|
||||
- **src/util/BookSettings.cpp** — Converted 3 `Serial.printf` calls to `LOG_DBG`/`LOG_ERR`, replaced `#include <HardwareSerial.h>` with `#include <Logging.h>`
|
||||
- **src/util/BookmarkStore.cpp** — Converted 2 `Serial.printf` calls to `LOG_ERR`/`LOG_DBG`, added `#include <Logging.h>`
|
||||
- **platformio.ini** — Added `-DENABLE_SERIAL_LOG` and `-DLOG_LEVEL=2` to `[env:mod]` build flags (was missing, other envs all have these)
|
||||
|
||||
## Build Verification
|
||||
|
||||
PlatformIO build (`pio run -e mod`) succeeded:
|
||||
- RAM: 31.0% (101724/327680 bytes)
|
||||
- Flash: 96.8% (6342796/6553600 bytes)
|
||||
|
||||
## Follow-up
|
||||
|
||||
- The merge is on branch `mod/merge-upstream` — fast-forward `mod/master` when ready
|
||||
- The `TxtReaderActivity.cpp` has a pre-existing `[[nodiscard]]` warning for `generateCoverBmp()` (not introduced by this merge)
|
||||
34
chat-summaries/2026-02-13_per-book-letterbox-fill.md
Normal file
34
chat-summaries/2026-02-13_per-book-letterbox-fill.md
Normal file
@@ -0,0 +1,34 @@
|
||||
# Per-book Letterbox Fill Override
|
||||
|
||||
**Date:** 2026-02-13
|
||||
**Branch:** mod/fix-edge-fills
|
||||
|
||||
## Task
|
||||
|
||||
Add the ability to override the sleep cover "letterbox fill mode" on a per-book basis (EPUB only for the menu UI; all book types respected at sleep time).
|
||||
|
||||
## Changes Made
|
||||
|
||||
### New files
|
||||
- `src/util/BookSettings.h` / `src/util/BookSettings.cpp` — Lightweight per-book settings utility. Stores a `letterboxFillOverride` field (0xFF = use global default) in `{cachePath}/book_settings.bin`. Versioned binary format with field count for forward compatibility, matching the pattern used by BookmarkStore and CrossPointSettings.
|
||||
|
||||
### Modified files
|
||||
- `src/activities/reader/EpubReaderMenuActivity.h` — Added `LETTERBOX_FILL` to the `MenuAction` enum. Added `bookCachePath`, `pendingLetterboxFill`, letterbox fill labels, and helper methods (`letterboxFillToIndex`, `indexToLetterboxFill`, `saveLetterboxFill`). Constructor now accepts a `bookCachePath` parameter and loads the current per-book settings.
|
||||
- `src/activities/reader/EpubReaderMenuActivity.cpp` — Handle `LETTERBOX_FILL` action: cycles through Default/Dithered/Solid/None on Confirm (handled locally like `ROTATE_SCREEN`), saves immediately. Renders the current value on the right side of the menu item.
|
||||
- `src/activities/reader/EpubReaderActivity.cpp` — Passes `epub->getCachePath()` to the menu activity constructor. Added `ROTATE_SCREEN` and `LETTERBOX_FILL` to the `onReaderMenuConfirm` switch as no-ops to prevent compiler warnings.
|
||||
- `src/activities/boot_sleep/SleepActivity.h` — Added `fillModeOverride` parameter to `renderBitmapSleepScreen()`.
|
||||
- `src/activities/boot_sleep/SleepActivity.cpp` — `renderCoverSleepScreen()` now loads `BookSettings` from the book's cache path after determining the book type. Passes the per-book override to `renderBitmapSleepScreen()`. `renderBitmapSleepScreen()` uses the override if valid, otherwise falls back to the global `SETTINGS.sleepScreenLetterboxFill`.
|
||||
|
||||
## How It Works
|
||||
|
||||
1. User opens the EPUB reader menu (Confirm button while reading).
|
||||
2. "Letterbox Fill" appears between "Reading Orientation" and "Table of Contents".
|
||||
3. Pressing Confirm cycles: Default → Dithered → Solid → None → Default...
|
||||
4. The selection is persisted immediately to `book_settings.bin` in the book's cache directory.
|
||||
5. When the device enters sleep with the cover screen, the per-book override is loaded and used instead of the global setting (if set).
|
||||
6. XTC and TXT books also have their per-book override checked at sleep time, but can only be configured for EPUB via the reader menu (XTC/TXT lack general menus).
|
||||
|
||||
## Follow-up Items
|
||||
|
||||
- Consider adding the letterbox fill override to XTC/TXT reader menus if those get general menus in the future.
|
||||
- The `BookSettings` struct is extensible — other per-book overrides can be added by appending fields and incrementing `BOOK_SETTINGS_COUNT`.
|
||||
36
chat-summaries/2026-02-13_summary.md
Normal file
36
chat-summaries/2026-02-13_summary.md
Normal file
@@ -0,0 +1,36 @@
|
||||
# Revert Letterbox Fill to Dithered / Solid / None (with edge cache)
|
||||
|
||||
**Date:** 2026-02-13
|
||||
|
||||
## Task
|
||||
Reverted letterbox fill modes from None/Solid/Extend Edges back to Dithered (default)/Solid/None per user request. Restored Bayer ordered dithering for "Dithered" mode and re-introduced edge average caching to avoid recomputing on every sleep.
|
||||
|
||||
## Changes Made
|
||||
|
||||
### `src/CrossPointSettings.h`
|
||||
- Reordered enum: `LETTERBOX_DITHERED=0`, `LETTERBOX_SOLID=1`, `LETTERBOX_NONE=2`
|
||||
- Changed default from `LETTERBOX_NONE` to `LETTERBOX_DITHERED`
|
||||
|
||||
### `src/SettingsList.h`
|
||||
- Updated UI labels from `{"None", "Solid", "Extend Edges"}` to `{"Dithered", "Solid", "None"}`
|
||||
|
||||
### `src/activities/boot_sleep/SleepActivity.h`
|
||||
- Added `#include <string>`
|
||||
- Restored `edgeCachePath` parameter to `renderBitmapSleepScreen()`
|
||||
|
||||
### `src/activities/boot_sleep/SleepActivity.cpp`
|
||||
- **Removed**: `getPackedPixel()`, `setPackedPixel()`, all EXTENDED-mode logic, `freeEdgeData()`, per-pixel edge arrays
|
||||
- **Simplified** `LetterboxFillData` to just `avgA`, `avgB`, `letterboxA`, `letterboxB`, `horizontal`, `valid`
|
||||
- **Restored** `BAYER_4X4[4][4]` matrix and `quantizeBayerDither()` function
|
||||
- **Renamed** `computeEdgeData()` → `computeEdgeAverages()` (averages-only, no edge pixel capture)
|
||||
- **Added** edge average cache: `loadEdgeCache()` / `saveEdgeCache()` (~12 byte binary file per cover)
|
||||
- **Updated** `drawLetterboxFill()`: DITHERED uses Bayer dithering, SOLID uses snap-to-level
|
||||
- **Updated** `renderBitmapSleepScreen()`: accepts `edgeCachePath`, tries cache before computing
|
||||
- **Updated** `renderCoverSleepScreen()`: derives `edgeCachePath` from cover BMP path (`_edges.bin`)
|
||||
|
||||
## Build
|
||||
Compilation succeeds (ESP32-C3 target, PlatformIO).
|
||||
|
||||
## Follow-up
|
||||
- The specific "The World in a Grain" cover still has rendering issues with dithered mode — to be investigated separately
|
||||
- Custom sleep BMPs (`/sleep/` directory, `/sleep.bmp`) intentionally skip caching since the selected BMP can change each sleep
|
||||
50
chat-summaries/2026-02-14_21-52-summary.md
Normal file
50
chat-summaries/2026-02-14_21-52-summary.md
Normal file
@@ -0,0 +1,50 @@
|
||||
# Placeholder Cover Generation for Books Without Covers
|
||||
|
||||
## Task
|
||||
Implement placeholder cover BMP generation for books that have no embedded cover image (or have covers in unsupported formats). Previously, these books showed empty rectangles on the home screen and fell back to the default sleep screen.
|
||||
|
||||
## Root Cause
|
||||
Cover generation failed silently in three cases:
|
||||
- **EPUB**: No `coverItemHref` in metadata, or cover is non-JPG format (PNG/SVG/GIF)
|
||||
- **TXT**: No matching image file on the SD card
|
||||
- **XTC**: First-page render failure (rare)
|
||||
|
||||
When `generateCoverBmp()` returned `false`, no BMP was created, and no fallback was attempted.
|
||||
|
||||
## Changes Made
|
||||
|
||||
### New Files
|
||||
- `lib/PlaceholderCover/PlaceholderCoverGenerator.h` - Header for the placeholder generator
|
||||
- `lib/PlaceholderCover/PlaceholderCoverGenerator.cpp` - Implementation: allocates a 1-bit pixel buffer, renders title/author text using EpdFont glyph data (ported from GfxRenderer::renderChar), writes a 1-bit BMP file with word-wrapped centered text, border, and separator line
|
||||
|
||||
### Modified Files
|
||||
- `src/activities/reader/EpubReaderActivity.cpp` - Added placeholder fallback after `generateCoverBmp()` and `generateThumbBmp()` fail during first-open prerender
|
||||
- `src/activities/reader/TxtReaderActivity.cpp` - Added placeholder fallback, added thumbnail generation (previously TXT had none), now passes thumb path to RECENT_BOOKS.addBook() instead of empty string
|
||||
- `src/activities/reader/XtcReaderActivity.cpp` - Added placeholder fallback after cover/thumb generation fail (rare case)
|
||||
- `src/activities/boot_sleep/SleepActivity.cpp` - Added placeholder generation before falling back to default sleep screen (handles books opened before this feature was added)
|
||||
- `lib/Txt/Txt.h` - Added `getThumbBmpPath()` and `getThumbBmpPath(int height)` methods
|
||||
- `lib/Txt/Txt.cpp` - Implemented the new thumb path methods
|
||||
|
||||
### Architecture
|
||||
- Option B approach: shared `PlaceholderCoverGenerator` utility called from reader activities
|
||||
- Generator is independent of `GfxRenderer` (no display framebuffer dependency)
|
||||
- Includes Ubuntu 10 Regular and Ubuntu 12 Bold font data directly for self-contained rendering
|
||||
- Memory: 48KB for full cover (480x800), 4-12KB for thumbnails; allocated and freed per call
|
||||
|
||||
## Flash Impact
|
||||
- Before: 96.4% flash usage (6,317,250 bytes)
|
||||
- After: 97.3% flash usage (6,374,546 bytes)
|
||||
- Delta: ~57KB (mostly from duplicate font bitmap data included in the generator)
|
||||
|
||||
## Layout Revision (traditional book cover style)
|
||||
- Border: moved inward with proportional edge padding (~10px at full size) and thickened to ~5px, keeping it visible within the device bezel
|
||||
- Title: 2x integer-scaled Ubuntu 12 Bold (effectively ~24pt) for full-size covers, positioned in the top 2/3 zone, max 5 lines
|
||||
- Author: positioned in the bottom 1/3 zone, max 3 lines with word wrapping
|
||||
- Book icon: 48x48 1-bit bitmap (generated by `scripts/generate_book_icon.py`), displayed at 2x for full covers, 1x for medium thumbnails, omitted for small thumbnails
|
||||
- Separator line between title and author zones
|
||||
- All dimensions scale proportionally for thumbnails
|
||||
|
||||
## Follow-up Items
|
||||
- Books opened before this feature was added won't get home screen thumbnails until re-opened (SleepActivity handles the sleep cover case by generating on demand)
|
||||
- Font data duplication adds ~57KB flash; could be reduced by exposing shared font references instead of including headers directly
|
||||
- Preview scripts in `scripts/` can regenerate the icon and layout previews: `generate_book_icon.py`, `preview_placeholder_cover.py`
|
||||
59
chat-summaries/2026-02-14_pr857-update-integration.md
Normal file
59
chat-summaries/2026-02-14_pr857-update-integration.md
Normal file
@@ -0,0 +1,59 @@
|
||||
# PR #857 Full Feature Update Integration
|
||||
|
||||
**Date:** 2026-02-14
|
||||
|
||||
## Task Description
|
||||
|
||||
Implemented the full feature update from PR #857 ("feat: Add dictionary word lookup feature") into our fork, following the detailed plan in `pr_857_update_integration_190041ae.plan.md`. This covered dictionary intelligence features (stemming, edit distance, fuzzy matching), the `ActivityWithSubactivity` refactor for inline definition display, en-dash/em-dash splitting, cross-page hyphenation, reverse-chronological lookup history, and a new "Did you mean?" suggestions activity.
|
||||
|
||||
## Changes Made
|
||||
|
||||
### New Files (2)
|
||||
- **`src/activities/reader/DictionarySuggestionsActivity.h`** — New "Did you mean?" activity header. Adapted from PR with `orientation` parameter for our `DictionaryDefinitionActivity` constructor.
|
||||
- **`src/activities/reader/DictionarySuggestionsActivity.cpp`** — Suggestions list UI with `UITheme`-aware layout, sub-activity management for definition display.
|
||||
|
||||
### Modified Files (9)
|
||||
1. **`src/util/Dictionary.h`** — Added `getStemVariants()`, `findSimilar()` (public) and `editDistance()` (private) declarations.
|
||||
2. **`src/util/Dictionary.cpp`** — Added ~250 lines: morphological stemming (`getStemVariants`), Levenshtein distance (`editDistance`), and fuzzy index scan (`findSimilar`). Preserved fork's `/.dictionary/` paths, `stardictCmp`/`asciiCaseCmp`, `cacheExists()`/`deleteCache()`.
|
||||
3. **`src/activities/reader/DictionaryDefinitionActivity.h`** — Added optional `onDone` callback parameter and member. Enables "Done" button to exit all the way back to the reader.
|
||||
4. **`src/activities/reader/DictionaryDefinitionActivity.cpp`** — Split Confirm handler: calls `onDone()` if set, else `onBack()`. Button hint shows "Done" when callback provided. Preserved all HTML parsing, styled rendering, side button hints.
|
||||
5. **`src/activities/reader/DictionaryWordSelectActivity.h`** — Changed base class to `ActivityWithSubactivity`. Replaced `onLookup` callback with `nextPageFirstWord` string. Added `pendingBackFromDef`/`pendingExitToReader` state.
|
||||
6. **`src/activities/reader/DictionaryWordSelectActivity.cpp`** — Major update:
|
||||
- En-dash/em-dash splitting in `extractWords()` (splits on U+2013/U+2014)
|
||||
- Cross-page hyphenation in `mergeHyphenatedWords()` using `nextPageFirstWord`
|
||||
- Cascading lookup flow: exact → stem variants → similar suggestions → "Not found"
|
||||
- Sub-activity delegation in `loop()` for definition/suggestions screens
|
||||
- Preserved custom `drawHints()` with overlap detection and `PageForward`/`PageBack` support
|
||||
7. **`src/activities/reader/LookedUpWordsActivity.h`** — Replaced `onSelectWord` with `onDone` callback. Added `readerFontId`, `orientation`, `pendingBackFromDef`/`pendingExitToReader`, `getPageItems()`.
|
||||
8. **`src/activities/reader/LookedUpWordsActivity.cpp`** — Major rewrite:
|
||||
- Reverse-chronological word display
|
||||
- Inline cascading lookup flow (same as word select)
|
||||
- `UITheme`-aware layout with `GUI.drawHeader()`/`GUI.drawList()`
|
||||
- `onNextRelease`/`onPreviousRelease`/`onNextContinuous`/`onPreviousContinuous` navigation
|
||||
- Sub-activity management for definition/suggestions
|
||||
- Preserved delete confirmation mode
|
||||
9. **`src/activities/reader/EpubReaderActivity.cpp`** — Simplified LOOKUP handler (removed `onLookup` callback, added `nextPageFirstWord` extraction). Simplified LOOKED_UP_WORDS handler (removed inline lookup, passes `readerFontId` and `orientation`). Removed unused `LookupHistory.h` include.
|
||||
|
||||
### Cleanup
|
||||
- Removed unused `DictionaryDefinitionActivity.h` include from `EpubReaderActivity.h`
|
||||
- Removed unused `util/LookupHistory.h` include from `EpubReaderActivity.cpp`
|
||||
|
||||
## Architectural Summary
|
||||
|
||||
**Before:** `EpubReaderActivity` orchestrated definition display via callbacks — word select and history both called back to the reader to create definition activities.
|
||||
|
||||
**After:** `DictionaryWordSelectActivity` and `LookedUpWordsActivity` manage their own sub-activity chains (definition, suggestions) using `ActivityWithSubactivity`. This enables the cascading lookup flow: exact match → stem variants → similar suggestions → "Not found".
|
||||
|
||||
## What Was Preserved (Fork Advantages)
|
||||
- Full HTML parsing in `DictionaryDefinitionActivity`
|
||||
- Custom `drawHints()` with overlap detection in `DictionaryWordSelectActivity`
|
||||
- `PageForward`/`PageBack` button support in word selection
|
||||
- `DELETE_DICT_CACHE` menu item and `cacheExists()`/`deleteCache()`
|
||||
- `stardictCmp`/`asciiCaseCmp` for proper StarDict index comparison
|
||||
- `/.dictionary/` path prefix
|
||||
|
||||
## Follow-up Items
|
||||
- Test the full lookup flow on device (exact → stems → suggestions → not found)
|
||||
- Verify cross-page hyphenation with a book that has page-spanning hyphenated words
|
||||
- Verify en-dash/em-dash splitting with books using those characters
|
||||
- Confirm reverse-chronological history order is intuitive for users
|
||||
29
chat-summaries/2026-02-15_00-15-summary.md
Normal file
29
chat-summaries/2026-02-15_00-15-summary.md
Normal file
@@ -0,0 +1,29 @@
|
||||
# Placeholder Cover Visual Refinements & Home Screen Integration
|
||||
|
||||
## Task
|
||||
Refined the placeholder cover layout to match a mockup, and integrated placeholder generation into the home screen's thumbnail loading.
|
||||
|
||||
## Changes Made
|
||||
|
||||
### Layout Refinements (`PlaceholderCoverGenerator.cpp`, `preview_placeholder_cover.py`)
|
||||
- **Icon position**: Moved from above-title to side-by-side (icon left, title right)
|
||||
- **Author scale**: Increased from 1x to 2x on full-size covers for larger author text
|
||||
- **Line spacing**: Reduced to 75% of advanceY so 2-3 title lines fit within icon height
|
||||
- **Vertical centering**: Title text centers against icon when 1-2 lines; top-aligns with overflow for 3+ lines. Uses `ascender`-based visual height instead of `advanceY`-based for accurate centering
|
||||
- **Horizontal centering**: The icon+gap+text block is now centered as a unit based on actual rendered text width, not the full available text area
|
||||
|
||||
### Home Screen Integration (`HomeActivity.cpp`)
|
||||
- Added `PlaceholderCoverGenerator` fallback in `loadRecentCovers()` — when format-specific `generateThumbBmp()` fails (or for TXT which had no handler), a placeholder thumbnail is generated instead of clearing `coverBmpPath` and showing a blank rectangle
|
||||
- This covers the case where a book was previously opened, cache was cleared, and the home screen needs to regenerate thumbnails
|
||||
|
||||
## Files Changed
|
||||
- `lib/PlaceholderCover/PlaceholderCoverGenerator.cpp` — layout logic updates
|
||||
- `scripts/preview_placeholder_cover.py` — matching preview updates
|
||||
- `src/activities/home/HomeActivity.cpp` — placeholder fallback in loadRecentCovers
|
||||
|
||||
## Commit
|
||||
`632b76c` on `mod/generate-placeholder-covers`
|
||||
|
||||
## Follow-up Items
|
||||
- Test on actual device to verify C++ bitmap font rendering matches preview expectations
|
||||
- The preview script uses Helvetica (different metrics than ubuntu_12_bold), so on-device appearance will differ slightly from previews
|
||||
55
chat-summaries/2026-02-15_00-30-summary.md
Normal file
55
chat-summaries/2026-02-15_00-30-summary.md
Normal file
@@ -0,0 +1,55 @@
|
||||
# Flash Size Optimization: Per-Family Font and Per-Language Hyphenation Flags
|
||||
|
||||
**Date**: 2026-02-15
|
||||
|
||||
## Task Description
|
||||
|
||||
Investigated ESP32-C3 flash usage (97.3% full at 6.375 MB / 6.5 MB app partition) and implemented build flags to selectively exclude font families and hyphenation language tries. Fonts alone consumed 65.6% of flash (3.99 MB).
|
||||
|
||||
## Changes Made
|
||||
|
||||
### 1. `lib/EpdFont/builtinFonts/all.h`
|
||||
- Wrapped Bookerly includes (16 files) in `#ifndef OMIT_BOOKERLY`
|
||||
- Wrapped Noto Sans 12-18pt includes (16 files) in `#ifndef OMIT_NOTOSANS` (kept `notosans_8_regular.h` outside guard for UI small font)
|
||||
- Wrapped OpenDyslexic includes (16 files) in `#ifndef OMIT_OPENDYSLEXIC`
|
||||
|
||||
### 2. `src/main.cpp`
|
||||
- Wrapped Bookerly 14pt font objects (previously always included) in `#ifndef OMIT_BOOKERLY`
|
||||
- Added per-family `#ifndef OMIT_*` guards inside the existing `#ifndef OMIT_FONTS` block for other sizes
|
||||
- Added matching guards to font registration in `setupDisplayAndFonts()`
|
||||
|
||||
### 3. `src/SettingsList.h`
|
||||
- Added `FontFamilyMapping` struct and `kFontFamilyMappings[]` compile-time table with per-family `#ifndef` guards
|
||||
- Switched Font Family setting from `SettingInfo::Enum` to `SettingInfo::DynamicEnum` with getter/setter that maps between list indices and fixed enum values (BOOKERLY=0, NOTOSANS=1, OPENDYSLEXIC=2)
|
||||
- Added `static_assert` ensuring at least one font family is available
|
||||
|
||||
### 4. `src/activities/settings/SettingsActivity.cpp`
|
||||
- Added DynamicEnum toggle support in `toggleCurrentSetting()` (cycles through options via `valueGetter`/`valueSetter`)
|
||||
- Added DynamicEnum display support in `render()` display lambda
|
||||
|
||||
### 5. `src/CrossPointSettings.cpp`
|
||||
- Guarded `getReaderFontId()` switch cases with `#ifndef OMIT_*`, added `default:` fallback to first available font
|
||||
- Guarded `getReaderLineCompression()` switch cases with `#ifndef OMIT_*`, added `default:` fallback
|
||||
- Added `#error` directive if all font families are omitted
|
||||
|
||||
### 6. `lib/Epub/Epub/hyphenation/LanguageRegistry.cpp`
|
||||
- Added per-language `#ifndef OMIT_HYPH_xx` guards around includes, `LanguageHyphenator` objects, and entries
|
||||
- Switched from `std::array<LanguageEntry, 6>` to `std::vector<LanguageEntry>` for variable entry count
|
||||
- Languages: DE (201 KB), EN (27 KB), ES (13 KB), FR (7 KB), IT (2 KB), RU (33 KB)
|
||||
|
||||
### 7. `platformio.ini`
|
||||
- Added to `[env:mod]`: `-DOMIT_OPENDYSLEXIC`, `-DOMIT_HYPH_DE`, `-DOMIT_HYPH_EN`, `-DOMIT_HYPH_ES`, `-DOMIT_HYPH_FR`, `-DOMIT_HYPH_IT`, `-DOMIT_HYPH_RU`
|
||||
|
||||
## Design Decisions
|
||||
- Enum values stay fixed (BOOKERLY=0, NOTOSANS=1, OPENDYSLEXIC=2) for settings file compatibility
|
||||
- Existing `OMIT_FONTS` flag left untouched; per-family flags nest inside it
|
||||
- DynamicEnum used for Font Family to handle index-to-value mapping when fonts are removed from the middle of the options list
|
||||
|
||||
## Estimated Savings (mod build)
|
||||
- OpenDyslexic fonts: ~1,052 KB
|
||||
- All hyphenation tries: ~282 KB
|
||||
- **Total: ~1,334 KB (~1.30 MB)** -- from 97.3% down to ~76.9%
|
||||
|
||||
## Follow-up Items
|
||||
- Build and verify the `mod` environment compiles cleanly and flash size reduction matches estimates
|
||||
- Other available flags for future use: `OMIT_BOOKERLY`, `OMIT_NOTOSANS` (individual language OMIT_HYPH_xx flags can also be used selectively)
|
||||
30
chat-summaries/2026-02-15_11-39-summary.md
Normal file
30
chat-summaries/2026-02-15_11-39-summary.md
Normal file
@@ -0,0 +1,30 @@
|
||||
# Table Rendering Fixes: Entities and Colspan Support
|
||||
|
||||
## Task Description
|
||||
Fix two issues with the newly implemented EPUB table rendering:
|
||||
1. Stray ` ` entities appearing as literal text in table cells instead of whitespace
|
||||
2. Cells with `colspan` attributes (e.g., section headers like "Anders Celsius", "Scientific career") rendering as narrow single-column cells instead of spanning the full table width
|
||||
|
||||
## Changes Made
|
||||
|
||||
### 1. `lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp` — `flushPartWordBuffer()`
|
||||
- Added detection and replacement of literal ` ` strings in the word buffer before flushing to `ParsedText`
|
||||
- This handles double-encoded `&nbsp;` entities common in Wikipedia and other generated EPUBs, where XML parsing converts `&` to `&` leaving literal ` ` in the character data
|
||||
|
||||
### 2. `lib/Epub/Epub/TableData.h` — `TableCell` struct
|
||||
- Added `int colspan = 1` field to store the HTML `colspan` attribute value
|
||||
|
||||
### 3. `lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp` — `startElement()`
|
||||
- Added parsing of the `colspan` attribute from `<td>` and `<th>` tags
|
||||
- Stores the parsed value (minimum 1) in the `TableCell::colspan` field
|
||||
|
||||
### 4. `lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp` — `processTable()`
|
||||
- **Column count**: Changed from `max(row.cells.size())` to sum of `cell.colspan` per row, correctly determining logical column count
|
||||
- **Natural width measurement**: Only non-spanning cells (colspan=1) contribute to per-column width calculations; spanning cells use combined width
|
||||
- **Layout**: Added `spanContentWidth()` and `spanFullCellWidth()` lambdas to compute the combined content width and full cell width for cells spanning multiple columns
|
||||
- **Cell mapping**: Each `PageTableCellData` now maps to an actual cell (not a logical column), with correct x-offset and combined column width for spanning cells
|
||||
- **Fill logic**: Empty cells are appended only for unused logical columns after all actual cells are placed
|
||||
|
||||
## Follow-up Items
|
||||
- Rowspan support is not yet implemented (uncommon in typical EPUB content)
|
||||
- The ` ` fix only handles the most common double-encoded entity; other double-encoded entities (e.g., `&mdash;`) could be handled similarly if needed
|
||||
49
chat-summaries/2026-02-15_13-00-summary.md
Normal file
49
chat-summaries/2026-02-15_13-00-summary.md
Normal file
@@ -0,0 +1,49 @@
|
||||
# Table Rendering Tweaks: Centering, Line Breaks, Padding
|
||||
|
||||
## Task Description
|
||||
Three visual tweaks to the EPUB table rendering based on comparison with a Wikipedia article (Anders Celsius):
|
||||
1. Full-width spanning rows (like "Anders Celsius", "Scientific career", "Signature") should be center-aligned
|
||||
2. `<br>` tags and block elements within table cells should create actual line breaks (e.g., "(aged 42)" then "Uppsala, Sweden" on the next line)
|
||||
3. Cell padding was too tight — text too close to grid borders
|
||||
|
||||
## Changes Made
|
||||
|
||||
### 1. Forced Line Breaks in Table Cells
|
||||
|
||||
**`lib/Epub/Epub/ParsedText.h`**:
|
||||
- Added `std::vector<bool> forceBreakAfter` member to track mandatory line break positions
|
||||
- Added `void addLineBreak()` public method
|
||||
|
||||
**`lib/Epub/Epub/ParsedText.cpp`**:
|
||||
- `addWord()`: grows `forceBreakAfter` vector alongside other word vectors
|
||||
- `addLineBreak()`: sets `forceBreakAfter.back() = true` on the last word
|
||||
- `computeLineBreaks()` (DP algorithm): respects forced breaks — cannot extend a line past a forced break point, and forced breaks override continuation groups
|
||||
- `computeHyphenatedLineBreaks()` (greedy): same — stops line at forced break, won't backtrack past one
|
||||
- `hyphenateWordAtIndex()`: when splitting a word, transfers the forced break flag to the remainder (last part)
|
||||
|
||||
**`lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp`** — `startNewTextBlock()`:
|
||||
- When `inTable`, instead of being a no-op, now: flushes the word buffer, calls `addLineBreak()` on the current ParsedText, and resets `nextWordContinues`
|
||||
- This means `<br>`, `<p>`, `<div>` etc. within table cells now produce real visual line breaks
|
||||
|
||||
### 2. Center-Aligned Full-Width Spanning Cells
|
||||
|
||||
**`lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp`** — `processTable()`:
|
||||
- Before laying out a cell's content, checks if `cell.colspan >= numCols` (spans full table width)
|
||||
- If so, sets the cell's BlockStyle alignment to `CssTextAlign::Center`
|
||||
- This correctly centers section headers and title rows in Wikipedia infobox-style tables
|
||||
|
||||
### 3. Increased Cell Padding
|
||||
|
||||
**`lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp`**:
|
||||
- `TABLE_CELL_PAD_X`: 2 → 4 pixels (horizontal padding)
|
||||
- Added `TABLE_CELL_PAD_Y = 2` pixels (vertical padding)
|
||||
- Row height now includes `2 * TABLE_CELL_PAD_Y` for top/bottom padding
|
||||
|
||||
**`lib/Epub/Epub/Page.cpp`**:
|
||||
- `TABLE_CELL_PADDING_X`: 2 → 4 pixels (matches parser constant)
|
||||
- Added `TABLE_CELL_PADDING_Y = 2` pixels
|
||||
- Cell text Y position now accounts for vertical padding: `baseY + 1 + TABLE_CELL_PADDING_Y`
|
||||
|
||||
## Follow-up Items
|
||||
- The padding constants are duplicated between `ChapterHtmlSlimParser.cpp` and `Page.cpp` — could be unified into a shared header
|
||||
- Vertical centering within cells (when a cell has fewer lines than the tallest cell) is not implemented
|
||||
32
chat-summaries/2026-02-15_14-30-summary.md
Normal file
32
chat-summaries/2026-02-15_14-30-summary.md
Normal file
@@ -0,0 +1,32 @@
|
||||
# Table Width Hints: HTML Attributes + CSS Width Support
|
||||
|
||||
## Task Description
|
||||
Add support for author-specified column widths from HTML `width` attributes and CSS `width` property on `<table>`, `<col>`, `<td>`, and `<th>` elements, using them as hints for column sizing in `processTable()`.
|
||||
|
||||
## Changes Made
|
||||
|
||||
### 1. CSS Layer (`lib/Epub/Epub/css/CssStyle.h`, `lib/Epub/Epub/css/CssParser.cpp`)
|
||||
- Added `width` bit to `CssPropertyFlags`
|
||||
- Added `CssLength width` field to `CssStyle`
|
||||
- Added `hasWidth()` convenience method
|
||||
- Updated `applyOver()`, `reset()`, `clearAll()`, `anySet()` to include `width`
|
||||
- Added `else if (propName == "width")` case to `CssParser::parseDeclarations()` using `interpretLength()`
|
||||
|
||||
### 2. Table Data (`lib/Epub/Epub/TableData.h`)
|
||||
- Added `CssLength widthHint` and `bool hasWidthHint` to `TableCell`
|
||||
- Added `std::vector<CssLength> colWidthHints` to `TableData` (from `<col>` tags)
|
||||
- Added `#include "css/CssStyle.h"` for `CssLength`
|
||||
|
||||
### 3. Parser (`lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp`)
|
||||
- Removed `"col"` from `TABLE_SKIP_TAGS` (was being skipped entirely)
|
||||
- Added `parseHtmlWidthAttr()` helper: parses HTML `width="200"` (pixels) and `width="50%"` (percent) into `CssLength`
|
||||
- Added `<col>` handling in `startElement`: parses `width` HTML attribute and `style` CSS, pushes to `tableData->colWidthHints`
|
||||
- Updated `<td>`/`<th>` handling: now parses `width` HTML attribute, `style` attribute, and stylesheet CSS width (via `resolveStyle`). CSS takes priority over HTML attribute. Stored in `TableCell::widthHint`
|
||||
|
||||
### 4. Layout (`processTable()`)
|
||||
- Added step 3a: resolves width hints per column. Priority: `<col>` hints > max cell hint (colspan=1 only). Percentages resolve relative to available content width.
|
||||
- Modified step 3b: hinted columns get their resolved pixel width (clamped to min col width). If all hinted widths exceed available space, they're scaled down proportionally. Unhinted columns use the existing two-pass fair-share algorithm on the remaining space.
|
||||
|
||||
## Follow-up Items
|
||||
- The `<colgroup>` tag is still skipped entirely; `<col>` tags within `<colgroup>` won't be reached. Could un-skip `<colgroup>` (make it transparent like `<thead>`/`<tbody>`) if needed.
|
||||
- `rowspan` is not yet supported.
|
||||
41
chat-summaries/2026-02-15_17-21-summary.md
Normal file
41
chat-summaries/2026-02-15_17-21-summary.md
Normal file
@@ -0,0 +1,41 @@
|
||||
# Cherry-pick Image Support from pablohc/crosspoint-reader@2d8cbcf (PR #556)
|
||||
|
||||
## Task
|
||||
Merge EPUB embedded image support (JPEG/PNG) from pablohc's fork into the mod branch, based on upstream PR #556.
|
||||
|
||||
## Changes Made
|
||||
|
||||
### New Files (11)
|
||||
- `lib/Epub/Epub/blocks/ImageBlock.h` / `.cpp` - Image block type for page layout
|
||||
- `lib/Epub/Epub/converters/DitherUtils.h` - 4x4 Bayer dithering for 4-level grayscale
|
||||
- `lib/Epub/Epub/converters/ImageDecoderFactory.h` / `.cpp` - Format-based decoder selection
|
||||
- `lib/Epub/Epub/converters/ImageToFramebufferDecoder.h` / `.cpp` - Base decoder interface
|
||||
- `lib/Epub/Epub/converters/JpegToFramebufferConverter.h` / `.cpp` - JPEG decoder (picojpeg)
|
||||
- `lib/Epub/Epub/converters/PngToFramebufferConverter.h` / `.cpp` - PNG decoder (PNGdec)
|
||||
- `lib/Epub/Epub/converters/PixelCache.h` - 2-bit pixel cache for fast re-render
|
||||
- `scripts/generate_test_epub.py` - Test EPUB generator
|
||||
|
||||
### Modified Files (13)
|
||||
- `lib/Epub/Epub/blocks/Block.h` - Removed unused `layout()` virtual
|
||||
- `lib/Epub/Epub/blocks/TextBlock.h` - Removed unused `layout()` override
|
||||
- `lib/Epub/Epub/Page.h` / `.cpp` - Added `PageImage` class, `TAG_PageImage=3`, `hasImages()`, `getImageBoundingBox()`
|
||||
- `lib/Epub/Epub/parsers/ChapterHtmlSlimParser.h` / `.cpp` - Image extraction/decoding from EPUB, new constructor params
|
||||
- `lib/Epub/Epub/Section.cpp` - Derive content base/image paths, bumped version 12→13
|
||||
- `lib/GfxRenderer/GfxRenderer.h` / `.cpp` - Added `getRenderMode()`, implemented `displayWindow()`
|
||||
- `lib/hal/HalDisplay.h` / `.cpp` - Added `displayWindow()` for partial refresh
|
||||
- `src/activities/reader/EpubReaderActivity.cpp` - Image-aware refresh with double FAST_REFRESH optimization
|
||||
- `platformio.ini` - Added `PNGdec` dependency, `PNG_MAX_BUFFERED_PIXELS=6402` build flag
|
||||
|
||||
## Key Conflict Resolutions
|
||||
- `TAG_PageImage = 3` (not 2) to avoid collision with mod's `TAG_PageTableRow = 2`
|
||||
- Preserved mod's bookmark ribbon rendering in `renderContents`
|
||||
- Preserved mod's table rendering (`PageTableRow`) alongside new `PageImage`
|
||||
- Section file version bumped to invalidate cached sections
|
||||
|
||||
## Build Result
|
||||
- `mod` environment: SUCCESS (RAM 31.0%, Flash 77.5%)
|
||||
|
||||
## Follow-up Items
|
||||
- Test on device with JPEG/PNG EPUBs
|
||||
- Run `scripts/generate_test_epub.py` to create test EPUBs
|
||||
- Consider whether `displayWindow()` experimental path should be enabled
|
||||
52
chat-summaries/2026-02-15_17-30-summary.md
Normal file
52
chat-summaries/2026-02-15_17-30-summary.md
Normal file
@@ -0,0 +1,52 @@
|
||||
# EPUB Table Rendering Implementation
|
||||
|
||||
## Task
|
||||
Replace the `[Table omitted]` placeholder in the EPUB reader with full column-aligned table rendering, including grid lines, proportional column widths, and proper serialization.
|
||||
|
||||
## Changes Made
|
||||
|
||||
### New file
|
||||
- **`lib/Epub/Epub/TableData.h`** -- Lightweight structs (`TableCell`, `TableRow`, `TableData`) for buffering table content during SAX parsing.
|
||||
|
||||
### Modified files
|
||||
|
||||
- **`lib/Epub/Epub/ParsedText.h` / `.cpp`**
|
||||
- Added `getNaturalWidth()` public method to measure the single-line content width of a ParsedText. Used by column width calculation.
|
||||
|
||||
- **`lib/Epub/Epub/Page.h` / `.cpp`**
|
||||
- Added `TAG_PageTableRow = 2` to `PageElementTag` enum.
|
||||
- Added `getTag()` pure virtual method to `PageElement` base class for tag-based serialization.
|
||||
- Added `PageTableCellData` struct (cell lines, column width, x-offset).
|
||||
- Added `PageTableRow` class with render (grid lines + cell text), serialize, and deserialize support.
|
||||
- Updated `Page::serialize()` to use `el->getTag()` instead of hardcoded tag.
|
||||
- Updated `Page::deserialize()` to handle `TAG_PageTableRow`.
|
||||
|
||||
- **`lib/Epub/Epub/parsers/ChapterHtmlSlimParser.h`**
|
||||
- Added `#include "../TableData.h"`.
|
||||
- Added table state fields: `bool inTable`, `std::unique_ptr<TableData> tableData`.
|
||||
- Added `processTable()` and `addTableRowToPage()` method declarations.
|
||||
|
||||
- **`lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp`**
|
||||
- Added table-related tag arrays (`TABLE_TRANSPARENT_TAGS`, `TABLE_SKIP_TAGS`).
|
||||
- Replaced `[Table omitted]` placeholder with full table buffering logic in `startElement`.
|
||||
- Modified `startNewTextBlock` to be a no-op when inside a table (cell content stays in one ParsedText).
|
||||
- Added table close handling in `endElement` for `</td>`, `</th>`, and `</table>`.
|
||||
- Disabled the 750-word early split when inside a table.
|
||||
- Implemented `processTable()`: column width calculation (natural + proportional distribution), per-cell layout via `layoutAndExtractLines`, `PageTableRow` creation.
|
||||
- Implemented `addTableRowToPage()`: page-break handling for table rows.
|
||||
|
||||
## Design Decisions
|
||||
- Tables are buffered entirely during parsing, then processed on `</table>` close (two-pass: measure then layout).
|
||||
- Column widths are proportional to natural content width, with equal distribution of extra space when content fits.
|
||||
- Grid lines (1px) drawn around every cell; 2px horizontal cell padding.
|
||||
- Nested tables are skipped (v1 limitation).
|
||||
- `<caption>`, `<colgroup>`, `<col>` are skipped; `<thead>`, `<tbody>`, `<tfoot>` are transparent.
|
||||
- `<th>` cells get bold text. Cell text is left-aligned with no paragraph indent.
|
||||
- Serialization is backward-compatible: old firmware encountering the new tag will re-parse the section.
|
||||
|
||||
## Follow-up Items
|
||||
- Nested table support (currently skipped)
|
||||
- `colspan` / `rowspan` support
|
||||
- `<caption>` rendering as centered text above the table
|
||||
- CSS border detection (currently always draws grid lines)
|
||||
- Consider CSS-based cell alignment
|
||||
39
chat-summaries/2026-02-15_20-30-summary.md
Normal file
39
chat-summaries/2026-02-15_20-30-summary.md
Normal file
@@ -0,0 +1,39 @@
|
||||
# Adjust Low Power Mode: Fix Processing Bug and Sync with PR #852
|
||||
|
||||
**Date:** 2026-02-15
|
||||
**Branch:** mod/adjust-low-power-mode
|
||||
|
||||
## Task
|
||||
|
||||
Fix a bug where the device enters low-power mode (10MHz CPU) during first-time book opening and chapter indexing, causing significant slowdown. Also sync minor logging differences with upstream PR #852.
|
||||
|
||||
## Root Causes (three issues combined)
|
||||
|
||||
1. **Missing delegation in ActivityWithSubactivity**: `main.cpp` calls `preventAutoSleep()` on `ReaderActivity` (the top-level activity). `ReaderActivity` creates `EpubReaderActivity` as a subactivity, but `ActivityWithSubactivity` never delegated `preventAutoSleep()` or `skipLoopDelay()` to the active subactivity.
|
||||
|
||||
2. **Stale check across activity transitions**: The `preventAutoSleep()` check at the top of the main loop runs before `loop()`. When an activity transitions mid-loop (HomeActivity -> ReaderActivity), the pre-loop check is stale but the post-loop power-saving decision fires.
|
||||
|
||||
3. **Section object vs section file**: `!section` alone was insufficient as a condition. The Section object is created early in the `!section` block, making `section` non-null, but `createSectionFile()` (the slow operation) runs afterward. A separate `loadingSection` flag is needed to cover the full duration.
|
||||
|
||||
## Changes Made
|
||||
|
||||
1. **ActivityWithSubactivity** (`src/activities/ActivityWithSubactivity.h`)
|
||||
- Added `preventAutoSleep()` override that delegates to `subActivity->preventAutoSleep()`
|
||||
- Added `skipLoopDelay()` override with same delegation pattern
|
||||
|
||||
2. **main.cpp** (`src/main.cpp`)
|
||||
- Added a second `preventAutoSleep()` re-check after `currentActivity->loop()` returns, before the power-saving block
|
||||
|
||||
3. **EpubReaderActivity** (`src/activities/reader/EpubReaderActivity.h`, `.cpp`)
|
||||
- Added `volatile bool loadingSection` flag
|
||||
- `preventAutoSleep()` returns `!section || loadingSection`
|
||||
- `!section` covers the pre-Section-object period (including cover prerendering in onEnter)
|
||||
- `loadingSection` covers the full `!section` block in `renderScreen()` where `createSectionFile()` runs
|
||||
- Flag is also cleared on the error path
|
||||
|
||||
4. **TxtReaderActivity** (`src/activities/reader/TxtReaderActivity.h`)
|
||||
- `preventAutoSleep()` returns `!initialized`
|
||||
- Covers cover prerendering and page index building
|
||||
|
||||
5. **HalPowerManager.cpp** (`lib/hal/HalPowerManager.cpp`)
|
||||
- Synced log messages with upstream PR: `LOG_ERR` -> `LOG_DBG` with frequency values (matching commit `ff89fb1`)
|
||||
28
chat-summaries/2026-02-15_cover-thumbnail-fix.md
Normal file
28
chat-summaries/2026-02-15_cover-thumbnail-fix.md
Normal file
@@ -0,0 +1,28 @@
|
||||
# Fix: Cover/Thumbnail Pipeline on Home Screen
|
||||
|
||||
**Date:** 2026-02-15
|
||||
|
||||
## Task Description
|
||||
|
||||
Multiple issues with book cover thumbnails on the home screen:
|
||||
1. After clearing a book's cache, the home screen showed a placeholder instead of the real cover.
|
||||
2. Books without covers showed blank rectangles instead of generated placeholder covers.
|
||||
|
||||
## Root Cause
|
||||
|
||||
`Epub::generateThumbBmp()` wrote an empty 0-byte BMP file as a "don't retry" sentinel when a book had no cover. This empty file:
|
||||
- Blocked the placeholder fallback in `EpubReaderActivity::onEnter()` (file exists check passes)
|
||||
- Tricked the home screen into thinking a valid thumbnail exists (skips regeneration)
|
||||
- Failed to parse in `LyraTheme::drawRecentBookCover()` resulting in a blank gray rectangle
|
||||
|
||||
## Changes Made
|
||||
|
||||
- **`lib/Epub/Epub.cpp`**: Removed the empty sentinel file write from `generateThumbBmp()`. Now it simply returns `false` when there's no cover, letting callers generate valid placeholder BMPs that serve the same "don't retry" purpose.
|
||||
- **`src/activities/home/HomeActivity.cpp`**: Changed placeholder fallback in `loadRecentCovers()` from `if (!success && !Storage.exists(coverPath))` to `if (!success)` as defense-in-depth for edge cases like global cache clear.
|
||||
- **`src/RecentBooksStore.h`**: Added `removeBook(const std::string& path)` method declaration.
|
||||
- **`src/RecentBooksStore.cpp`**: Implemented `removeBook()` — finds and erases the book by path, then persists the updated list.
|
||||
- **`src/activities/reader/EpubReaderActivity.cpp`**: After clearing cache in the `DELETE_CACHE` handler, calls `RECENT_BOOKS.removeBook(epub->getPath())` so the book is cleanly removed from recents when its cache is wiped.
|
||||
|
||||
## Follow-up Items
|
||||
|
||||
- None.
|
||||
42
chat-summaries/2026-02-15_merge-master-css-perf.md
Normal file
42
chat-summaries/2026-02-15_merge-master-css-perf.md
Normal file
@@ -0,0 +1,42 @@
|
||||
# Merge upstream master (CSS perf #779) into mod/master-img
|
||||
|
||||
**Date**: 2026-02-15
|
||||
|
||||
## Task
|
||||
|
||||
Merge the latest changes from `master` (upstream) into `mod/master-img`.
|
||||
|
||||
## Changes Merged
|
||||
|
||||
One upstream commit: `46c2109 perf: Improve large CSS files handling (#779)`
|
||||
|
||||
This commit significantly refactored the CSS subsystem:
|
||||
- Streaming CSS parser with `StackBuffer` for zero-heap parsing
|
||||
- Extracted `parseDeclarationIntoStyle()` from inline logic
|
||||
- Rule limits and selector validation
|
||||
- `CssParser` now owns its `cachePath` and manages caching internally
|
||||
- CSS loading skipped when "Book's Embedded Style" is off
|
||||
|
||||
## Conflicts Resolved
|
||||
|
||||
### Section.cpp (2 regions)
|
||||
- Combined mod's image support variables (`contentBase`, `imageBasePath`) with master's new CSS parser loading pattern (`cssParser->loadFromCache()`)
|
||||
- Merged constructor call: kept mod's `epub`, `contentBase`, `imageBasePath` params while adopting master's `cssParser` local variable pattern
|
||||
- Added master's `cssParser->clear()` calls on error/success paths
|
||||
|
||||
### CssParser.cpp (1 region)
|
||||
- Accepted master's complete rewrite of the CSS parser
|
||||
- Ported mod's `width` CSS property handler into the new `parseDeclarationIntoStyle()` function
|
||||
|
||||
## Auto-merged Files Verified
|
||||
- `CssStyle.h`: `width` property and supporting code preserved
|
||||
- `platformio.ini`: `PNGdec` library and `PNG_MAX_BUFFERED_PIXELS` preserved; `[env:mod]` section intact; master's `gnu++2a` and `build_unflags` applied
|
||||
- `ChapterHtmlSlimParser.h`: image/table support members preserved
|
||||
- `RecentBooksStore.cpp`: `removeBook()` method preserved
|
||||
- `Epub.cpp`, `Epub.h`, `CssParser.h`, `ReaderActivity.cpp`: auto-merged cleanly
|
||||
|
||||
## Build Result
|
||||
- `pio run -e mod` succeeded with zero errors
|
||||
|
||||
## Commit
|
||||
- `744d616 Merge branch 'master' into mod/master-img`
|
||||
51
chat-summaries/2026-02-16_05-30-summary.md
Normal file
51
chat-summaries/2026-02-16_05-30-summary.md
Normal file
@@ -0,0 +1,51 @@
|
||||
# Port PR #838 and PR #907 into fork
|
||||
|
||||
## Task
|
||||
Cherry-pick / manually port two upstream PRs into the fork:
|
||||
- **PR #907**: Cover image outlines to improve legibility
|
||||
- **PR #838**: Fallback logic for epub cover extraction
|
||||
|
||||
## Changes Made
|
||||
|
||||
### PR #907 (cover outlines) — `src/components/themes/lyra/LyraTheme.cpp`
|
||||
- Always draw a rectangle outline around cover tiles before drawing the bitmap on top
|
||||
- Removed `hasCover` flag — simplified logic so outline is always present, preventing low-contrast covers from blending into the background
|
||||
|
||||
### PR #838 (cover fallback logic) — 3 files
|
||||
|
||||
#### `lib/Epub/Epub.h`
|
||||
- Added declarations for 4 new methods: `generateInvalidFormatCoverBmp`, `generateInvalidFormatThumbBmp`, `isValidThumbnailBmp` (static), `getCoverCandidates` (private)
|
||||
- Added doc comments on existing `generateCoverBmp` and `generateThumbBmp`
|
||||
|
||||
#### `lib/Epub/Epub.cpp`
|
||||
- Added `#include <HalDisplay.h>` and `#include <algorithm>`
|
||||
- **`generateCoverBmp`**: Added invalid BMP detection/retry, cover fallback candidate probing (`getCoverCandidates`), case-insensitive extension checking. Returns `false` on failure (no internal X-pattern fallback) so callers control the fallback chain
|
||||
- **`generateThumbBmp`**: Same changes as `generateCoverBmp` — invalid BMP detection, fallback probing, case-insensitive check. Returns `false` on failure (no internal X-pattern fallback) so callers control the fallback chain
|
||||
- **`generateInvalidFormatThumbBmp`** (new): Creates 1-bit BMP with X pattern as thumbnail marker
|
||||
- **`generateInvalidFormatCoverBmp`** (new): Creates 1-bit BMP with X pattern as cover marker, using display dimensions
|
||||
- **`isValidThumbnailBmp`** (new, static): Validates BMP by checking file size > 0 and 'BM' header
|
||||
- **`getCoverCandidates`** (new, private): Returns list of common cover filenames to probe
|
||||
|
||||
#### `src/activities/home/HomeActivity.cpp`
|
||||
- Replaced `Storage.exists()` check with `Epub::isValidThumbnailBmp()` to catch corrupt/empty thumbnails
|
||||
- Added epub.load retry logic (cache-only first, then full build)
|
||||
- After successful thumbnail generation, update `RECENT_BOOKS` with the new thumb path
|
||||
- Thumbnail fallback chain: Real Cover → PlaceholderCoverGenerator → X-Pattern marker (epub only; xtc/other formats fall back to placeholder only)
|
||||
|
||||
## Adaptations from upstream PR
|
||||
- All `Serial.printf` calls converted to `LOG_DBG`/`LOG_ERR` macros (fork convention)
|
||||
- No `#include <HardwareSerial.h>` (not needed with Logging.h)
|
||||
- Retained fork's `bookMetadataCache` null-check guards
|
||||
- HomeActivity changes manually adapted to fork's `loadRecentCovers()` structure (upstream has inline code in a different method)
|
||||
- Fork's `PlaceholderCoverGenerator` is the preferred fallback; X-pattern marker is last resort only
|
||||
|
||||
#### `src/activities/boot_sleep/SleepActivity.cpp`
|
||||
- EPUB sleep screen cover now follows Real Cover → Placeholder → X-Pattern fallback chain
|
||||
- Upgraded `Storage.exists()` check to `Epub::isValidThumbnailBmp()` for the epub cover path
|
||||
|
||||
#### `src/activities/reader/EpubReaderActivity.cpp`
|
||||
- Cover and thumbnail pre-rendering now follows Real Cover → Placeholder → X-Pattern fallback chain
|
||||
- Upgraded all `Storage.exists()` checks to `Epub::isValidThumbnailBmp()` for cover/thumb paths
|
||||
|
||||
## Follow-up Items
|
||||
- Build and test on device to verify cover generation pipeline works end-to-end
|
||||
29
chat-summaries/2026-02-16_15-18-summary.md
Normal file
29
chat-summaries/2026-02-16_15-18-summary.md
Normal file
@@ -0,0 +1,29 @@
|
||||
# Created CrossPoint Reader Development Skill
|
||||
|
||||
**Date**: 2026-02-16
|
||||
**Task**: Create a Cursor agent skill for CrossPoint Reader firmware development guidance.
|
||||
|
||||
## Changes Made
|
||||
|
||||
Created project-level skill at `.cursor/skills/crosspoint-reader-dev/` with 6 files:
|
||||
|
||||
| File | Lines | Purpose |
|
||||
|------|-------|---------|
|
||||
| `SKILL.md` | 202 | Core rules: agent identity, hardware constraints, resource protocol, architecture overview, HAL usage, coding standards, error handling, activity lifecycle, UI rules |
|
||||
| `architecture.md` | 138 | Build system (PlatformIO CLI + VS Code), build flags, environments, generated files, local config, platform detection |
|
||||
| `coding-patterns.md` | 135 | FreeRTOS tasks, malloc patterns, global font loading, button mapping, UI rendering rules |
|
||||
| `debugging-and-testing.md` | 148 | Build commands, serial monitoring, crash debugging (OOM, stack overflow, use-after-free, watchdog), testing checklist, CI/CD pipeline |
|
||||
| `git-workflow.md` | 98 | Repository detection, branch naming, commit messages, when to commit |
|
||||
| `cache-management.md` | 100 | SD card cache structure, invalidation rules, file format versioning |
|
||||
|
||||
## Design Decisions
|
||||
|
||||
- **Progressive disclosure**: SKILL.md kept to 202 lines (well under 500 limit) with always-needed info; detailed references in separate files one level deep
|
||||
- **Project-level storage**: `.cursor/skills/` so it's shared with anyone using the repo
|
||||
- **Description** includes broad trigger terms: ESP32-C3, PlatformIO, EPUB, e-ink, HAL, activity lifecycle, FreeRTOS, SD card, embedded C++
|
||||
|
||||
## Follow-up Items
|
||||
|
||||
- Consider adding the skill directory to `.gitignore` if this should remain personal, or commit if sharing with collaborators
|
||||
- Update cache file format version numbers if they've changed since the guide was written
|
||||
- Skill will auto-activate when the agent detects firmware/embedded development context
|
||||
57
chat-summaries/2026-02-16_19-30-summary.md
Normal file
57
chat-summaries/2026-02-16_19-30-summary.md
Normal file
@@ -0,0 +1,57 @@
|
||||
# Reader Menu Improvements
|
||||
|
||||
## Task
|
||||
Overhaul the EPUB reader menu: consolidate dictionary actions behind long-press, add portrait/landscape toggle with preferred-orientation settings, add font size cycling, and rename several menu labels.
|
||||
|
||||
## Changes Made
|
||||
|
||||
### 1. Consolidated Dictionary Menu Items
|
||||
- Removed "Lookup Word History" and "Delete Dictionary Cache" from the reader menu
|
||||
- Long-pressing Confirm on "Lookup Word" now opens the Lookup History screen
|
||||
- "Delete Dictionary Cache" moved to a sentinel entry at the bottom of the Lookup History word list
|
||||
|
||||
**Files:** `EpubReaderMenuActivity.h`, `EpubReaderMenuActivity.cpp`, `LookedUpWordsActivity.h`, `LookedUpWordsActivity.cpp`, `EpubReaderActivity.cpp`
|
||||
|
||||
### 2. Toggle Portrait/Landscape
|
||||
- Renamed "Reading Orientation" to "Toggle Portrait/Landscape" in the reader menu (kept the original name in global Settings)
|
||||
- Short-press now toggles between preferred portrait and preferred landscape orientations
|
||||
- Long-press opens a popup sub-menu with all 4 orientation options
|
||||
- Added `preferredPortrait` and `preferredLandscape` settings to `CrossPointSettings` (serialized at end for backward-compat)
|
||||
- Added corresponding settings to `SettingsList.h` using `DynamicEnum` (maps non-sequential enum values correctly)
|
||||
|
||||
**Files:** `CrossPointSettings.h`, `CrossPointSettings.cpp`, `SettingsList.h`, `EpubReaderMenuActivity.h`, `EpubReaderMenuActivity.cpp`
|
||||
|
||||
### 3. Toggle Font Size
|
||||
- Added new `TOGGLE_FONT_SIZE` menu action
|
||||
- Cycles through Small → Medium → Large → Extra Large → Small on each press
|
||||
- Shows current size value next to the label (like orientation)
|
||||
- Applied on menu exit via extended `onBack` callback `(uint8_t orientation, uint8_t fontSize)`
|
||||
- Added `applyFontSize()` to `EpubReaderActivity` (saves to settings, resets section for re-layout)
|
||||
|
||||
**Files:** `EpubReaderMenuActivity.h`, `EpubReaderMenuActivity.cpp`, `EpubReaderActivity.h`, `EpubReaderActivity.cpp`
|
||||
|
||||
### 4. Label Renames
|
||||
- "Letterbox Fill" → "Override Letterbox Fill" (reader menu only; global setting keeps original name)
|
||||
- "Sync Progress" → "Sync Reading Progress"
|
||||
|
||||
**Files:** `english.yaml` (I18n source), regenerated `I18nKeys.h`, `I18nStrings.h`, `I18nStrings.cpp`
|
||||
|
||||
### 5. Long-Press Safety
|
||||
- Added `ignoreNextConfirmRelease` flag to `EpubReaderMenuActivity` to prevent stale releases
|
||||
- Added `initialSkipRelease` constructor parameter to `LookedUpWordsActivity`
|
||||
- Extended `ignoreNextConfirmRelease` guard to cover the word-selection path (not just delete-confirm mode)
|
||||
- Orientation sub-menu also uses `ignoreNextConfirmRelease` to avoid selecting on long-press release
|
||||
|
||||
### 6. New I18n Strings
|
||||
- `STR_TOGGLE_ORIENTATION`: "Toggle Portrait/Landscape"
|
||||
- `STR_TOGGLE_FONT_SIZE`: "Toggle Font Size"
|
||||
- `STR_OVERRIDE_LETTERBOX_FILL`: "Override Letterbox Fill"
|
||||
- `STR_PREFERRED_PORTRAIT`: "Preferred Portrait"
|
||||
- `STR_PREFERRED_LANDSCAPE`: "Preferred Landscape"
|
||||
|
||||
## Build Status
|
||||
Successfully compiled (default environment). RAM: 31.1%, Flash: 99.4%.
|
||||
|
||||
## Follow-up Items
|
||||
- Translations: New strings fall back to English for all non-English languages. Translators can add entries to their respective YAML files.
|
||||
- The `SettingInfo::Enum` approach doesn't work for non-sequential enum values (portrait=0, inverted=2). Used `DynamicEnum` with getter/setter lambdas instead.
|
||||
31
chat-summaries/2026-02-16_21-30-summary.md
Normal file
31
chat-summaries/2026-02-16_21-30-summary.md
Normal file
@@ -0,0 +1,31 @@
|
||||
# Merge Assessment & Cherry-pick: master -> mod/master
|
||||
|
||||
## Task
|
||||
Assess and merge new commits from `master` into `mod/master`.
|
||||
|
||||
## Analysis
|
||||
- 19 commits on `master` not on `mod/master`, but 16 were already cherry-picked or manually ported (different hashes, same content)
|
||||
- A full `git merge master` produced 30+ conflicts due to duplicate cherry-picks with different patch IDs
|
||||
- Identified 3 genuinely new commits
|
||||
|
||||
## Changes Made
|
||||
|
||||
### 1. Cherry-pick `97c3314` (#932) - `f21720d`
|
||||
- perf: Skip constructing unnecessary `std::string` in TextBlock.cpp
|
||||
- 1-line change, applied cleanly
|
||||
|
||||
### 2. Cherry-pick `2a32d8a` (#926) - `424e332`
|
||||
- chore: Improve Russian language support
|
||||
- Renamed `russia.yaml` -> `russian.yaml`, updated `i18n.md`, fixed translation strings
|
||||
- Applied cleanly
|
||||
|
||||
### 3. Cherry-pick `0bc6747` (#827) - `61fb11c`
|
||||
- feat: Add PNG cover image support for EPUB books
|
||||
- Added `PngToBmpConverter` library (new files, 858 lines)
|
||||
- Resolved 2 conflicts:
|
||||
- `Epub.cpp`: Discarded incoming JPG/PNG block (used old variable names), added PNG thumbnail support to mod's existing structure using `effectiveCoverImageHref` with case-insensitive checks. Fixed `generateCoverBmp()` PNG block to also use `effectiveCoverImageHref`. Added `.png` to `getCoverCandidates()`.
|
||||
- `ImageToFramebufferDecoder.cpp`: Took upstream `LOG_ERR` version over mod's `Serial.printf`
|
||||
|
||||
## Follow-up Items
|
||||
- Build and test PNG cover rendering on device
|
||||
- `mod/master` is now fully caught up with `master`
|
||||
66
chat-summaries/2026-02-16_22-00-summary.md
Normal file
66
chat-summaries/2026-02-16_22-00-summary.md
Normal file
@@ -0,0 +1,66 @@
|
||||
# Improve Home Screen
|
||||
|
||||
## Task Description
|
||||
Enhance the Lyra theme home screen with six improvements: empty-state placeholder, adaptive recent book cards, 2-line title wrapping with author, adjusted button positioning, optional clock display, and a manual Set Time activity.
|
||||
|
||||
## Changes Made
|
||||
|
||||
### i18n Strings (8 YAML files + auto-generated C++ files)
|
||||
- Added `STR_CHOOSE_SOMETHING`, `STR_HOME_SCREEN_CLOCK`, `STR_CLOCK_AMPM`, `STR_CLOCK_24H`, `STR_SET_TIME` to all 8 translation YAML files with localized text
|
||||
- Regenerated `lib/I18n/I18nKeys.h`, `I18nStrings.h`, `I18nStrings.cpp` via `gen_i18n.py`
|
||||
|
||||
### Settings: Clock Format (`CrossPointSettings.h`, `.cpp`, `SettingsList.h`)
|
||||
- Added `CLOCK_FORMAT` enum (`CLOCK_OFF`, `CLOCK_AMPM`, `CLOCK_24H`) and `homeScreenClock` member (default OFF)
|
||||
- Added persistence in `writeSettings()` and `loadFromFile()` (appended at end for backward compatibility)
|
||||
- Added "Home Screen Clock" setting under Display category in `SettingsList.h`
|
||||
|
||||
### Lyra Theme: Empty State Placeholder (`LyraTheme.cpp`)
|
||||
- When `recentBooks` is empty, draws centered "Choose something to read" text instead of blank area
|
||||
|
||||
### Lyra Theme: 1-Book Horizontal Layout (`LyraTheme.cpp`)
|
||||
- When 1 recent book: cover on the left (natural aspect ratio), title + author on the right
|
||||
- Title uses `UI_12_FONT_ID` with generous wrapping (up to 5 lines, no truncation unless very long)
|
||||
- Author in `UI_10_FONT_ID` below title with 4px gap
|
||||
|
||||
### Lyra Theme: Multi-Book Tile Layout (`LyraTheme.cpp`)
|
||||
- 2-3 books: tile-based layout with cover centered within tile (no stretching)
|
||||
- Cover bitmap rendering preserves aspect ratio: crops if wider than slot, centers if narrower
|
||||
- Title wraps to 2 lines with ellipsis, author in `SMALL_FONT_ID` below
|
||||
|
||||
### Lyra Theme: Selection Background Fix (`LyraTheme.cpp`)
|
||||
- Bottom section of selection highlight now uses full remaining height below cover
|
||||
- Prevents author text from clipping outside the selection area
|
||||
|
||||
### Lyra Theme: Shared Helpers (`LyraTheme.cpp`)
|
||||
- Extracted `wrapText` lambda for reusable word-wrap logic (parameterized by font, maxLines, maxWidth)
|
||||
- Extracted `renderCoverBitmap` lambda for aspect-ratio-preserving cover rendering
|
||||
|
||||
### Lyra Metrics (`LyraTheme.h`)
|
||||
- Increased `homeCoverTileHeight` from 287 to 310 to accommodate expanded text area
|
||||
- Menu buttons shift down automatically since they're positioned relative to this metric
|
||||
|
||||
### Home Screen Clock (`HomeActivity.cpp`)
|
||||
- Added clock rendering in the header area (top-left) after `drawHeader()`
|
||||
- Respects `homeScreenClock` setting (OFF / AM/PM / 24H)
|
||||
- Skips rendering if system time is unset (year <= 2000)
|
||||
|
||||
### Set Time Activity (NEW: `SetTimeActivity.h`, `SetTimeActivity.cpp`)
|
||||
- New sub-activity for manual time entry: displays HH:MM with field selection
|
||||
- Left/Right switches between hours and minutes, Up/Down adjusts values
|
||||
- Confirm saves via `settimeofday()`, Back discards
|
||||
- Wired into Settings > Display as an action item
|
||||
|
||||
### Settings Activity Wiring (`SettingsActivity.h`, `SettingsActivity.cpp`)
|
||||
- Added `SetTime` to `SettingAction` enum
|
||||
- Added include and switch case for `SetTimeActivity`
|
||||
- Added "Set Time" action to display settings category
|
||||
|
||||
## Build Verification
|
||||
- `pio run -e default` succeeded
|
||||
- RAM: 31.1% (101,772 / 327,680 bytes)
|
||||
- Flash: 99.5%
|
||||
|
||||
## Follow-up Items
|
||||
- Test on hardware: verify clock display, card layout with 0/1/2/3 books, Set Time activity
|
||||
- Fine-tune `homeCoverTileHeight` value if text area feels too tight or loose visually
|
||||
- Consider NTP auto-sync when WiFi is available (currently only during KOReader sync)
|
||||
63
chat-summaries/2026-02-16_upstream-sync-summary.md
Normal file
63
chat-summaries/2026-02-16_upstream-sync-summary.md
Normal file
@@ -0,0 +1,63 @@
|
||||
# Upstream Sync: `upstream/master` into `mod/master`
|
||||
|
||||
**Date:** 2026-02-16
|
||||
**Task:** Synchronize recent upstream changes into the mod fork while preserving all mod-specific features.
|
||||
|
||||
## Strategy
|
||||
|
||||
- **Cherry-pick** approach (not full merge) for granular control
|
||||
- 13 upstream commits cherry-picked across 3 phases; 4 skipped (mod already had enhanced implementations)
|
||||
- Working branch `mod/sync-upstream` used, then fast-forwarded into `mod/master`
|
||||
|
||||
## Phases & Commits
|
||||
|
||||
### Phase 1 — Low-risk (6 commits)
|
||||
| PR | Description | Notes |
|
||||
|----|-------------|-------|
|
||||
| #689 | Hyphenation optimization | Naming conflict resolved (mod's `OMIT_HYPH_*` guards preserved) |
|
||||
| #832 | Settings size auto-calc | Mod's `sleepScreenLetterboxFill` added to new `SettingsWriter` |
|
||||
| #840 | clang-format-fix shebang | Clean |
|
||||
| #917 | SCOPE.md dictionary docs | Clean |
|
||||
| #856 | Multiple author display | Clean |
|
||||
| #858 | Miniz compilation warning | Clean |
|
||||
|
||||
### Phase 2 — Major refactors (3 commits)
|
||||
| PR | Description | Notes |
|
||||
|----|-------------|-------|
|
||||
| #774 | Activity render() refactor | ~15 conflicts. Mod's 5 custom activities (Bookmarks, Dictionary) adapted to new `render(RenderLock&&)` pattern. Deadlock in `EpubReaderActivity.cpp` identified and fixed (redundant mutex around `enterNewActivity()`). |
|
||||
| #916 | RAII RenderLock | Clean cherry-pick + audit of mod code for manual mutex usage |
|
||||
| #728 | I18n system | ~15-20 conflicts. 16 new `StrId` keys added for mod strings. `DynamicEnum` font handling preserved. `SettingsList.h` and `SettingsActivity.cpp` adapted. |
|
||||
|
||||
### Phase 3 — Post-I18n fixes (4 commits)
|
||||
| PR | Description | Notes |
|
||||
|----|-------------|-------|
|
||||
| #884 | Empty button icon fix | Clean |
|
||||
| #906 | Webserver docs update | Clean |
|
||||
| #792 | Translators doc | Clean |
|
||||
| #796 | Battery icon alignment | Clean |
|
||||
|
||||
### Mod adaptation commits (3)
|
||||
- `mod: adapt mod activities to #774 render() pattern` — Systematic refactor of 5 activity pairs
|
||||
- `mod: convert remaining manual render locks to RAII RenderLock` — Audit & cleanup
|
||||
- `mod: remove duplicate I18n.h include in HomeActivity.cpp` — Cleanup
|
||||
|
||||
## Skipped Upstream Commits (4)
|
||||
- #676, #700 (image/cover improvements) — Mod already has enhanced pipeline
|
||||
- #668 (JPEG support) — Already in mod
|
||||
- #780 (cover fallback) — Already cherry-picked into mod
|
||||
|
||||
## Files Changed
|
||||
111 files changed, ~23,700 insertions, ~17,700 deletions across hyphenation tries, I18n system, activity refactors, and documentation.
|
||||
|
||||
## Key Decisions
|
||||
- **Image pipeline:** Kept mod's versions entirely; upstream's cover fixes were already ported
|
||||
- **Deadlock fix:** Removed redundant `xSemaphoreTake`/`xSemaphoreGive` around `enterNewActivity()` in `EpubReaderActivity.cpp` — the new `RenderLock` inside `enterNewActivity()` would deadlock with the outer manual lock
|
||||
- **I18n integration:** Added mod-specific `StrId` keys rather than keeping hardcoded strings
|
||||
|
||||
## Verification
|
||||
- All mod features (bookmarks, dictionary, letterbox fill, placeholder covers, table rendering, embedded images) verified for code-path integrity post-sync
|
||||
- No broken references or missing dependencies found
|
||||
|
||||
## Follow-up Items
|
||||
- Full PlatformIO build on hardware to confirm compilation
|
||||
- Runtime testing of all mod features on device
|
||||
38
chat-summaries/2026-02-17_02-33-summary.md
Normal file
38
chat-summaries/2026-02-17_02-33-summary.md
Normal file
@@ -0,0 +1,38 @@
|
||||
# Clock Persistence, Size Setting, and Timezone Support
|
||||
|
||||
## Task Description
|
||||
Implemented the plan from `clock_settings_and_timezone_fd0bf03f.plan.md` covering three features:
|
||||
1. Fix homeScreenClock setting not persisting across reboots
|
||||
2. Add clock size setting (Small/Medium/Large)
|
||||
3. Add timezone selection with North American presets and custom UTC offset
|
||||
|
||||
## Changes Made
|
||||
|
||||
### 1. Fix Persistence Bug
|
||||
- **`src/CrossPointSettings.cpp`**: Removed stale legacy `sleepScreenGradientDir` read (lines 270-271) from `loadFromFile()` that was causing a one-byte deserialization offset, corrupting `preferredPortrait`, `preferredLandscape`, and `homeScreenClock`.
|
||||
|
||||
### 2. Clock Size Setting
|
||||
- **`src/CrossPointSettings.h`**: Added `CLOCK_SIZE` enum (SMALL/MEDIUM/LARGE) and `uint8_t clockSize` field.
|
||||
- **`src/CrossPointSettings.cpp`**: Added `clockSize` to `writeSettings()` and `loadFromFile()` serialization.
|
||||
- **`src/SettingsList.h`**: Added clock size enum setting in Display category.
|
||||
- **`lib/I18n/I18nKeys.h`**: Added `STR_CLOCK_SIZE`, `STR_CLOCK_SIZE_SMALL`, `STR_CLOCK_SIZE_MEDIUM`, `STR_CLOCK_SIZE_LARGE`.
|
||||
- **All 8 YAML translation files**: Added clock size strings.
|
||||
- **`src/components/themes/BaseTheme.cpp`** and **`src/components/themes/lyra/LyraTheme.cpp`**: Updated `drawHeader()` to select font (SMALL_FONT_ID / UI_10_FONT_ID / UI_12_FONT_ID) based on `clockSize` setting.
|
||||
|
||||
### 3. Timezone Support
|
||||
- **`src/CrossPointSettings.h`**: Added `TIMEZONE` enum (UTC, Eastern, Central, Mountain, Pacific, Alaska, Hawaii, Custom), `uint8_t timezone` and `int8_t timezoneOffsetHours` fields, and `getTimezonePosixStr()` declaration.
|
||||
- **`src/CrossPointSettings.cpp`**: Added timezone/offset serialization, validation (-12 to +14), and `getTimezonePosixStr()` implementation returning POSIX TZ strings (including DST rules for NA timezones).
|
||||
- **`lib/I18n/I18nKeys.h`** + **all YAML files**: Added timezone strings and "Set UTC Offset" label.
|
||||
- **`src/SettingsList.h`**: Added timezone enum setting in Display category.
|
||||
- **`src/activities/settings/SetTimezoneOffsetActivity.h/.cpp`** (new files): UTC offset picker activity (-12 to +14), using same UI pattern as `SetTimeActivity`.
|
||||
- **`src/activities/settings/SettingsActivity.h`**: Added `SetTimezoneOffset` to `SettingAction` enum.
|
||||
- **`src/activities/settings/SettingsActivity.cpp`**: Added include, action entry, handler for SetTimezoneOffset, and `setenv`/`tzset` call after every settings save.
|
||||
- **`src/main.cpp`**: Apply saved timezone via `setenv`/`tzset` on boot after `SETTINGS.loadFromFile()`.
|
||||
- **`src/util/TimeSync.cpp`**: Apply timezone before starting NTP sync so time displays correctly.
|
||||
|
||||
## Build Status
|
||||
Firmware builds successfully (99.5% flash usage).
|
||||
|
||||
## Follow-up Items
|
||||
- Test on device: verify clock persistence, size changes, timezone selection, and custom UTC offset picker.
|
||||
- Timezone strings use English fallbacks for non-English languages.
|
||||
33
chat-summaries/2026-02-17_03-04-summary.md
Normal file
33
chat-summaries/2026-02-17_03-04-summary.md
Normal file
@@ -0,0 +1,33 @@
|
||||
# Move Clock Settings to Own Category
|
||||
|
||||
## Task Description
|
||||
Moved all clock-related settings into a dedicated "Clock" tab in the settings screen, renamed "Home Screen Clock" to "Clock" (since it's now shown globally), and made "Set UTC Offset" conditionally visible only when timezone is set to "Custom".
|
||||
|
||||
## Changes Made
|
||||
|
||||
### 1. Rename "Home Screen Clock" -> "Clock"
|
||||
- **`lib/I18n/I18nKeys.h`**: Renamed `STR_HOME_SCREEN_CLOCK` to `STR_CLOCK`.
|
||||
- **All 8 YAML translation files**: Updated key name and shortened labels (e.g., "Home Screen Clock" -> "Clock", "Hodiny na domovske obrazovce" -> "Hodiny").
|
||||
- **`src/CrossPointSettings.h`**: Renamed field `homeScreenClock` to `clockFormat`.
|
||||
- **`src/CrossPointSettings.cpp`**: Updated all references (`writeSettings`, `loadFromFile`).
|
||||
- **`src/SettingsList.h`**: Updated setting entry and JSON API key.
|
||||
- **`src/main.cpp`**, **`src/components/themes/BaseTheme.cpp`**, **`src/components/themes/lyra/LyraTheme.cpp`**: Updated all `SETTINGS.homeScreenClock` references to `SETTINGS.clockFormat`.
|
||||
|
||||
### 2. New "Clock" settings category
|
||||
- **`lib/I18n/I18nKeys.h`**: Added `STR_CAT_CLOCK`.
|
||||
- **All 8 YAML translation files**: Added localized "Clock" category label.
|
||||
- **`src/SettingsList.h`**: Changed category of Clock, Clock Size, and Timezone from `STR_CAT_DISPLAY` to `STR_CAT_CLOCK`.
|
||||
|
||||
### 3. Wire up in SettingsActivity
|
||||
- **`src/activities/settings/SettingsActivity.h`**: Added `clockSettings` vector, `rebuildClockActions()` helper, bumped `categoryCount` to 5.
|
||||
- **`src/activities/settings/SettingsActivity.cpp`**:
|
||||
- Added `STR_CAT_CLOCK` to `categoryNames` (index 1, between Display and Reader).
|
||||
- Added routing for `STR_CAT_CLOCK` in the category-sorting loop.
|
||||
- Implemented `rebuildClockActions()`: always adds "Set Time", conditionally adds "Set UTC Offset" only when `timezone == TZ_CUSTOM`. Called on `onEnter()` and after every `toggleCurrentSetting()`.
|
||||
- Updated category switch indices (Display=0, Clock=1, Reader=2, Controls=3, System=4).
|
||||
|
||||
## Build Status
|
||||
Firmware builds successfully.
|
||||
|
||||
## Follow-up Items
|
||||
- Test on device: verify the new Clock tab, setting visibility toggle, and tab bar layout with 5 tabs.
|
||||
50
chat-summaries/2026-02-17_clock-fix-ntp-sleep-summary.md
Normal file
50
chat-summaries/2026-02-17_clock-fix-ntp-sleep-summary.md
Normal file
@@ -0,0 +1,50 @@
|
||||
# Clock Bug Fix, NTP Auto-Sync, and Sleep Persistence
|
||||
|
||||
**Date**: 2026-02-17
|
||||
|
||||
## Task Description
|
||||
|
||||
Three clock-related improvements:
|
||||
1. Fix SetTimeActivity immediately dismissing when opened
|
||||
2. Add automatic NTP time sync on WiFi connection
|
||||
3. Verify time persistence across deep sleep modes
|
||||
|
||||
## Changes Made
|
||||
|
||||
### 1. Bug Fix: SetTimeActivity immediate dismiss (`src/activities/settings/SetTimeActivity.cpp`)
|
||||
- **Root cause**: `loop()` used `wasReleased()` for Back, Confirm, Left, and Right buttons. The parent `SettingsActivity` enters this subactivity on a `wasPressed()` event, so the button release from the original press immediately triggered the exit path.
|
||||
- **Fix**: Changed all four `wasReleased()` calls to `wasPressed()`, matching the pattern used by all other subactivities (e.g., `LanguageSelectActivity`).
|
||||
|
||||
### 2. Shared NTP Utility (`src/util/TimeSync.h`, `src/util/TimeSync.cpp`)
|
||||
- Created `TimeSync` namespace with three functions:
|
||||
- `startNtpSync()` -- non-blocking: configures and starts SNTP service
|
||||
- `waitForNtpSync(int timeoutMs = 5000)` -- blocking: starts SNTP and polls until sync completes or timeout
|
||||
- `stopNtpSync()` -- stops the SNTP service
|
||||
|
||||
### 3. NTP on WiFi Connection (`src/activities/network/WifiSelectionActivity.cpp`)
|
||||
- Added `#include "util/TimeSync.h"` and call to `TimeSync::startNtpSync()` in `checkConnectionStatus()` right after `WL_CONNECTED` is detected. This is non-blocking so it doesn't delay the UI flow.
|
||||
|
||||
### 4. KOReaderSync refactor (`src/activities/reader/KOReaderSyncActivity.cpp`)
|
||||
- Removed the local `syncTimeWithNTP()` anonymous namespace function and `#include <esp_sntp.h>`
|
||||
- Replaced both call sites with `TimeSync::waitForNtpSync()` (blocking, since KOReader needs accurate time for API requests)
|
||||
- Added `#include "util/TimeSync.h"`
|
||||
|
||||
### 5. Boot time debug log (`src/main.cpp`)
|
||||
- Added `#include <ctime>` and a debug log in `setup()` that prints the current RTC time on boot (or "not set" if epoch). This helps verify time persistence across deep sleep during testing.
|
||||
|
||||
## Sleep Persistence Notes
|
||||
|
||||
- ESP32-C3's default ESP-IDF config uses both RTC and high-resolution timers for timekeeping
|
||||
- The RTC timer continues during deep sleep, so `time()` / `gettimeofday()` return correct wall-clock time after wake (with drift from the internal ~150kHz RC oscillator)
|
||||
- Time does NOT survive a full power-on reset (RTC timer resets)
|
||||
- NTP auto-sync on WiFi connection handles drift correction
|
||||
|
||||
## Build Verification
|
||||
|
||||
- `pio run -e mod` -- SUCCESS (RAM: 31.0%, Flash: 78.8%)
|
||||
|
||||
## Follow-up Items
|
||||
|
||||
- Test on hardware: set time manually, sleep, wake, verify time in serial log
|
||||
- Test NTP: connect to WiFi from Settings, verify time updates automatically
|
||||
- Consider adding `TimeSync::stopNtpSync()` call when WiFi is disconnected (currently SNTP just stops getting responses, which is harmless)
|
||||
@@ -0,0 +1,47 @@
|
||||
# Clock UI: Symmetry, Auto-Update, and System-Wide Header Clock
|
||||
|
||||
**Date**: 2026-02-17
|
||||
|
||||
## Task Description
|
||||
|
||||
Three improvements to clock display:
|
||||
1. Make clock positioning symmetric with battery icon in headers
|
||||
2. Auto-update the clock without requiring a button press
|
||||
3. Show the clock everywhere the battery appears in system UI headers
|
||||
|
||||
## Changes Made
|
||||
|
||||
### 1. Moved clock rendering into `drawHeader` (BaseTheme + LyraTheme)
|
||||
|
||||
Previously the clock was drawn only in `HomeActivity::render()` with ad-hoc positioning (`contentSidePadding`, `topPadding`). Now it's rendered inside `drawHeader()` in both theme implementations, using the same positioning pattern as the battery:
|
||||
|
||||
- **Battery** (right): icon at `rect.x + rect.width - 12 - batteryWidth`, text at `rect.y + 5`
|
||||
- **Clock** (left): text at `rect.x + 12`, `rect.y + 5`
|
||||
|
||||
Both use `SMALL_FONT_ID`, so the font matches. The 12px margin from the edge is now symmetric on both sides.
|
||||
|
||||
**Files changed:**
|
||||
- `src/components/themes/BaseTheme.cpp` -- added clock block in `drawHeader()`, added `#include <ctime>` and `#include "CrossPointSettings.h"`
|
||||
- `src/components/themes/lyra/LyraTheme.cpp` -- same changes for Lyra theme's `drawHeader()`
|
||||
|
||||
### 2. Clock now appears on all header screens automatically
|
||||
|
||||
Since `drawHeader()` is called by: HomeActivity, MyLibraryActivity, RecentBooksActivity, SettingsActivity, LookedUpWordsActivity, DictionarySuggestionsActivity -- the clock now appears on all of these screens when enabled. No per-activity code needed.
|
||||
|
||||
### 3. Removed standalone clock code from HomeActivity
|
||||
|
||||
- `src/activities/home/HomeActivity.cpp` -- removed the 15-line clock rendering block that was separate from `drawHeader()`
|
||||
|
||||
### 4. Added auto-update (once per minute) on home screen
|
||||
|
||||
- `src/activities/home/HomeActivity.h` -- added `lastRenderedMinute` field to track the currently displayed minute
|
||||
- `src/activities/home/HomeActivity.cpp` -- added minute-change detection in `loop()` that calls `requestUpdate()` when the minute rolls over, triggering a screen refresh
|
||||
|
||||
## Build Verification
|
||||
|
||||
- `pio run -e mod` -- SUCCESS (RAM: 31.0%, Flash: 78.8%)
|
||||
|
||||
## Follow-up Items
|
||||
|
||||
- The auto-update only applies to HomeActivity. Other screens (Settings, Library, etc.) will show the current time when they render but won't auto-refresh purely for clock updates, which is appropriate for e-ink.
|
||||
- The DictionarySuggestionsActivity and LookedUpWordsActivity pass a non-zero `contentX` offset in their header rect, so the clock position adjusts correctly via `rect.x + 12`.
|
||||
24
chat-summaries/2026-02-17_home-highlight-fixes.md
Normal file
24
chat-summaries/2026-02-17_home-highlight-fixes.md
Normal file
@@ -0,0 +1,24 @@
|
||||
# Home Screen Book Card Highlight Fixes
|
||||
|
||||
## Task
|
||||
Fix visual issues with the selection highlight on home screen recent book cards:
|
||||
1. Bottom padding too tight against author text descenders
|
||||
2. Rounded corners not uniform across all four corners
|
||||
|
||||
## Changes Made
|
||||
|
||||
### LyraTheme.h
|
||||
- Increased `homeCoverTileHeight` from 310 to 318 to provide bottom padding below author text
|
||||
|
||||
### LyraTheme.cpp
|
||||
- Reduced `cornerRadius` from 6 to 4 to prevent radius clamping in the 8px-tall top strip (which was clamping `min(6, w/2, 4) = 4` while the bottom section used the full 6)
|
||||
|
||||
### GfxRenderer.cpp
|
||||
- No net changes. Two approaches were tried and reverted:
|
||||
1. **Dither origin** (global shift): misaligned the highlight's dot pattern with surrounding UI
|
||||
2. **Arc-relative dither** (per-corner relative coords): created visible seam artifacts at 3 of 4 corners where the arc's relative dither met the adjacent rectangle's absolute dither with inverted parity
|
||||
|
||||
## Key Decisions
|
||||
- **Bottom padding**: Originally tried shrinking the highlight (subtracting extra padding from bottom section height), but this cut off the author text. Correct approach: increase the tile height so the highlight naturally has more room.
|
||||
- **Corner radius**: Reducing from 6 to 4 ensures all corners (both the thin top strip and taller bottom section) use the same `maxRadius` through `fillRoundedRect`'s clamping formula.
|
||||
- **Dither approaches (both reverted)**: Two dither fixes were tried: (1) global dither origin shift misaligned with surrounding UI, (2) arc-relative dither created visible seam artifacts at corners where parity mismatched. The absolute dither pattern in `fillArc` is the least-bad option — the minor L/R asymmetry at radius 4 is much less noticeable than seam artifacts.
|
||||
37
chat-summaries/2026-02-17_long-press-confirm-toc.md
Normal file
37
chat-summaries/2026-02-17_long-press-confirm-toc.md
Normal file
@@ -0,0 +1,37 @@
|
||||
# Long-Press Confirm to Open Table of Contents
|
||||
|
||||
**Date**: 2026-02-17
|
||||
**Branch**: mod/improve-home-screen
|
||||
|
||||
## Task
|
||||
|
||||
Add long-press detection on the Confirm button while reading an EPUB to directly open the Table of Contents (chapter selection), bypassing the reader menu. Short press retains existing behavior (opens menu).
|
||||
|
||||
## Changes Made
|
||||
|
||||
### `src/activities/reader/EpubReaderActivity.h`
|
||||
- Added `bool ignoreNextConfirmRelease` member to suppress short-press after a long-press Confirm
|
||||
- Added `void openChapterSelection()` private method declaration
|
||||
|
||||
### `src/activities/reader/EpubReaderActivity.cpp`
|
||||
- Added `constexpr unsigned long longPressConfirmMs = 700` threshold constant
|
||||
- Extracted `openChapterSelection()` helper method from the duplicated `EpubReaderChapterSelectionActivity` construction code
|
||||
- Added long-press Confirm detection in `loop()` (before the existing short-press check): opens TOC directly if `epub->getTocItemsCount() > 0`
|
||||
- Refactored `onReaderMenuConfirm(SELECT_CHAPTER)` to use the new helper (was ~35 lines of inline construction)
|
||||
- Refactored `onReaderMenuConfirm(GO_TO_BOOKMARK)` fallback (no bookmarks + TOC available) to use the same helper
|
||||
- Reset `ignoreNextConfirmRelease` when `skipNextButtonCheck` clears, to avoid stale state across subactivity transitions
|
||||
|
||||
### `src/activities/reader/EpubReaderChapterSelectionActivity.h`
|
||||
- Added `bool ignoreNextConfirmRelease` member
|
||||
- Added `initialSkipRelease` constructor parameter (default `false`) to consume stale Confirm release when opened via long-press
|
||||
|
||||
### `src/activities/reader/EpubReaderChapterSelectionActivity.cpp`
|
||||
- Added guard in `loop()` to skip the first Confirm release when `ignoreNextConfirmRelease` is true
|
||||
|
||||
## Pattern Used
|
||||
|
||||
Follows the existing Back button short/long-press pattern: `isPressed() + getHeldTime() >= threshold` for long press, `wasReleased()` for short press, with `ignoreNextConfirmRelease` flag (same pattern as `EpubReaderMenuActivity`, `LookedUpWordsActivity`, and other activities).
|
||||
|
||||
## Follow-up Items
|
||||
|
||||
- None identified
|
||||
17
chat-summaries/2026-02-17_summary.md
Normal file
17
chat-summaries/2026-02-17_summary.md
Normal file
@@ -0,0 +1,17 @@
|
||||
# Cherry-pick upstream PR #939 — dangling pointer fix
|
||||
|
||||
## Task
|
||||
Cherry-pick commit `b47e1f6` from upstream PR [#939](https://github.com/crosspoint-reader/crosspoint-reader/pull/939) into `mod/master`.
|
||||
|
||||
## Changes
|
||||
- **File**: `src/activities/home/MyLibraryActivity.cpp` (lines 199-200)
|
||||
- **Fix**: Changed `folderName` from `auto` (deduced as `const char*` pointing to a temporary) to `std::string`, and called `.c_str()` at the point of use instead. This eliminates a dangling pointer caused by `.c_str()` on a temporary `std::string` from `basepath.substr(...)`.
|
||||
|
||||
## Method
|
||||
- Fetched PR ref via `git fetch upstream pull/939/head:pr-939`
|
||||
- Cherry-picked `b47e1f6` — applied cleanly with no conflicts
|
||||
- Build verified: SUCCESS (PlatformIO, 68s)
|
||||
- Cleaned up temporary `pr-939` branch ref
|
||||
|
||||
## Follow-up
|
||||
- None required. The commit preserves original authorship (Uri Tauber).
|
||||
42
chat-summaries/2026-02-18_fix-indexing-display-summary.md
Normal file
42
chat-summaries/2026-02-18_fix-indexing-display-summary.md
Normal file
@@ -0,0 +1,42 @@
|
||||
# Fix Indexing Display Issues
|
||||
|
||||
**Date:** 2026-02-18
|
||||
**Branch:** `mod/merge-upstream-pr-979`
|
||||
|
||||
## Task
|
||||
|
||||
Fixed three issues with the indexing display implementation that was added as part of merging PR #979 (silent pre-indexing for next chapter):
|
||||
|
||||
1. **Restore original popup for direct chapter jumps** — The conditional logic that showed a full-screen "Indexing..." message when a status bar display mode was selected was removed. Direct chapter jumps now always show the original small popup overlay, regardless of the Indexing Display setting.
|
||||
|
||||
2. **Clear status bar indicator after silent indexing** — Added `preIndexedNextSpine` tracking member to prevent the `silentIndexingActive` flag from being re-set on re-renders after indexing completes. Changed `silentIndexNextChapterIfNeeded` to return `bool` and added `requestUpdate()` call to trigger a clean re-render that clears the indicator.
|
||||
|
||||
3. **Handle single-page chapters** — Updated the pre-indexing condition to trigger on the sole page of a 1-page chapter (not just the penultimate page of multi-page chapters).
|
||||
|
||||
## Files Changed
|
||||
|
||||
- `src/activities/reader/EpubReaderActivity.h` — Added `preIndexedNextSpine` member, changed `silentIndexNextChapterIfNeeded` return type to `bool`
|
||||
- `src/activities/reader/EpubReaderActivity.cpp` — All three fixes applied
|
||||
|
||||
## Build
|
||||
|
||||
PlatformIO build succeeded (RAM: 31.1%, Flash: 99.6%).
|
||||
|
||||
## Follow-up fix: False "Indexing" indicator + image flash on e-ink
|
||||
|
||||
Two related issues: (1) paging backwards to a penultimate page of an already-indexed chapter showed "Indexing" in the status bar, and (2) the `requestUpdate()` that cleared the indicator caused images to flash on e-ink.
|
||||
|
||||
Root cause: `silentIndexingActive` was set optimistically based on page position alone, before checking whether the next chapter's cache actually exists. The subsequent `requestUpdate()` to clear the indicator triggered a full re-render causing image artifacts.
|
||||
|
||||
Fix — replaced optimistic flag with a pre-check:
|
||||
- Before rendering, probes the next chapter's section file via `Section::loadSectionFile`. If cached, sets `preIndexedNextSpine` and leaves `silentIndexingActive = false`. Only sets `true` when indexing is genuinely needed.
|
||||
- Removed `requestUpdate()` entirely — the indicator clears naturally on the next page turn.
|
||||
- Added early-out in `silentIndexNextChapterIfNeeded` for `preIndexedNextSpine` match to avoid redundant Section construction.
|
||||
|
||||
The pre-check cost (one `loadSectionFile` call) only happens once per chapter due to `preIndexedNextSpine` caching.
|
||||
|
||||
Silent indexing is only performed on text-only penultimate pages (`!p->hasImages()`). On image pages, silent indexing is skipped entirely — the normal popup handles indexing on the next chapter transition. This avoids conflicts with the grayscale rendering pipeline (`displayWindow` after `displayGrayBuffer` triggers `grayscaleRevert`, causing image inversion/ghosting).
|
||||
|
||||
For text-only pages: after `renderContents` returns, the indicator is cleared via `displayWindow(FAST_REFRESH)` on just the status bar strip. This is safe because text-only pages use simple BW rendering without the grayscale pipeline.
|
||||
|
||||
Build succeeded (RAM: 31.1%, Flash: 99.6%).
|
||||
39
chat-summaries/2026-02-18_merge-pr979-silent-indexing.md
Normal file
39
chat-summaries/2026-02-18_merge-pr979-silent-indexing.md
Normal file
@@ -0,0 +1,39 @@
|
||||
# Merge PR #979: Silent Pre-Indexing + Indexing Display Setting
|
||||
|
||||
**Date:** 2026-02-18
|
||||
**Branch:** `mod/merge-upstream-pr-979`
|
||||
|
||||
## Task Description
|
||||
|
||||
Merged upstream PR #979 (silent pre-indexing for the next chapter) and implemented both "Possible Improvements" from the PR:
|
||||
1. A user setting to choose between Popup, Status Bar Text, or Status Bar Icon for indexing feedback.
|
||||
2. Status bar indicator rendering during silent pre-indexing.
|
||||
|
||||
## Changes Made
|
||||
|
||||
### PR #979 Cherry-Pick (2 files)
|
||||
- **`src/activities/reader/EpubReaderActivity.h`** -- Added `silentIndexNextChapterIfNeeded()` method declaration and `silentIndexingActive` flag.
|
||||
- **`src/activities/reader/EpubReaderActivity.cpp`** -- Moved viewport dimension calculations before the `!section` block. Added `silentIndexNextChapterIfNeeded()` implementation that pre-indexes the next chapter when the penultimate page is displayed.
|
||||
|
||||
### New "Indexing Display" Setting (5 files)
|
||||
- **`src/CrossPointSettings.h`** -- Added `INDEXING_DISPLAY` enum (POPUP, STATUS_TEXT, STATUS_ICON) and `indexingDisplay` field.
|
||||
- **`src/CrossPointSettings.cpp`** -- Added persistence (write/read) for the new setting at the end of the settings chain.
|
||||
- **`src/SettingsList.h`** -- Registered the new Enum setting in the Display category.
|
||||
- **`lib/I18n/I18nKeys.h`** -- Added 4 new string IDs: `STR_INDEXING_DISPLAY`, `STR_INDEXING_POPUP`, `STR_INDEXING_STATUS_TEXT`, `STR_INDEXING_STATUS_ICON`.
|
||||
- **`lib/I18n/translations/*.yaml`** (8 files) -- Added translations for all 8 languages.
|
||||
|
||||
### Status Bar Indicator Implementation
|
||||
- **`EpubReaderActivity.cpp`** -- Conditional popup logic: only shows popup when setting is POPUP; for STATUS_TEXT/STATUS_ICON, shows centered "Indexing..." text on blank screen during non-silent indexing. For silent pre-indexing, sets `silentIndexingActive` flag before rendering so `renderStatusBar()` draws the indicator (text or 8x8 hourglass icon) in the status bar.
|
||||
- Defined an 8x8 1-bit hourglass bitmap icon (`kIndexingIcon`) for the icon mode.
|
||||
|
||||
## Build Verification
|
||||
|
||||
- PlatformIO build: SUCCESS
|
||||
- RAM: 31.1% (101,820 / 327,680 bytes)
|
||||
- Flash: 99.6% (6,525,028 / 6,553,600 bytes)
|
||||
|
||||
## Follow-Up Items
|
||||
|
||||
- Test on device: verify silent indexing triggers on penultimate page, verify all three display modes work correctly.
|
||||
- The status bar indicator shows optimistically on penultimate pages even if the next chapter is already cached (false positive clears immediately after `loadSectionFile` check).
|
||||
- Flash usage is at 99.6% -- monitor for future additions.
|
||||
59
chat-summaries/2026-02-18_merge-upstream-prs.md
Normal file
59
chat-summaries/2026-02-18_merge-upstream-prs.md
Normal file
@@ -0,0 +1,59 @@
|
||||
# Merge Upstream PRs #965, #939, #852, #972, #971, #977, #975
|
||||
|
||||
**Date:** 2026-02-18
|
||||
**Branch:** mod/merge-upstream-1
|
||||
|
||||
## Task
|
||||
|
||||
Port 7 upstream PRs from crosspoint-reader/crosspoint-reader into the mod branch.
|
||||
|
||||
## Status per PR
|
||||
|
||||
| PR | Description | Result |
|
||||
|---|---|---|
|
||||
| #939 | Fix dangling pointer in MyLibraryActivity | Already ported, no changes needed |
|
||||
| #852 | HalPowerManager idle CPU freq scaling | Completed partial port (Lock RAII, WiFi check, skipLoopDelay, render locks) |
|
||||
| #965 | Fix paragraph formatting inside list items | Fully ported |
|
||||
| #972 | Micro-optimizations to eliminate value copies | Ported (fontMap move, getDataFromBook const ref) |
|
||||
| #971 | Remove redundant hasPrintableChars pass | Fully ported |
|
||||
| #977 | Skip unsupported image formats during parsing | Fully ported |
|
||||
| #975 | Fix UITheme memory leak on theme reload | Fully ported |
|
||||
|
||||
## Changes Made
|
||||
|
||||
### PR #852 (partial port completion)
|
||||
- `lib/hal/HalPowerManager.h` -- Added `LockMode` enum, `currentLockMode`/`modeMutex` members, nested `Lock` RAII class (non-copyable/non-movable), `extern powerManager` declaration
|
||||
- `lib/hal/HalPowerManager.cpp` -- Added mutex init in `begin()`, WiFi.getMode() check in `setPowerSaving()`, Lock constructor/destructor, LockMode guard
|
||||
- `src/activities/settings/ClearCacheActivity.h` -- Added `skipLoopDelay()` override
|
||||
- `src/activities/settings/OtaUpdateActivity.h` -- Added `skipLoopDelay()` override
|
||||
- `src/activities/Activity.cpp` -- Added `HalPowerManager::Lock` in `renderTaskLoop()`
|
||||
- `src/activities/ActivityWithSubactivity.cpp` -- Added `HalPowerManager::Lock` in `renderTaskLoop()`
|
||||
|
||||
### PR #965
|
||||
- `lib/Epub/Epub/parsers/ChapterHtmlSlimParser.h` -- Added `listItemUntilDepth` member
|
||||
- `lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp` -- Modified `startElement` to handle `<p>` inside `<li>` without line break; added depth reset in `endElement`
|
||||
|
||||
### PR #972
|
||||
- `lib/GfxRenderer/GfxRenderer.cpp` -- `std::move(font)` in `insertFont`
|
||||
- `src/RecentBooksStore.h` / `.cpp` -- `getDataFromBook` now takes `const std::string&`
|
||||
|
||||
### PR #971
|
||||
- `lib/EpdFont/EpdFont.h` / `.cpp` -- Removed `hasPrintableChars` method
|
||||
- `lib/EpdFont/EpdFontFamily.h` / `.cpp` -- Removed `hasPrintableChars` method
|
||||
- `lib/GfxRenderer/GfxRenderer.cpp` -- Removed 3 early-return guards calling `hasPrintableChars`
|
||||
|
||||
### PR #977
|
||||
- `lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp` -- Added `PARSE_BUFFER_SIZE` constant, `isFormatSupported` guard before image extraction, timing instrumentation
|
||||
|
||||
### PR #975
|
||||
- `src/components/UITheme.h` -- Changed `currentTheme` to `std::unique_ptr<const BaseTheme>`
|
||||
- `src/components/UITheme.cpp` -- Changed allocations to `std::make_unique`
|
||||
|
||||
## Build Result
|
||||
|
||||
Build succeeded: RAM 31.1%, Flash 99.5%
|
||||
|
||||
## Follow-up Items
|
||||
|
||||
- PR #972 LyraTheme loop variable change not applicable (our code uses index-based loops)
|
||||
- Test on device to verify all changes work as expected
|
||||
49
chat-summaries/2026-02-19_10-46-summary.md
Normal file
49
chat-summaries/2026-02-19_10-46-summary.md
Normal file
@@ -0,0 +1,49 @@
|
||||
# Sync mod/master with upstream 1.1.0-RC
|
||||
|
||||
## Task Description
|
||||
Integrated 19 missing upstream PRs from `master` (1.1.0-RC) into `mod/master` via phased cherry-picking, preserving all existing mod enhancements.
|
||||
|
||||
## Changes Made
|
||||
|
||||
### Branch Setup
|
||||
- Created safety branch `mod/backup-pre-sync` from `mod/master`
|
||||
- Created integration branch `mod/sync-upstream-1.1.0` (19 cherry-picked commits + 1 cleanup)
|
||||
|
||||
### Phase 1: Low-Risk PRs (8 PRs)
|
||||
- **#646** Ukrainian hyphenation support (resolved conflict: merged with mod's `OMIT_HYPH_*` conditional compilation guards)
|
||||
- **#732** Lyra screens (resolved 5 conflicts: preserved mod's clock, 3-cover layout, cover rendering; added upstream's drawSubHeader, popup, keyboard/text field methods)
|
||||
- **#725** Lyra Icons (resolved conflicts: added icon infrastructure, iconForName(), Lyra icon assets; removed stale Lyra3CoversTheme.cpp)
|
||||
- **#768** Tweak Lyra popup UI (resolved conflicts: preserved mod's epub loading logic, added upstream popup constants)
|
||||
- **#880** Flash objects listing script (clean)
|
||||
- **#897** Keyboard font size increase (clean)
|
||||
- **#927** Translators list update (clean)
|
||||
- **#935** Missing up/down button labels (resolved conflicts: kept GUI.drawList() over old manual rendering)
|
||||
|
||||
### Phase 2: Medium-Risk PRs (8 PRs - all applied cleanly)
|
||||
- **#783** KOSync repositioning fix
|
||||
- **#923** Bresenham line drawing
|
||||
- **#933** Font map lookup performance
|
||||
- **#944** 4-bit BMP support
|
||||
- **#952** Skip large CSS files to prevent crashes
|
||||
- **#963** Word width and space calculation fix
|
||||
- **#964** Scale cover images up
|
||||
- **#970** Fix prev-page teleport to end of book
|
||||
|
||||
### Phase 3: Large PR
|
||||
- **#831** Compressed fonts (30.7% flash reduction, 73 files). Resolved 2 conflicts: merged font decompressor init in main.cpp, merged clearFontCache with mod's silent indexing in EpubReaderActivity.
|
||||
|
||||
### Phase 4: Overlap Assessment
|
||||
- **#556** (JPEG/PNG image support): Confirmed mod already has complete coverage. All converter files identical. No action needed.
|
||||
- **#980** (Basic table support): Confirmed mod's column-aligned table rendering is a strict superset. No action needed.
|
||||
|
||||
### Post-Sync
|
||||
- Removed stale `Lyra3CoversTheme.h` (3-cover support merged into LyraTheme)
|
||||
- Fixed `UITheme.cpp` to use `LyraTheme` for LYRA_3_COVERS variant
|
||||
- Updated `open-x4-sdk` submodule to `91e7e2b` (drawImageTransparent support for Lyra Icons)
|
||||
- Ran clang-format on all source files
|
||||
- Build verified: RAM 31.4%, Flash 57.0%
|
||||
|
||||
## Follow-Up Items
|
||||
- Merge `mod/sync-upstream-1.1.0` into `mod/master` when ready
|
||||
- On-device smoke testing (book loading, images, tables, bookmarks, dictionary, sleep screen, clock, home screen)
|
||||
- Safety branch `mod/backup-pre-sync` available for rollback if needed
|
||||
33
chat-summaries/2026-02-19_21-30-summary.md
Normal file
33
chat-summaries/2026-02-19_21-30-summary.md
Normal file
@@ -0,0 +1,33 @@
|
||||
# Port Upstream PRs #997, #1003, #1005, #1010
|
||||
|
||||
## Task
|
||||
Cherry-pick / port four upstream PRs from crosspoint-reader/crosspoint-reader into the mod fork.
|
||||
|
||||
## Changes Made
|
||||
|
||||
### PR #997 -- Already Ported (no changes)
|
||||
Glyph null-safety in `getSpaceWidth`/`getTextAdvanceX` was already present via commit `c1b8e53`.
|
||||
|
||||
### PR #1010 -- Fix Dangling Pointer
|
||||
- **src/main.cpp**: `onGoToReader()` now copies the `initialEpubPath` string before calling `exitActivity()`, preventing a dangling reference when the owning activity is destroyed.
|
||||
|
||||
### PR #1005 -- Use HalPowerManager for Battery Percentage
|
||||
- **lib/hal/HalPowerManager.h**: Changed `getBatteryPercentage()` return type from `int` to `uint16_t`.
|
||||
- **lib/hal/HalPowerManager.cpp**: Same return type change.
|
||||
- **src/Battery.h**: Emptied contents (was `static BatteryMonitor battery(BAT_GPIO0)`).
|
||||
- **src/main.cpp**: Removed `#include "Battery.h"`.
|
||||
- **src/activities/home/HomeActivity.cpp**: Removed `#include "Battery.h"`.
|
||||
- **src/components/themes/BaseTheme.cpp**: Replaced `Battery.h` include with `HalPowerManager.h`, replaced `battery.readPercentage()` with `powerManager.getBatteryPercentage()` (2 occurrences).
|
||||
- **src/components/themes/lyra/LyraTheme.cpp**: Same replacements (2 occurrences).
|
||||
|
||||
### PR #1003 -- Render Image Placeholders While Waiting for Decode
|
||||
- **lib/Epub/Epub/blocks/ImageBlock.h/.cpp**: Added `isCached()` method that checks if the `.pxc` cache file exists.
|
||||
- **lib/Epub/Epub/Page.h/.cpp**: Added `PageImage::isCached()`, `PageImage::renderPlaceholder()`, `Page::renderTextOnly()`, `Page::countUncachedImages()`, `Page::renderImagePlaceholders()`. Added `#include <GfxRenderer.h>` to Page.h.
|
||||
- **src/activities/reader/EpubReaderActivity.cpp**: Modified `renderContents()` to check for uncached images and display text + placeholder rectangles immediately (Phase 1 with HALF_REFRESH), then proceed with full image decode and display (Phase 2 with fast refresh). Existing mod-specific double FAST_REFRESH logic for anti-aliased image pages is preserved for the cached-image path.
|
||||
|
||||
## Build Result
|
||||
SUCCESS -- RAM: 31.5%, Flash: 70.4%. No linter errors.
|
||||
|
||||
## Follow-up Items
|
||||
- PR #1003 is still open upstream (not merged); may need to rebase if upstream changes before merge.
|
||||
- The phased rendering for uncached images skips the mod's double FAST_REFRESH technique (relies on Phase 1's HALF_REFRESH instead). If grayscale quality on first-decode image pages is suboptimal, this could be revisited.
|
||||
33
chat-summaries/2026-02-19_port-pr978-font-perf.md
Normal file
33
chat-summaries/2026-02-19_port-pr978-font-perf.md
Normal file
@@ -0,0 +1,33 @@
|
||||
# Port Upstream PR #978: Improve Font Drawing Performance
|
||||
|
||||
**Date:** 2026-02-19
|
||||
**Branch:** `mod/more-upstream-patches-for-1.1.0`
|
||||
**Commit:** `3a06418`
|
||||
|
||||
## Task
|
||||
|
||||
Port upstream PR #978 (perf: Improve font drawing performance) which optimizes glyph rendering by 15-23% on device.
|
||||
|
||||
## Changes Made
|
||||
|
||||
### Cherry-picked from upstream (commit `07d715e`)
|
||||
|
||||
- **`lib/GfxRenderer/GfxRenderer.cpp`**: Introduced `TextRotation` enum and `renderCharImpl<TextRotation>` template function that consolidates normal and 90-CW-rotated rendering paths. Hoists the `is2Bit` conditional above pixel loops (eliminating per-pixel branch). Uses `if constexpr` for compile-time rotation path selection. Fixes operator precedence bug in `bmpVal` calculation.
|
||||
- **`lib/GfxRenderer/GfxRenderer.h`**: Changed `renderChar` signature (`const int* y` -> `int* y`). Moved `getGlyphBitmap` from private to public (needed by the free-function template).
|
||||
|
||||
### Mod-specific extension
|
||||
|
||||
- Extended `TextRotation` enum with `Rotated90CCW` and refactored `drawTextRotated90CCW` to delegate to the template. This fixed two bugs in the mod's CCW code:
|
||||
1. Operator precedence bug in `bmpVal`: `3 - (byte >> bit_index) & 0x3` -> `3 - ((byte >> bit_index) & 0x3)`
|
||||
2. Missing compressed font support: was using raw `bitmap[offset]` instead of `getGlyphBitmap()`
|
||||
|
||||
## Recovery of Discarded Changes
|
||||
|
||||
During plan-mode dry-run cherry-pick reset, `git checkout -- .` inadvertently discarded unstaged working tree changes in 12 files (from PRs #1005, #1010, #1003). These were recovered by:
|
||||
- Cherry-picking upstream commits `cabbfcf` (#1005) and `63b2643` (#1010) with conflict resolution (kept mod's `CrossPointSettings.h` include)
|
||||
- Fetching unmerged PR #1003 branch and manually applying the diff (ImageBlock::isCached, Page placeholder rendering, EpubReaderActivity phased rendering)
|
||||
- Committed as `18be265`
|
||||
|
||||
## Build
|
||||
|
||||
PlatformIO build succeeded. RAM: 31.5%, Flash: 70.4%.
|
||||
@@ -0,0 +1,37 @@
|
||||
# Port Upstream 1.1.0-RC Fixes
|
||||
|
||||
**Date:** 2026-02-19
|
||||
**Task:** Port 3 commits from upstream crosspoint-reader PR #992 (1.1.0-rc branch) into mod/master.
|
||||
|
||||
## Commits Ported
|
||||
|
||||
### 1. chore: Bump version to 1.1.0 (402e887) — Skipped
|
||||
- mod/master already has `version = 1.1.1-rc`, which is ahead of upstream's 1.1.0.
|
||||
|
||||
### 2. fix: Crash on unsupported bold/italic glyphs (3e2c518)
|
||||
- **File:** `lib/GfxRenderer/GfxRenderer.cpp`
|
||||
- Added null-safety checks to `getSpaceWidth()` and `getTextAdvanceX()`.
|
||||
- `getSpaceWidth()`: returns 0 if the space glyph is missing in the styled font variant.
|
||||
- `getTextAdvanceX()`: falls back to `REPLACEMENT_GLYPH` (U+FFFD) if a glyph is missing, and treats as zero-width if the replacement is also unavailable.
|
||||
- Prevents a RISC-V Load access fault (nullptr dereference) when indexing chapters with characters unsupported by bold/italic font variants.
|
||||
|
||||
### 3. fix: Increase PNGdec buffer for wide images (b8e743e)
|
||||
- **Files:** `platformio.ini`, `lib/Epub/Epub/converters/PngToFramebufferConverter.cpp`
|
||||
- Bumped `PNG_MAX_BUFFERED_PIXELS` from 6402 to 16416 (supports up to 2048px wide RGBA images).
|
||||
- Added `bytesPerPixelFromType()` and `requiredPngInternalBufferBytes()` helper functions.
|
||||
- Added a pre-decode safety check that aborts with a log error if the PNG scanline buffer would overflow PNGdec's internal buffer.
|
||||
|
||||
## Verification
|
||||
- Build (`pio run -e default`) succeeded cleanly with no errors or warnings.
|
||||
- RAM: 31.5%, Flash: 70.4%.
|
||||
|
||||
## 4. CSS-aware image sizing (PR #1002, commit c8ba4fe)
|
||||
- **Files:** `lib/Epub/Epub/css/CssStyle.h`, `lib/Epub/Epub/css/CssParser.cpp`, `lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp`
|
||||
- Added `imageHeight` field to `CssPropertyFlags` and `CssStyle` (our existing `width` field maps to upstream's `imageWidth`).
|
||||
- Added CSS `height` property parsing into `imageHeight`.
|
||||
- Added `imageHeight` and `width` to cache serialization; bumped `CSS_CACHE_VERSION` 2->3.
|
||||
- Replaced viewport-fit-only image scaling in `ChapterHtmlSlimParser` with CSS-aware sizing: resolves CSS height/width (including inline styles), preserves aspect ratio, clamps to viewport, includes divide-by-zero guards.
|
||||
- `platformio.ini` changes excluded from commit per user request (PNG buffer bump was already committed separately).
|
||||
|
||||
## Follow-up
|
||||
- None required. Changes are straightforward upstream ports.
|
||||
@@ -0,0 +1,34 @@
|
||||
# Sleep Cover: Double FAST_REFRESH for Dithered Letterbox
|
||||
|
||||
**Date:** 2026-02-19
|
||||
|
||||
## Task
|
||||
|
||||
Apply the "double FAST_REFRESH" technique (proven for inline EPUB images via PR #556) to sleep cover rendering when dithered letterboxing is enabled. This replaces the corruption-prone HALF_REFRESH with two FAST_REFRESH passes through a white intermediate state.
|
||||
|
||||
Also reverted the hash-based block dithering workaround back to standard Bayer dithering for all gray ranges, confirming the root cause was HALF_REFRESH rather than the dithering pattern.
|
||||
|
||||
## Changes Made
|
||||
|
||||
### `src/activities/boot_sleep/SleepActivity.cpp`
|
||||
|
||||
- **Added `USE_SLEEP_DOUBLE_FAST_REFRESH` define** (set to 1): compile-time toggle for easy A/B testing.
|
||||
- **Removed `bayerCrossesBwBoundary()` and `hashBlockDither()`**: These were the HALF_REFRESH crosstalk workaround (2x2 hash blocks for gray 171-254). Removed entirely since double FAST_REFRESH addresses the root cause.
|
||||
- **Simplified `drawLetterboxFill()`**: Dithered mode now always uses `quantizeBayerDither()` for all gray ranges -- same algorithm used for non-boundary grays. No more hash/Bayer branching.
|
||||
- **Modified `renderBitmapSleepScreen()` BW pass**: When dithered letterbox is active and `USE_SLEEP_DOUBLE_FAST_REFRESH` is enabled:
|
||||
1. Clear screen to white + FAST_REFRESH (establishes clean baseline)
|
||||
2. Render letterbox fill + cover + FAST_REFRESH (shows actual content)
|
||||
- **Letterbox fill included in all greyscale passes**: Initially skipped to avoid scan coupling, but re-enabled after testing confirmed the double FAST_REFRESH baseline prevents corruption. This ensures the letterbox color matches the cover edge after the greyscale LUT is applied.
|
||||
- **Standard HALF_REFRESH path preserved** for letterbox=none, letterbox=solid, or when define is disabled.
|
||||
|
||||
## Build
|
||||
|
||||
Successfully compiled with `pio run` (0 errors, 0 warnings).
|
||||
|
||||
## Testing
|
||||
|
||||
Confirmed working on-device -- dithered letterbox renders cleanly without corruption, and letterbox color matches the cover edge including after greyscale layer application.
|
||||
|
||||
## Commit
|
||||
|
||||
`0fda903` on branch `mod/sleep-screen-tweaks-on-1.1.0-rc-double-fast`
|
||||
36
chat-summaries/2026-02-20_15-30-port-upstream-prs.md
Normal file
36
chat-summaries/2026-02-20_15-30-port-upstream-prs.md
Normal file
@@ -0,0 +1,36 @@
|
||||
# Port Upstream PRs #1038, #1045, #1037, #1019
|
||||
|
||||
## Task
|
||||
|
||||
Manually port 4 open upstream PRs into the `mod/sync-upstream-PRs` branch, pull in Romanian translations from `upstream/master`, and create a MERGED.md tracking document.
|
||||
|
||||
## Changes Made
|
||||
|
||||
### PR #1038 (partial -- incremental fixes)
|
||||
- `lib/Epub/Epub/ParsedText.cpp`: Added `.erase()` calls in `layoutAndExtractLines` to remove consumed words after line extraction (fixes redundant early flush bug). Also fixed `wordContinues` flag handling in `hyphenateWordAtIndex` to preserve prefix's attach-to-previous flag.
|
||||
|
||||
### PR #1045 (direct string changes)
|
||||
- Updated `STR_FORGET_BUTTON` in all 9 translation yaml files to shorter labels.
|
||||
- Pulled `romanian.yaml` from `upstream/master` (merged upstream PR #987).
|
||||
|
||||
### PR #1037 (4-file manual port)
|
||||
- `lib/Utf8/Utf8.h`: Added `utf8IsCombiningMark()` utility function.
|
||||
- `lib/EpdFont/EpdFont.cpp`: Added combining mark positioning in `getTextBounds`.
|
||||
- `lib/Epub/Epub/hyphenation/HyphenationCommon.cpp`: Added NFC-like precomposition for common Western European diacritics in `collectCodepoints()`.
|
||||
- `lib/GfxRenderer/GfxRenderer.cpp`: Added combining mark rendering in `drawText`, `drawTextRotated90CW`, `drawTextRotated90CCW` (mod-only), and `getTextAdvanceX`. Also wrapped cursor advance in `renderCharImpl` to skip combining marks.
|
||||
|
||||
### PR #1019 (1-file manual port)
|
||||
- `src/activities/home/MyLibraryActivity.cpp`: Added `getFileExtension()` helper and updated `drawList` call to show file extensions in the File Browser.
|
||||
|
||||
### Tracking
|
||||
- `mod/prs/MERGED.md`: Created tracking document with details for all 4 PRs.
|
||||
|
||||
## Build Verification
|
||||
|
||||
`pio run -e mod` succeeded (24.49s, Flash: 57.2%, RAM: 31.4%).
|
||||
|
||||
## Follow-up Items
|
||||
|
||||
- All 4 upstream PRs are still open; monitor for any review feedback or changes before they merge.
|
||||
- Romanian yaml is missing mod-specific string translations (using English fallbacks).
|
||||
- PR #1019 open question: how file extensions look on long filenames.
|
||||
23
chat-summaries/2026-02-20_16-52-summary.md
Normal file
23
chat-summaries/2026-02-20_16-52-summary.md
Normal file
@@ -0,0 +1,23 @@
|
||||
# Expandable Selected Row for Long Filenames
|
||||
|
||||
## Task
|
||||
|
||||
Implement a mod enhancement to PR #1019 (Display file extensions in File Browser). When the selected row's filename is too long, expand that row to 2 lines with character-level text wrapping. The file extension moves to the bottom-right of the expanded area. Non-selected rows retain single-line truncation behavior.
|
||||
|
||||
## Changes Made
|
||||
|
||||
### `src/components/themes/BaseTheme.cpp`
|
||||
- Added `wrapTextToLines` static helper in the anonymous namespace: character-level UTF-8-aware text wrapping with "..." truncation on the final line.
|
||||
- Modified `drawList`: pre-loop expansion detection for the selected item, pagination adjustment (`pageItems - 1`), expanded selection highlight (`2 * rowHeight`), 2-line title rendering via `wrapTextToLines`, extension repositioned to bottom-right of expanded area, `yPos` tracking for subsequent items.
|
||||
|
||||
### `src/components/themes/lyra/LyraTheme.cpp`
|
||||
- Added identical `wrapTextToLines` helper.
|
||||
- Modified `drawList` with analogous expansion logic, adapted for Lyra-specific styling (rounded-rect selection highlight, icon support, scroll bar-aware content width, preliminary text width computation for expansion check).
|
||||
|
||||
### `mod/prs/MERGED.md`
|
||||
- Updated PR #1019 section to document the mod enhancement, files modified, and design decisions.
|
||||
|
||||
## Follow-up Items
|
||||
|
||||
- Visual testing on device to verify text positioning and edge cases (very long single-word filenames, last item on page expanding, single-item lists).
|
||||
- The page-up/page-down navigation in `MyLibraryActivity::loop()` uses the base `pageItems` from `getNumberOfItemsPerPage` which doesn't account for expansion. This causes a minor cosmetic mismatch (page jump size differs by 1 from visual page when expansion is active) but is functionally correct.
|
||||
29
chat-summaries/2026-02-20_17-32-summary.md
Normal file
29
chat-summaries/2026-02-20_17-32-summary.md
Normal file
@@ -0,0 +1,29 @@
|
||||
# Fix Text Wrapping and Spacing for Expanded Selected Row
|
||||
|
||||
## Task
|
||||
|
||||
Improve the expandable selected row feature (from PR #1019 enhancement). Two issues were identified from device testing:
|
||||
1. Character-level wrapping broke words mid-character (e.g., "Preside / nt"), resulting in unnatural line breaks.
|
||||
2. Poor vertical spacing -- text lines clustered near the top of the expanded highlight area with large empty space at the bottom.
|
||||
|
||||
## Changes Made
|
||||
|
||||
### `src/components/themes/BaseTheme.cpp`
|
||||
- Rewrote `wrapTextToLines` with 3-tier break logic:
|
||||
1. Preferred delimiters: " -- ", " - ", en-dash, em-dash (breaks at last occurrence to maximize line 1)
|
||||
2. Word boundaries: last space or hyphen that fits
|
||||
3. Character-level fallback for long unbroken tokens
|
||||
- Extracted `truncateWithEllipsis` helper to reduce duplication
|
||||
- Fixed expanded row rendering: text lines vertically centered in 2x row height area, extension baseline-aligned with last text line
|
||||
|
||||
### `src/components/themes/lyra/LyraTheme.cpp`
|
||||
- Same `wrapTextToLines` rewrite and `truncateWithEllipsis` helper
|
||||
- Same vertical centering for expanded row text lines
|
||||
- Icon also vertically centered in expanded area
|
||||
- Extension baseline-aligned with last text line instead of fixed offset
|
||||
|
||||
### `mod/prs/MERGED.md`
|
||||
- Updated PR #1019 mod enhancement section to reflect the new wrapping strategy and spacing improvements
|
||||
|
||||
## Follow-up Items
|
||||
- Device testing to verify improved wrapping and spacing visually
|
||||
51
chat-summaries/2026-02-20_22-30-summary.md
Normal file
51
chat-summaries/2026-02-20_22-30-summary.md
Normal file
@@ -0,0 +1,51 @@
|
||||
# Port Upstream 1.1.0 RC Commits to mod/master
|
||||
|
||||
## Task Description
|
||||
|
||||
Audited all 11 commits from upstream PR #992 (1.1.0 Release Candidate) and ported the remaining unported/partially-ported ones to `mod/master`.
|
||||
|
||||
## Commit Audit Results
|
||||
|
||||
### Already Fully Ported (no changes needed)
|
||||
- **402e887** - Bump version to 1.1.0 (skip, not relevant)
|
||||
- **3e2c518** (#997) - Glyph null-safety (ported in c1b8e53)
|
||||
- **b8e743e** (#995) - PNGdec buffer size (ported in c1b8e53)
|
||||
- **588984e** (#1010) - Dangling pointer (ported in 18be265)
|
||||
- **87d9d1d** (#978) - Font drawing performance (ported in 3a06418)
|
||||
- **2cc497c** (#957) - Double FAST_REFRESH (ported in ad282ca)
|
||||
|
||||
### Already Effectively Present
|
||||
- **8db3542** (#1017) - Cover outlines for Lyra themes (mod's LyraTheme.cpp already draws outlines unconditionally; Lyra3CoversTheme.cpp doesn't exist in mod)
|
||||
|
||||
### Ported in This Session
|
||||
|
||||
#### 1. Aligned #1002 + Ported #1018 (CSS cache invalidation)
|
||||
**Files:** `CssParser.h`, `CssParser.cpp`, `Epub.cpp`, `ChapterHtmlSlimParser.cpp`
|
||||
- Added `tryInterpretLength()` (bool return + out-param) to properly skip non-length CSS values like `auto`, `inherit`
|
||||
- Added `deleteCache()` method to CssParser
|
||||
- Moved `CSS_CACHE_VERSION` to static class member
|
||||
- Added stale cache file removal in `loadFromCache()` on version mismatch
|
||||
- Added "both CSS width and height set" branch in image sizing logic
|
||||
- Refactored `parseCssFiles()` to early-return when cache exists
|
||||
- Refactored `load()` to call `loadFromCache()` and invalidate sections on stale cache
|
||||
|
||||
#### 2. Ported #1014 (Strip unused CSS rules)
|
||||
**File:** `CssParser.cpp`
|
||||
- Added selector filtering in `processRuleBlockWithStyle` to skip `+`, `>`, `[`, `:`, `#`, `~`, `*`, and whitespace selectors
|
||||
- Fixed `normalized()` trailing whitespace to use `while` loop and also strip `\n`
|
||||
- Added TODO comments for multi-class selector support
|
||||
|
||||
#### 3. Ported #990 (Continue reading card classic theme)
|
||||
**Files:** `BaseTheme.h`, `BaseTheme.cpp`
|
||||
- Changed `homeTopPadding` from 20 to 40
|
||||
- Computed `bookWidth` from cover BMP aspect ratio (clamped to 90% screen width, fallback to half-width)
|
||||
- Fixed centering: added `rect.x` offset to `bookX`, `boxX`, and `continueBoxX`
|
||||
- Simplified cover drawing (removed scaling/centering math since bookWidth now matches aspect ratio)
|
||||
|
||||
## Build Status
|
||||
All changes compile successfully (0 errors, 0 warnings in modified files).
|
||||
|
||||
## Follow-up Items
|
||||
- Test on device to verify CSS cache invalidation works correctly (books with stale caches should auto-rebuild)
|
||||
- Test classic theme continue reading card with various cover aspect ratios
|
||||
- Test image sizing with EPUBs that specify both CSS width and height on images
|
||||
27
chat-summaries/2026-02-21_00-00-summary.md
Normal file
27
chat-summaries/2026-02-21_00-00-summary.md
Normal file
@@ -0,0 +1,27 @@
|
||||
# NTP Sync Clock Feature
|
||||
|
||||
## Task
|
||||
Add a "Sync Clock" action to the Clock settings category that connects to WiFi and performs NTP time synchronization.
|
||||
|
||||
## Changes Made
|
||||
|
||||
### New Files
|
||||
- **src/activities/settings/NtpSyncActivity.h** - Activity class extending `ActivityWithSubactivity` with states: WIFI_SELECTION, SYNCING, SUCCESS, FAILED
|
||||
- **src/activities/settings/NtpSyncActivity.cpp** - Full implementation:
|
||||
- Launches `WifiSelectionActivity` with auto-connect enabled
|
||||
- On WiFi connect: blocking `waitForNtpSync(8000ms)`
|
||||
- Shows synced time on success; auto-dismisses after 5 seconds
|
||||
- Failed state requires manual Back press
|
||||
- Clean WiFi teardown in `onExit()`
|
||||
|
||||
### Modified Files
|
||||
- **src/activities/settings/SettingsActivity.h** - Added `SyncClock` to `SettingAction` enum
|
||||
- **src/activities/settings/SettingsActivity.cpp** - Added include, switch case handler, and action in `rebuildClockActions()`
|
||||
- **lib/I18n/translations/*.yaml** (all 9 files) - Added `STR_SYNC_CLOCK` and `STR_TIME_SYNCED` string keys
|
||||
- **lib/I18n/I18nKeys.h, I18nStrings.h, I18nStrings.cpp** - Regenerated from YAML
|
||||
|
||||
## Build Result
|
||||
SUCCESS - RAM: 32.7%, Flash: 70.7%
|
||||
|
||||
## Follow-up Items
|
||||
- Non-English translations use English fallback values; translators can update later
|
||||
52
chat-summaries/2026-02-21_12-00-summary.md
Normal file
52
chat-summaries/2026-02-21_12-00-summary.md
Normal file
@@ -0,0 +1,52 @@
|
||||
# Manage Books Feature - Implementation
|
||||
|
||||
## Task Description
|
||||
Implemented the full Manage Books feature plan across 10 commits on the `mod/manage-books` branch. The feature adds Archive, Delete, Delete Cache, and Reindex book management actions, an interactive End of Book menu, and a bugfix for Clear Reading Cache.
|
||||
|
||||
## Changes Made (10 commits)
|
||||
|
||||
### COMMIT 1: `feat(hal): expose rename() on HalStorage`
|
||||
- `lib/hal/HalStorage.h` / `.cpp` — Added `rename(path, newPath)` forwarding to SDCardManager for file/directory move operations.
|
||||
|
||||
### COMMIT 2: `feat(i18n): add string keys for book management feature`
|
||||
- `lib/I18n/translations/english.yaml` — Added 15 new string keys (STR_MANAGE_BOOK, STR_ARCHIVE_BOOK, STR_UNARCHIVE_BOOK, etc.)
|
||||
- `lib/I18n/I18nKeys.h` — Regenerated via gen_i18n.py
|
||||
|
||||
### COMMIT 3: `feat: add BookManager utility and RecentBooksStore::clear()`
|
||||
- **New:** `src/util/BookManager.h` / `.cpp` — Static utility namespace with `archiveBook`, `unarchiveBook`, `deleteBook`, `deleteBookCache`, `reindexBook`, `isArchived`, `getBookCachePath`. Archive mirrors directory structure under `/.archive/` and renames cache dirs to match new path hashes.
|
||||
- `src/RecentBooksStore.h` / `.cpp` — Added `clear()` method.
|
||||
|
||||
### COMMIT 4: `feat: add BookManageMenuActivity popup sub-activity`
|
||||
- **New:** `src/activities/home/BookManageMenuActivity.h` / `.cpp` — Contextual popup menu with Archive/Unarchive, Delete, Delete Cache Only, Reindex Book. Supports long-press on Reindex for REINDEX_FULL (includes cover/thumb regeneration).
|
||||
|
||||
### COMMIT 5: `refactor: change browse activities to ActivityWithSubactivity`
|
||||
- `HomeActivity`, `MyLibraryActivity`, `RecentBooksActivity` — Changed base class from `Activity` to `ActivityWithSubactivity`. Added `subActivity` guard at top of `loop()`.
|
||||
|
||||
### COMMIT 6: `feat: add long-press Confirm for book management in file browser and recents`
|
||||
- `MyLibraryActivity` / `RecentBooksActivity` — Long-press Confirm on a book opens BookManageMenuActivity. Actions executed via BookManager, file list refreshed afterward.
|
||||
|
||||
### COMMIT 7: `feat: add long-press on HomeActivity for book management and archive browsing`
|
||||
- `HomeActivity` — Long-press Confirm on recent book opens manage menu. Long-press on Browse Files navigates to `/.archive/`.
|
||||
- `main.cpp` — Wired `onMyLibraryOpenWithPath` callback through to HomeActivity constructor.
|
||||
|
||||
### COMMIT 8: `feat: replace Delete Book Cache with Manage Book in reader menu`
|
||||
- `EpubReaderMenuActivity` — Replaced DELETE_CACHE menu item with MANAGE_BOOK. Opens BookManageMenuActivity as sub-activity. Added ARCHIVE_BOOK, DELETE_BOOK, REINDEX_BOOK, REINDEX_BOOK_FULL action types.
|
||||
- `EpubReaderActivity` — Handles new actions via BookManager.
|
||||
|
||||
### COMMIT 9: `feat: add EndOfBookMenuActivity replacing static end-of-book text`
|
||||
- **New:** `src/activities/reader/EndOfBookMenuActivity.h` / `.cpp` — Interactive popup with Archive, Delete, Back to Beginning, Close Book, Close Menu options.
|
||||
- `EpubReaderActivity` / `XtcReaderActivity` — Replaced static "End of Book" text with EndOfBookMenuActivity.
|
||||
- `TxtReaderActivity` — Added end-of-book detection (advance past last page triggers menu).
|
||||
|
||||
### COMMIT 10: `fix: ClearCacheActivity now clears txt_* caches and recents list`
|
||||
- `ClearCacheActivity.cpp` — Added `txt_*` to directory prefix check. Calls `RECENT_BOOKS.clear()` after cache wipe.
|
||||
|
||||
## New Files
|
||||
- `src/util/BookManager.h` / `.cpp`
|
||||
- `src/activities/home/BookManageMenuActivity.h` / `.cpp`
|
||||
- `src/activities/reader/EndOfBookMenuActivity.h` / `.cpp`
|
||||
|
||||
## Follow-up Items
|
||||
- Test on device: verify all menu interactions, archive/unarchive flow, end-of-book menu behavior
|
||||
- Verify cache rename works correctly across different book formats
|
||||
- Consider adding translations for new strings in non-English language files
|
||||
24
chat-summaries/2026-02-21_16-30-summary.md
Normal file
24
chat-summaries/2026-02-21_16-30-summary.md
Normal file
@@ -0,0 +1,24 @@
|
||||
# Port Upstream PR #1027: ParsedText Word-Width Cache and Hyphenation Early Exit
|
||||
|
||||
## Task
|
||||
|
||||
Ported upstream PR #1027 (jpirnay) into the mod. The PR reduces `ParsedText::layoutAndExtractLines` CPU time by 5–9% via two independent optimizations.
|
||||
|
||||
## Changes Made
|
||||
|
||||
**`lib/Epub/Epub/ParsedText.cpp`** (single file):
|
||||
1. Added `#include <cstring>` for `memcpy`/`memcmp`
|
||||
2. Added 128-entry direct-mapped word-width cache in the anonymous namespace (`WordWidthCacheEntry`, FNV-1a hash, `cachedMeasureWordWidth`). 4 KB in BSS, zero heap allocation.
|
||||
3. Switched `calculateWordWidths` to use `cachedMeasureWordWidth` instead of `measureWordWidth`
|
||||
4. Added `lineBreakIndices.reserve(totalWordCount / 8 + 1)` in `computeLineBreaks`
|
||||
5. In `hyphenateWordAtIndex`: added reusable `prefix` string buffer (avoids per-iteration `substr` allocations) and early exit `break` when prefix exceeds available width (ascending byte-offset order means all subsequent candidates will also be too wide)
|
||||
|
||||
**`mod/prs/MERGED.md`**: Added PR #1027 entry (TOC link + full section with context, changes, differences, discussion).
|
||||
|
||||
## Key Adaptation
|
||||
|
||||
The upstream PR targets `std::list`-based code, but our mod already uses `std::vector` (from PR #1038). List-specific optimizations (splice in `extractLine`, `std::next` vs `std::advance`, `continuesVec` pointer sync) were not applicable. Only the algorithmic improvements were ported.
|
||||
|
||||
## Follow-up Items
|
||||
|
||||
- None. Port is complete.
|
||||
24
chat-summaries/2026-02-21_17-22-summary.md
Normal file
24
chat-summaries/2026-02-21_17-22-summary.md
Normal file
@@ -0,0 +1,24 @@
|
||||
# Port PR #1068: Correct hyphenation of URLs
|
||||
|
||||
## Task
|
||||
|
||||
Port upstream PR [#1068](https://github.com/crosspoint-reader/crosspoint-reader/pull/1068) by Uri-Tauber into the mod fork. The PR was not yet merged upstream, so it was manually patched in.
|
||||
|
||||
## Changes made
|
||||
|
||||
1. **`lib/Epub/Epub/hyphenation/HyphenationCommon.cpp`**: Added `case '/':` to `isExplicitHyphen` switch, treating `/` as an explicit hyphen delimiter for URL path segments.
|
||||
|
||||
2. **`lib/Epub/Epub/hyphenation/Hyphenator.cpp`**: Replaced the single combined filter in `buildExplicitBreakInfos` with a two-stage check:
|
||||
- First checks `isExplicitHyphen(cp)`
|
||||
- Then skips repeated separators (e.g., `//` in `http://`, `--`)
|
||||
- Then applies strict alphabetic-surround rule only for non-URL separators (`cp != '/' && cp != '-'`)
|
||||
|
||||
3. **`mod/prs/MERGED.md`**: Added PR #1068 entry with full documentation.
|
||||
|
||||
## Mod enhancements over upstream PR
|
||||
|
||||
- Included coderabbit's nitpick suggestion (not yet addressed in upstream) to prevent breaks between consecutive identical separators like `//` and `--`.
|
||||
|
||||
## Follow-up items
|
||||
|
||||
- None. Port is complete.
|
||||
26
chat-summaries/2026-02-21_23-15-summary.md
Normal file
26
chat-summaries/2026-02-21_23-15-summary.md
Normal file
@@ -0,0 +1,26 @@
|
||||
# Implement BmpViewer Activity (upstream PR #887)
|
||||
|
||||
## Task
|
||||
Port the BmpViewer feature from upstream crosspoint-reader PR #887 to the mod/master fork, enabling .bmp file viewing from the file browser.
|
||||
|
||||
## Changes Made
|
||||
|
||||
### New Files
|
||||
- **src/activities/util/BmpViewerActivity.h** -- Activity subclass declaration with file path, onGoBack callback, and loadFailed state
|
||||
- **src/activities/util/BmpViewerActivity.cpp** -- BMP loading/rendering implementation: shows loading indicator, opens file via Storage HAL, parses BMP headers, computes centered position with aspect-ratio-preserving layout, renders via `renderer.drawBitmap()`, draws localized back button hint, handles back button input in loop
|
||||
|
||||
### Modified Files
|
||||
- **src/activities/reader/ReaderActivity.h** -- Added `isBmpFile()` and `onGoToBmpViewer()` private declarations
|
||||
- **src/activities/reader/ReaderActivity.cpp** -- Added BmpViewerActivity include, `isBmpFile()` implementation, `onGoToBmpViewer()` implementation (sets currentBookPath, exits, enters BmpViewerActivity with goToLibrary callback), BMP routing in `onEnter()` before XTC/TXT checks
|
||||
- **src/activities/home/MyLibraryActivity.cpp** -- Added `.bmp` to the file extension filter in `loadFiles()`
|
||||
- **src/components/themes/lyra/LyraTheme.cpp** -- Changed `fillRect` to `fillRoundedRect` in `drawButtonHints()` for both FULL and SMALL button sizes to prevent white rectangles overflowing rounded button borders
|
||||
|
||||
## Build Result
|
||||
- PlatformIO build SUCCESS (default env)
|
||||
- RAM: 32.7% (107,308 / 327,680 bytes)
|
||||
- Flash: 71.1% (4,657,244 / 6,553,600 bytes)
|
||||
|
||||
## Follow-up Items
|
||||
- Test on hardware with various BMP files (different sizes, bit depths)
|
||||
- Consider adding image scaling for oversized BMPs (currently `drawBitmap` handles scaling)
|
||||
- Future enhancements mentioned in upstream PR: next/prev image navigation, "display and sleep" button
|
||||
27
chat-summaries/2026-02-21_port-pr1055-byte-framebuffer.md
Normal file
27
chat-summaries/2026-02-21_port-pr1055-byte-framebuffer.md
Normal file
@@ -0,0 +1,27 @@
|
||||
# Port PR #1055: Byte-level framebuffer writes
|
||||
|
||||
**Date:** 2026-02-21
|
||||
**Task:** Port upstream PR #1055 (jpirnay) into the mod, including coderabbit review nitpick.
|
||||
|
||||
## Changes made
|
||||
|
||||
### `lib/GfxRenderer/GfxRenderer.h`
|
||||
- Added two private method declarations: `fillPhysicalHSpanByte` (patterned byte-level span writer) and `fillPhysicalHSpan` (solid-fill wrapper).
|
||||
|
||||
### `lib/GfxRenderer/GfxRenderer.cpp`
|
||||
- Added `#include <cstring>` for `memset`.
|
||||
- Implemented `fillPhysicalHSpanByte`: bounds-clamped byte-level span writer with MSB-first bit packing, partial-byte masking at edges, and `memset` for aligned middle.
|
||||
- Implemented `fillPhysicalHSpan`: thin wrapper mapping `state` bool to `0x00`/`0xFF` pattern byte.
|
||||
- **`drawLine`**: Replaced per-pixel loops for axis-aligned lines with orientation-aware `fillPhysicalHSpan` calls (vertical lines in Portrait, horizontal lines in Landscape).
|
||||
- **`fillRect`**: Replaced per-row `drawLine` loop with orientation-specific byte-level fast paths for all 4 orientations.
|
||||
- **`fillRectDither`**: Replaced per-pixel `drawPixelDither` loops for DarkGray/LightGray with orientation-aware `fillPhysicalHSpanByte` using pre-computed byte patterns.
|
||||
- **`fillPolygon`** (coderabbit nitpick): Added `fillPhysicalHSpan` fast path for Landscape orientations in the scanline inner loop.
|
||||
|
||||
### `mod/prs/MERGED.md`
|
||||
- Appended PR #1055 entry with full documentation.
|
||||
|
||||
## Differences from upstream
|
||||
- `fillPolygon` landscape optimization (from coderabbit review nitpick) was applied as a mod enhancement.
|
||||
|
||||
## Follow-up items
|
||||
- None. Build verified successful.
|
||||
28
chat-summaries/2026-02-21_readme-mod-update.md
Normal file
28
chat-summaries/2026-02-21_readme-mod-update.md
Normal file
@@ -0,0 +1,28 @@
|
||||
# README.md Update for Mod Branch
|
||||
|
||||
**Date:** 2026-02-21
|
||||
|
||||
## Task
|
||||
|
||||
Updated `README.md` to distinguish this mod fork from upstream CrossPoint Reader, documenting all mod-exclusive features, updated feature status, upstream PR ports, and upstream compatibility notes.
|
||||
|
||||
## Changes Made
|
||||
|
||||
**File:** `README.md`
|
||||
|
||||
1. **Header/intro** — Renamed to "CrossPoint Reader (Mod)" with a link to the upstream repo and a summary of what the mod provides. Added blockquote linking to upstream.
|
||||
2. **Motivation** — Added a paragraph explaining the mod's purpose (faster iteration on features and fixes). Fixed "truely" typo.
|
||||
3. **Features & Usage checklist** — Updated to reflect current mod capabilities:
|
||||
- Checked: image support (JPEG/PNG), table rendering, bookmarks, dictionary, book management, clock, letterbox fill, placeholder covers, screen rotation (4 orientations), end-of-book menu, silent pre-indexing
|
||||
- Added sub-items: file extensions, expandable rows
|
||||
- Still unchecked: user provided fonts, full UTF, EPUB picker with cover art
|
||||
4. **New section: "What This Mod Adds"** — Organized by category (Reading Enhancements, Home Screen & Navigation, Book Management, Reader Menu, Display & Rendering, Performance). Documents bookmarks, dictionary, tables, end-of-book menu, clock/NTP, adaptive home screen, file browser improvements, archive system, manage book menu, long-press actions, letterbox fill, landscape CCW, silent pre-indexing, placeholder covers, and 5 ported upstream performance PRs.
|
||||
5. **New section: "Upstream Compatibility"** — Documents what's missing from the mod (BmpViewer, Catalan translations), build differences (font/hyphenation omissions, version string), and links to `mod/prs/MERGED.md`.
|
||||
6. **Installing** — Replaced web flasher instructions with `pio run -e mod --target upload`. Noted `env:default` alternative.
|
||||
7. **Development** — Updated clone command to use `-b mod/master`. Added build environments table (`mod` vs `default`). Updated flash command.
|
||||
8. **Contributing** — Simplified to note this is a personal mod fork with a link to the upstream repo for contributions.
|
||||
|
||||
## Follow-up Items
|
||||
|
||||
- `USER_GUIDE.md` is out of date and does not document mod features (bookmarks, dictionary, clock, book management, etc.)
|
||||
- The data caching section in Internals could be expanded to mention mod-specific cache files (`bookmarks.bin`, `book_settings.bin`, `dictionary.cache`, image `.pxc` files)
|
||||
28
chat-summaries/2026-02-26_12-00-summary.md
Normal file
28
chat-summaries/2026-02-26_12-00-summary.md
Normal file
@@ -0,0 +1,28 @@
|
||||
# Boot NTP Auto-Sync Feature
|
||||
|
||||
## Task
|
||||
Add a "Auto Sync on Boot" toggle in Clock Settings that silently syncs time via WiFi/NTP during boot, with no user interaction required and graceful failure handling.
|
||||
|
||||
## Changes Made
|
||||
|
||||
### New files
|
||||
- `src/util/BootNtpSync.h` / `src/util/BootNtpSync.cpp` -- Background FreeRTOS task that scans WiFi, connects to a saved network, runs NTP sync, then tears down WiFi. Provides `start()`, `cancel()`, and `isRunning()` API.
|
||||
|
||||
### Modified files
|
||||
- `src/CrossPointSettings.h` -- Added `uint8_t autoNtpSync = 0` field
|
||||
- `src/CrossPointSettings.cpp` -- Added persistence (write/read) for the new field
|
||||
- `src/SettingsList.h` -- Added Toggle entry under Clock category
|
||||
- `lib/I18n/translations/*.yaml` (all 9 languages) -- Added `STR_AUTO_NTP_SYNC` string
|
||||
- `lib/I18n/I18nKeys.h` / `lib/I18n/I18nStrings.cpp` -- Regenerated via `gen_i18n.py`
|
||||
- `src/main.cpp` -- Calls `BootNtpSync::start()` during setup (non-blocking)
|
||||
- `src/activities/settings/NtpSyncActivity.cpp` -- Added `BootNtpSync::cancel()` guard
|
||||
- `src/activities/network/WifiSelectionActivity.cpp` -- Added cancel guard
|
||||
- `src/activities/settings/OtaUpdateActivity.cpp` -- Added cancel guard
|
||||
- `src/activities/settings/KOReaderAuthActivity.cpp` -- Added cancel guard
|
||||
- `src/activities/reader/KOReaderSyncActivity.cpp` -- Added cancel guard
|
||||
- `src/activities/network/CrossPointWebServerActivity.cpp` -- Added cancel guard
|
||||
|
||||
## Follow-up Items
|
||||
- Translations: all non-English languages currently use English fallback for the new string
|
||||
- The FreeRTOS task uses 4096 bytes of stack; monitor for stack overflow if WiFi scan behavior changes
|
||||
- WiFi adds ~50-60KB RAM pressure during the sync window (temporary)
|
||||
43
chat-summaries/2026-02-26_18-46-summary.md
Normal file
43
chat-summaries/2026-02-26_18-46-summary.md
Normal file
@@ -0,0 +1,43 @@
|
||||
# Port Upstream PRs #1207 and #1209
|
||||
|
||||
## Task
|
||||
Ported two upstream PRs from crosspoint-reader/crosspoint-reader to the fork:
|
||||
- **PR #1207**: `fix: use HTTPClient::writeToStream for downloading files from OPDS`
|
||||
- **PR #1209**: `feat: Support for multiple OPDS servers`
|
||||
|
||||
Cherry-picking was not possible due to significant divergences (WiFiClient vs NetworkClient naming, i18n changes, binary vs JSON settings, missing JsonSettingsIO/ObfuscationUtils).
|
||||
|
||||
## Changes Made
|
||||
|
||||
### PR #1207 (2 files modified)
|
||||
- `src/network/HttpDownloader.cpp` — Added `FileWriteStream` class, replaced manual chunked download loop with `HTTPClient::writeToStream`, improved Content-Length handling and post-download validation
|
||||
- `src/activities/browser/OpdsBookBrowserActivity.cpp` — Truncated download status text to fit screen width
|
||||
|
||||
### PR #1209 (6 files created, ~15 files modified, 2 files deleted)
|
||||
|
||||
**New files:**
|
||||
- `src/OpdsServerStore.h` / `src/OpdsServerStore.cpp` — Singleton store for up to 8 OPDS servers with MAC-based XOR+base64 password obfuscation and JSON persistence (self-contained — fork lacks JsonSettingsIO/ObfuscationUtils, so persistence was inlined)
|
||||
- `src/activities/settings/OpdsServerListActivity.h` / `.cpp` — Dual-mode activity: settings list (add/edit/delete) and server picker
|
||||
- `src/activities/settings/OpdsSettingsActivity.h` / `.cpp` — Individual server editor (name, URL, username, password, delete)
|
||||
|
||||
**Modified files:**
|
||||
- `src/network/HttpDownloader.h` / `.cpp` — Added per-call username/password parameters (default empty), removed global SETTINGS dependency
|
||||
- `src/activities/browser/OpdsBookBrowserActivity.h` / `.cpp` — Constructor accepts `OpdsServer`, uses server-specific credentials and URL, shows server name in header
|
||||
- `src/activities/home/HomeActivity.h` / `.cpp` — `hasOpdsUrl` → `hasOpdsServers`, uses `OPDS_STORE.hasServers()`
|
||||
- `src/main.cpp` — Added OPDS_STORE loading on boot, server picker when multiple servers configured
|
||||
- `src/activities/settings/SettingsActivity.cpp` — Replaced CalibreSettingsActivity with OpdsServerListActivity
|
||||
- `src/network/CrossPointWebServer.h` / `.cpp` — Added REST endpoints: GET/POST /api/opds, POST /api/opds/delete
|
||||
- `src/network/html/SettingsPage.html` — Added OPDS server management UI (CSS + JS)
|
||||
- `src/SettingsList.h` — Removed legacy OPDS entries (opdsServerUrl, opdsUsername, opdsPassword)
|
||||
- 9 translation YAML files — Added STR_ADD_SERVER, STR_SERVER_NAME, STR_NO_SERVERS, STR_DELETE_SERVER, STR_DELETE_CONFIRM, STR_OPDS_SERVERS
|
||||
|
||||
**Deleted files:**
|
||||
- `src/activities/settings/CalibreSettingsActivity.h` / `.cpp`
|
||||
|
||||
## Key Adaptation Decisions
|
||||
- Kept `WiFiClient`/`WiFiClientSecure` naming (fork hasn't adopted `NetworkClient` rename)
|
||||
- Inlined JSON persistence and MAC-based obfuscation directly into `OpdsServerStore.cpp` (fork lacks `JsonSettingsIO` and `ObfuscationUtils` libraries)
|
||||
- Legacy single-server settings auto-migrate to new `opds.json` on first boot
|
||||
|
||||
## Build Status
|
||||
Compilation verified: SUCCESS (RAM: 32.8%, Flash: 71.4%)
|
||||
33
chat-summaries/2026-03-02_00-00-summary.md
Normal file
33
chat-summaries/2026-03-02_00-00-summary.md
Normal file
@@ -0,0 +1,33 @@
|
||||
# OPDS Post-Download Prompt Screen
|
||||
|
||||
## Task
|
||||
After an OPDS download completes, instead of immediately returning to the catalog listing, show a prompt screen with two options: "Back to Listing" and "Open Book". A 5-second auto-timer executes the default action. A per-server setting controls which option is default (back to listing by default for backward compatibility).
|
||||
|
||||
## Changes Made
|
||||
|
||||
### New i18n strings
|
||||
- `lib/I18n/translations/english.yaml` — Added `STR_DOWNLOAD_COMPLETE`, `STR_OPEN_BOOK`, `STR_BACK_TO_LISTING`, `STR_AFTER_DOWNLOAD`
|
||||
- Regenerated `lib/I18n/I18nKeys.h`, `lib/I18n/I18nStrings.cpp` via `gen_i18n.py`
|
||||
|
||||
### Per-server setting
|
||||
- `src/OpdsServerStore.h` — Added `afterDownloadAction` field to `OpdsServer` (0 = back to listing, 1 = open book)
|
||||
- `src/OpdsServerStore.cpp` — Serialized/deserialized `after_download` in JSON
|
||||
|
||||
### Browser activity
|
||||
- `src/activities/browser/OpdsBookBrowserActivity.h` — Added `DOWNLOAD_COMPLETE` state, `onGoToReader` callback, `downloadedFilePath`, `downloadCompleteTime`, `promptSelection` members, `executePromptAction()` method
|
||||
- `src/activities/browser/OpdsBookBrowserActivity.cpp` — On download success: transition to `DOWNLOAD_COMPLETE` state instead of `BROWSING`. Added loop handling (Left/Right to toggle selection, Confirm to execute, Back to go to listing, 5s auto-timer). Added render for prompt screen with bracketed selection UI. Added `executePromptAction()` helper.
|
||||
|
||||
### Callback threading
|
||||
- `src/main.cpp` — Passed `onGoToReader` callback to `OpdsBookBrowserActivity` constructor in both single-server and multi-server code paths
|
||||
|
||||
### Settings UI
|
||||
- `src/activities/settings/OpdsSettingsActivity.cpp` — Incremented `BASE_ITEMS` from 6 to 7. Added "After Download" field at index 6 (toggles between "Back to Listing" and "Open Book"). Shifted Delete to index 7.
|
||||
|
||||
### Countdown refinements (follow-up)
|
||||
- Added live countdown display using `FAST_REFRESH` -- shows "(5s)", "(4s)", etc., updating each second
|
||||
- Any button press (Left/Right to change selection, Back, Confirm) cancels the countdown entirely instead of resetting it
|
||||
- Added `countdownActive` bool and `lastCountdownSecond` int to track state without redundant redraws
|
||||
|
||||
## Follow-up Items
|
||||
- Translations for the 4 new strings in non-English languages
|
||||
- On-device testing of the full flow (download → prompt → open book / back to listing)
|
||||
18
chat-summaries/2026-03-02_06-48-summary.md
Normal file
18
chat-summaries/2026-03-02_06-48-summary.md
Normal file
@@ -0,0 +1,18 @@
|
||||
# OPDS Per-Server Default Save Directory
|
||||
|
||||
## Task
|
||||
Add a configurable default download path per OPDS server. The directory picker now starts at the server's saved path instead of always at root. New servers default to "/".
|
||||
|
||||
## Changes Made
|
||||
|
||||
### Modified files
|
||||
- **`src/OpdsServerStore.h`** -- Added `downloadPath` field (default `"/"`) to `OpdsServer` struct.
|
||||
- **`src/OpdsServerStore.cpp`** -- Serialize/deserialize `download_path` in JSON. Existing servers without the field default to `"/"`.
|
||||
- **`src/activities/settings/OpdsSettingsActivity.cpp`** -- Added "Download Path" as field index 4 (shifted Delete to 5). Uses `DirectoryPickerActivity` as subactivity for folder selection. Displays current path in row value.
|
||||
- **`src/activities/util/DirectoryPickerActivity.h`** / **`.cpp`** -- Added `initialPath` constructor parameter (default `"/"`). `onEnter()` validates the path exists on disk and falls back to `"/"` if not.
|
||||
- **`src/activities/browser/OpdsBookBrowserActivity.cpp`** -- Passes `server.downloadPath` as `initialPath` when launching the directory picker.
|
||||
- **`lib/I18n/translations/*.yaml`** (all 9 languages) -- Added `STR_DOWNLOAD_PATH` with translations.
|
||||
- **`lib/I18n/I18nKeys.h`**, **`I18nStrings.h`**, **`I18nStrings.cpp`** -- Regenerated.
|
||||
|
||||
## Follow-up Items
|
||||
- Test on device: verify settings UI shows/saves path, picker opens at saved path, fallback works when directory is removed
|
||||
46
chat-summaries/2026-03-02_12-00-summary.md
Normal file
46
chat-summaries/2026-03-02_12-00-summary.md
Normal file
@@ -0,0 +1,46 @@
|
||||
# OPDS Server Reordering
|
||||
|
||||
## Task
|
||||
Add the ability to reorder OPDS servers via a `sortOrder` field, editable on-device and with up/down buttons in the web UI.
|
||||
|
||||
## Changes Made
|
||||
|
||||
### Data Model (`src/OpdsServerStore.h`)
|
||||
- Added `int sortOrder = 0` field to `OpdsServer` struct
|
||||
|
||||
### Store Logic (`src/OpdsServerStore.cpp`)
|
||||
- `saveToFile()`: persists `sort_order` to JSON
|
||||
- `loadFromFile()`: reads `sort_order`, assigns sequential defaults to servers missing it, sorts after load
|
||||
- `migrateFromSettings()`: assigns `sortOrder = 1` to migrated server
|
||||
- `addServer()`: auto-assigns `sortOrder = max(existing) + 1` when left at 0
|
||||
- `updateServer()`: re-sorts after update
|
||||
- Added `sortServers()` private helper: sorts by sortOrder ascending, ties broken case-insensitively by name (falling back to URL)
|
||||
- Added `moveServer(index, direction)`: swaps sortOrder with adjacent server, re-sorts, saves
|
||||
|
||||
### On-Device UI (`src/activities/settings/OpdsSettingsActivity.cpp`)
|
||||
- Added "Position" as first menu item (index 0), shifting all others by 1
|
||||
- Uses `NumericStepperActivity` (new) for position editing: numeric stepper with Up/Down and PageForward/PageBack to increment/decrement
|
||||
- `saveServer()` now re-locates the server by name+url after sort to keep `serverIndex` valid
|
||||
|
||||
### NumericStepperActivity (new: `src/activities/util/NumericStepperActivity.{h,cpp}`)
|
||||
- Reusable numeric stepper modeled after `SetTimezoneOffsetActivity`
|
||||
- Displays value with highlight rect and up/down arrow indicators
|
||||
- Up/PageForward increment, Down/PageBack decrement (clamped to min/max)
|
||||
- Confirm saves, Back cancels
|
||||
- Side button hints show +/-
|
||||
|
||||
### Web API (`src/network/CrossPointWebServer.{h,cpp}`)
|
||||
- `GET /api/opds`: now includes `sortOrder` in response
|
||||
- `POST /api/opds`: preserves `downloadPath` and `sortOrder` on update
|
||||
- New `POST /api/opds/reorder`: accepts `{index, direction: "up"|"down"}`, calls `moveServer()`
|
||||
|
||||
### Web UI (`src/network/html/SettingsPage.html`)
|
||||
- Added up/down arrow buttons to each OPDS server card (hidden at boundaries)
|
||||
- Added `reorderOpdsServer()` JS function calling the new API endpoint
|
||||
|
||||
### i18n
|
||||
- Added `STR_POSITION` to all 9 translation YAML files
|
||||
- Regenerated `I18nKeys.h`, `I18nStrings.h`, `I18nStrings.cpp`
|
||||
|
||||
## Follow-up Items
|
||||
- None identified; build passes cleanly.
|
||||
35
chat-summaries/2026-03-02_21-00-summary.md
Normal file
35
chat-summaries/2026-03-02_21-00-summary.md
Normal file
@@ -0,0 +1,35 @@
|
||||
# Port Upstream KOReader Sync PRs
|
||||
|
||||
## Task
|
||||
Port three unmerged upstream PRs into the fork:
|
||||
- PR #1185: Cache KOReader document hash
|
||||
- PR #1217: Proper KOReader XPath synchronisation
|
||||
- PR #1090: Push progress and sleep (with silent failure adaptation)
|
||||
|
||||
## Changes Made
|
||||
|
||||
### PR #1185 — Cache KOReader Document Hash
|
||||
- **lib/KOReaderSync/KOReaderDocumentId.h**: Added private static helpers `getCacheFilePath`, `loadCachedHash`, `saveCachedHash`
|
||||
- **lib/KOReaderSync/KOReaderDocumentId.cpp**: Cache lookup before hash computation, persist after; uses mtime fingerprint + file size for validation
|
||||
|
||||
### PR #1217 — Proper KOReader XPath Synchronisation
|
||||
- **lib/KOReaderSync/ChapterXPathIndexer.h/.cpp**: New files — Expat-based on-demand XHTML parsing for bidirectional XPath/progress mapping
|
||||
- **lib/KOReaderSync/ProgressMapper.h/.cpp**: XPath-first mapping in both directions, percentage fallback, DocFragment 1-based indexing fix, `std::clamp` sanitization
|
||||
- **docs/contributing/koreader-sync-xpath-mapping.md**: Design doc
|
||||
|
||||
### PR #1090 — Push Progress & Sleep (adapted)
|
||||
Adapted to fork's `ActivityWithSubactivity` + callback architecture (upstream uses `Activity` + `startActivityForResult`).
|
||||
|
||||
- **lib/I18n/translations/english.yaml** + auto-generated **I18nKeys.h**: Added `STR_PUSH_AND_SLEEP`
|
||||
- **src/activities/reader/EpubReaderMenuActivity.h**: Added `PUSH_AND_SLEEP` to `MenuAction` enum and `buildMenuItems()`
|
||||
- **src/activities/reader/KOReaderSyncActivity.h/.cpp**: Added `SyncMode::PUSH_ONLY`, `deferFinish()` mechanism, PUSH_ONLY paths in `performSync`/`performUpload`/`loop`
|
||||
- **src/activities/reader/EpubReaderActivity.h/.cpp**: Added `pendingSleep` flag, `extern void enterDeepSleep()`, `PUSH_AND_SLEEP` case in `onReaderMenuConfirm`
|
||||
|
||||
**Silent failure**: Both `onCancel` and `onSyncComplete` callbacks set `pendingSleep = true`, so the device sleeps regardless of sync success/failure. No credentials also triggers sleep directly.
|
||||
|
||||
## Build
|
||||
Compiles cleanly on `default` environment (ESP32-C3). RAM: 32.8%, Flash: 71.7%.
|
||||
|
||||
## Follow-up Items
|
||||
- Changes are unstaged — commit when ready
|
||||
- Other language YAML files will auto-fallback to English for `STR_PUSH_AND_SLEEP`
|
||||
50
chat-summaries/2026-03-07_18-00-summary.md
Normal file
50
chat-summaries/2026-03-07_18-00-summary.md
Normal file
@@ -0,0 +1,50 @@
|
||||
# Fresh Replay: Sync mod/master with upstream/master (continued)
|
||||
|
||||
## Task
|
||||
Continue the "Fresh Replay" synchronization of mod/master with upstream/master (HEAD: 170cc25). This session picked up from Phase 2c (GfxRenderer/theme modifications) and completed through Phase 4 (verification).
|
||||
|
||||
## Changes Made
|
||||
|
||||
### Phase 2c-e: GfxRenderer, themes, SleepActivity, SettingsActivity, platformio
|
||||
- Added `drawPixelGray` to GfxRenderer for letterbox fill rendering
|
||||
- Added `PRERENDER_THUMB_HEIGHTS` to UITheme for placeholder cover generation
|
||||
- Added `[env:mod]` build environment to platformio.ini
|
||||
- Implemented sleep screen letterbox fill (solid/dithered) with edge caching in SleepActivity
|
||||
- Added placeholder cover fallback (PlaceholderCoverGenerator) for XTC/TXT/EPUB sleep screens
|
||||
- Added Clock settings category to SettingsActivity with timezone, NTP sync, set-time actions
|
||||
- Replaced CalibreSettingsActivity with OpdsServerListActivity for OPDS server management
|
||||
- Added DynamicEnum rendering support for settings
|
||||
- Added long-press book management to RecentBooksActivity
|
||||
|
||||
### Phase 3: Re-port unmerged upstream PRs
|
||||
- **#1055** (byte-level framebuffer writes): fillPhysicalHSpan*, optimized fillRect/drawLine/fillRectDither/fillPolygon
|
||||
- **#1027** (word-width cache): 128-entry FNV-1a cache, hyphenation early exit (7-9% layout speedup)
|
||||
- **#1068** (URL hyphenation): Already present in upstream
|
||||
- **#1019** (file extensions in browser): Already present in upstream
|
||||
- **#1090/#1185/#1217** (KOReader sync): Binary credential store, document hash caching, ChapterXPathIndexer
|
||||
- **#1209** (OPDS multi-server): OpdsBookBrowserActivity accepts OpdsServer, directory picker, download-complete prompt
|
||||
- **#857** (Dictionary): Activities already ported in Phase 1/2
|
||||
- **#1003** (Placeholder covers): Already integrated in Phase 2
|
||||
|
||||
### Fixes
|
||||
- Added `STR_OFF` i18n string for clock format setting
|
||||
- Fixed include paths (ActivityResult.h from subdirectories)
|
||||
- Replaced `Epub::isValidThumbnailBmp` with `Storage.exists()` (method doesn't exist in upstream)
|
||||
- Replaced `StringUtils::checkFileExtension` with `FsHelpers` equivalents
|
||||
|
||||
### Image pipeline decision
|
||||
- Kept upstream's JPEGDEC implementation — mod's picojpeg was a workaround for the older codebase
|
||||
- No mod-specific image pipeline changes needed
|
||||
|
||||
## Branch Status
|
||||
- **mod/master-resync**: 6 commits ahead of upstream/master (170cc25)
|
||||
- **mod/backup-pre-sync-2026-03-07**: Safety snapshot of original mod/master
|
||||
- 189 files changed, ~114,566 insertions, ~379 deletions vs upstream
|
||||
|
||||
## Follow-up Items
|
||||
- Run full PlatformIO build on hardware to verify compilation
|
||||
- Run clang-format on all modified files
|
||||
- Test on device: clock display, sleep screen letterbox fill, dictionary, OPDS browsing
|
||||
- KOReaderSyncActivity PUSH_ONLY mode (from PR #1090) not yet re-added to activity
|
||||
- Consider adding `StringUtils.h` if other mod code needs `checkFileExtension`
|
||||
- Update mod version string
|
||||
42
chat-summaries/2026-03-07_19-00-summary.md
Normal file
42
chat-summaries/2026-03-07_19-00-summary.md
Normal file
@@ -0,0 +1,42 @@
|
||||
# KOReaderSyncActivity PUSH_ONLY Mode Re-addition
|
||||
|
||||
**Date**: 2026-03-07
|
||||
**Branch**: `mod/master-resync`
|
||||
|
||||
## Task
|
||||
|
||||
Re-add the `PUSH_ONLY` sync mode to `KOReaderSyncActivity` that was lost during the upstream resync/ActivityManager migration (originally from PR #1090). This mode allows the reader to silently push local progress to the KOReader sync server and then enter deep sleep — no interactive UI.
|
||||
|
||||
## Changes
|
||||
|
||||
### `src/activities/reader/KOReaderSyncActivity.h`
|
||||
- Added `enum class SyncMode { INTERACTIVE, PUSH_ONLY }` to the class
|
||||
- Added `syncMode` constructor parameter (defaults to `INTERACTIVE`)
|
||||
- Added `deferFinish(bool success)`, `pendingFinish`, `pendingFinishSuccess` for safe async finish from blocking calls
|
||||
|
||||
### `src/activities/reader/KOReaderSyncActivity.cpp`
|
||||
- Implemented `deferFinish()` — sets a flag that `loop()` picks up to call `setResult()`/`finish()`
|
||||
- `onEnter()`: In PUSH_ONLY mode, silently finish if no credentials (no error UI)
|
||||
- `performSync()`: In PUSH_ONLY mode, skip remote fetch entirely and go straight to `performUpload()`
|
||||
- `performUpload()`: In PUSH_ONLY mode, use `deferFinish()` instead of setting UI state on success/failure
|
||||
- `loop()`: Check `pendingFinish` first and perform deferred finish if set
|
||||
|
||||
### `src/activities/ActivityManager.h`
|
||||
- Added `requestSleep()` / `isSleepRequested()` — allows activities to signal that the device should enter deep sleep. Checked by the main loop.
|
||||
|
||||
### `src/main.cpp`
|
||||
- Added `activityManager.isSleepRequested()` check in the main loop, before the auto-sleep timeout check
|
||||
|
||||
### `src/activities/reader/EpubReaderMenuActivity.h` / `.cpp`
|
||||
- Added `PUSH_AND_SLEEP` to the `MenuAction` enum
|
||||
- Added menu item `{PUSH_AND_SLEEP, STR_PUSH_AND_SLEEP}` between SYNC and CLOSE_BOOK
|
||||
|
||||
### `src/activities/reader/EpubReaderActivity.cpp`
|
||||
- Added `#include "activities/ActivityManager.h"`
|
||||
- Added `PUSH_AND_SLEEP` case in `onReaderMenuConfirm`: launches `KOReaderSyncActivity` in `PUSH_ONLY` mode, then calls `activityManager.requestSleep()` on completion (regardless of success/failure)
|
||||
|
||||
### `lib/I18n/translations/english.yaml` / `lib/I18n/I18nKeys.h`
|
||||
- Added `STR_PUSH_AND_SLEEP: "Push & Sleep"` and regenerated I18n keys
|
||||
|
||||
## Follow-up Items
|
||||
- None
|
||||
61
chat-summaries/2026-03-07_20-00-summary.md
Normal file
61
chat-summaries/2026-03-07_20-00-summary.md
Normal file
@@ -0,0 +1,61 @@
|
||||
# Missing Mod Features Audit — Implementation
|
||||
|
||||
**Date**: 2026-03-07
|
||||
**Branch**: `mod/master-resync`
|
||||
|
||||
## Task
|
||||
|
||||
Comprehensive audit of `mod/master-resync` vs `mod/backup-pre-sync-2026-03-07` identified 4 mod features lost during the upstream resync. All 4 have been re-implemented.
|
||||
|
||||
## Changes
|
||||
|
||||
### 1. EndOfBookMenuActivity wired into EpubReaderActivity (HIGH)
|
||||
|
||||
**Files**: `EpubReaderActivity.h`, `EpubReaderActivity.cpp`
|
||||
|
||||
- Added `pendingEndOfBookMenu` and `endOfBookMenuOpened` flags
|
||||
- In `render()`: when reaching end-of-book, sets `pendingEndOfBookMenu = true` (deferred to avoid render-lock deadlock)
|
||||
- In `loop()`: checks flag and launches `EndOfBookMenuActivity` via `startActivityForResult`
|
||||
- Result handler covers all 6 actions: ARCHIVE (→ goHome), DELETE (→ goHome), TABLE_OF_CONTENTS (→ last chapter), BACK_TO_BEGINNING (→ spine 0), CLOSE_BOOK (→ goHome), CLOSE_MENU (→ stay at end)
|
||||
- Added `#include "EndOfBookMenuActivity.h"` and `#include "util/BookManager.h"`
|
||||
|
||||
### 2. Book management from reader menu (MEDIUM)
|
||||
|
||||
**Files**: `EpubReaderMenuActivity.h`, `EpubReaderMenuActivity.cpp`, `EpubReaderActivity.cpp`
|
||||
|
||||
- Added `ARCHIVE_BOOK`, `DELETE_BOOK`, `REINDEX_BOOK` to `MenuAction` enum
|
||||
- Added corresponding menu items between CLOSE_BOOK and DELETE_CACHE
|
||||
- Added handlers in `onReaderMenuConfirm`: each calls `BookManager::archiveBook/deleteBook/reindexBook` then `activityManager.goHome()`
|
||||
|
||||
### 3. Silent next-chapter pre-indexing (MEDIUM)
|
||||
|
||||
**Files**: `EpubReaderActivity.h`, `EpubReaderActivity.cpp`
|
||||
|
||||
- Added `preIndexedNextSpine` field and `silentIndexNextChapterIfNeeded()` method
|
||||
- Triggers when user is on second-to-last or last page of a chapter
|
||||
- Creates section file for `currentSpineIndex + 1` in advance
|
||||
- Called after every page turn in `loop()`
|
||||
- ~35 lines of self-contained implementation
|
||||
|
||||
### 4. Letterbox fill toggle in reader menu (LOW)
|
||||
|
||||
**Files**: `EpubReaderMenuActivity.h`, `EpubReaderMenuActivity.cpp`, `EpubReaderActivity.cpp`
|
||||
|
||||
- Added `LETTERBOX_FILL` to `MenuAction` enum
|
||||
- Added `bookCachePath` constructor parameter (with default `""` for backward compat)
|
||||
- Added per-book `pendingLetterboxFill`, `letterboxFillLabels`, `letterboxFillToIndex()`, `indexToLetterboxFill()`, `saveLetterboxFill()`
|
||||
- Cycles Default → Dithered → Solid → None → Default on Confirm
|
||||
- Renders current value on right edge of menu item
|
||||
- Loads/saves per-book setting via `BookSettings`
|
||||
- Updated call site in `EpubReaderActivity` to pass `epub->getCachePath()`
|
||||
|
||||
## Audit False Positives (confirmed NOT gaps)
|
||||
|
||||
- GfxRenderer kerning/ligatures/wrappedText — present on resync
|
||||
- HttpDownloader auth fallback — present with OPDS settings fallback
|
||||
- Lyra3CoversTheme — exists on resync
|
||||
- ActivityWithSubactivity → Activity migration — intentional upstream change
|
||||
- EndOfBookMenuActivity callbacks → setResult/finish — correctly migrated
|
||||
|
||||
## Follow-up Items
|
||||
- None
|
||||
23
chat-summaries/2026-03-07_21-00-summary.md
Normal file
23
chat-summaries/2026-03-07_21-00-summary.md
Normal file
@@ -0,0 +1,23 @@
|
||||
# Fix mod build environment compilation errors
|
||||
|
||||
## Task
|
||||
Fix compilation errors in the `mod` PlatformIO build environment after the upstream resync. The `default` environment was also verified.
|
||||
|
||||
## Changes Made
|
||||
|
||||
### Include path fixes (11 files)
|
||||
- `src/activities/{reader,settings,util}/*.cpp`: Changed bare `#include "ActivityResult.h"` to `#include "activities/ActivityResult.h"` (10 files)
|
||||
- `src/activities/reader/DictionarySuggestionsActivity.cpp`: Changed `#include "RenderLock.h"` to `#include "activities/RenderLock.h"`
|
||||
|
||||
### API compatibility fixes
|
||||
- `src/util/Dictionary.h`: Replaced invalid `class FsFile;` forward declaration with `#include <HalStorage.h>` (`FsFile` is now a `using` alias)
|
||||
- `lib/Epub/Epub/blocks/TextBlock.h`: Added `getWordXpos()` public accessor
|
||||
- `lib/GfxRenderer/GfxRenderer.{h,cpp}`: Re-added `drawTextRotated90CCW()` with `Rotated90CCW` enum value and coordinate mapping, adapted to new fixed-point rendering
|
||||
- `src/activities/reader/{DictionarySuggestionsActivity,DictionaryWordSelectActivity,LookedUpWordsActivity}.cpp`: Fixed `setResult()` rvalue ref binding (6 lambdas)
|
||||
- `src/activities/reader/EpubReaderActivity.cpp`: Fixed `std::max(uint8_t, int)` type mismatch
|
||||
- `src/util/StringUtils.{h,cpp}`: Re-added `checkFileExtension()` and `sortFileList()` functions
|
||||
- `src/RecentBooksStore.{h,cpp}`: Added missing `removeBook()` method
|
||||
|
||||
## Follow-up
|
||||
- Both `mod` and `default` environments build successfully at 95.2% flash usage
|
||||
- No functional testing performed yet (on-device verification needed)
|
||||
42
chat-summaries/2026-03-08_00-30-summary.md
Normal file
42
chat-summaries/2026-03-08_00-30-summary.md
Normal file
@@ -0,0 +1,42 @@
|
||||
# Restore Lost Mod Features Post-Upstream-Sync
|
||||
|
||||
## Task
|
||||
Implemented the full plan to restore 8 mod-specific features lost during the upstream sync, organized into 5 phased commits.
|
||||
|
||||
## Changes Made
|
||||
|
||||
### Phase 0 — Commit Pending Changes (`4627ec9`)
|
||||
- Staged and committed 22 modified files from previous sessions (ADC fix, logging guard, include path fixes, etc.) as a clean baseline.
|
||||
|
||||
### Phase 1 — UI Rendering Fixes (`f44657a`)
|
||||
- **Clock display**: Added clock rendering to `BaseTheme::drawHeader()` and `LyraTheme::drawHeader()` supporting OFF/AM-PM/24H formats and Small/Medium/Large sizes.
|
||||
- **Placeholder covers**: Fixed `splitWords()` in `PlaceholderCoverGenerator.cpp` to treat `\n`, `\r`, `\t` as whitespace (not just spaces). Removed `drawBorder()` call since the UI already draws frames around book cards.
|
||||
|
||||
### Phase 2 — Reader Menu Restructure (`0493f30`)
|
||||
- **Manage Book submenu**: Replaced 4 individual top-level menu items (Archive, Delete, Reindex, Delete Cache) with single "Manage Book" entry that launches `BookManageMenuActivity`.
|
||||
- **Dictionary submenu**: Created new `DictionaryMenuActivity` and replaced 3 scattered dictionary items with single "Dictionary" entry.
|
||||
- **Long-press TOC**: Added 700ms long-press detection on Confirm button to open Table of Contents directly, bypassing the menu.
|
||||
- Added `STR_DICTIONARY` i18n key and regenerated I18nKeys.h/I18nStrings.h.
|
||||
|
||||
### Phase 3 — Settings and Indexing (`22c1892`)
|
||||
- **Letterbox Fill position**: Moved from bottom of settings list to immediately after Sleep Screen Cover Filter.
|
||||
- **Indexing Display setting**: Added to Display section with Popup/Status Bar Text/Status Bar Icon modes.
|
||||
- **Silent indexing**: Restored proactive next-chapter indexing logic and status bar indicator (text or hourglass icon) in `EpubReaderActivity`.
|
||||
|
||||
### Phase 4 — Book Preparation (`a5ca15d`)
|
||||
- **Prerender on first open**: Restored the cover/thumbnail prerender block in `EpubReaderActivity::onEnter()` with "Preparing book..." popup and progress bar.
|
||||
- Added `isValidThumbnailBmp()`, `generateInvalidFormatCoverBmp()`, and `generateInvalidFormatThumbBmp()` methods to `Epub` class.
|
||||
|
||||
## Files Modified
|
||||
- `src/components/themes/BaseTheme.cpp`, `src/components/themes/lyra/LyraTheme.cpp` — clock display
|
||||
- `lib/PlaceholderCover/PlaceholderCoverGenerator.cpp` — whitespace splitting, border removal
|
||||
- `src/activities/reader/EpubReaderMenuActivity.h/.cpp` — menu restructure
|
||||
- `src/activities/reader/DictionaryMenuActivity.h/.cpp` — new submenu (created)
|
||||
- `src/activities/reader/EpubReaderActivity.h/.cpp` — long-press TOC, MANAGE_BOOK/DICTIONARY handlers, silent indexing, book prerender
|
||||
- `src/SettingsList.h` — settings reordering, indexing display entry
|
||||
- `lib/Epub/Epub.h`, `lib/Epub/Epub.cpp` — BMP validation and fallback generation methods
|
||||
- `lib/I18n/translations/english.yaml`, `lib/I18n/I18nKeys.h`, `lib/I18n/I18nStrings.h` — STR_DICTIONARY key
|
||||
|
||||
## Follow-up Items
|
||||
- On-device verification of all 8 features by user
|
||||
- Optional: verify `pio run -e default` still builds cleanly
|
||||
38
chat-summaries/2026-03-08_02-00-summary.md
Normal file
38
chat-summaries/2026-03-08_02-00-summary.md
Normal file
@@ -0,0 +1,38 @@
|
||||
# Fix Reader Bugs: Covers, Indexing, TOC, Cache Deletion
|
||||
|
||||
**Date:** 2026-03-08
|
||||
**Commit:** `022f519` on `mod/master-resync`
|
||||
|
||||
## Task Description
|
||||
|
||||
Fixed four bugs reported after the upstream sync feature restoration:
|
||||
|
||||
1. **Placeholder cover text rendering** — only first letter of each word visible
|
||||
2. **Silent indexing indicator** — wrong timing (shown on first page, not during actual indexing) and icon positioned above the status bar
|
||||
3. **Long-press confirm for TOC** — immediately selected the first chapter upon button release
|
||||
4. **Book cache deletion from home screen** — showed "no open books" and required double confirm press
|
||||
|
||||
## Changes Made
|
||||
|
||||
### lib/PlaceholderCover/PlaceholderCoverGenerator.cpp
|
||||
- Added `#include <EpdFontData.h>` for `fp4::toPixel()`
|
||||
- Fixed `renderGlyph()`: `glyph->advanceX` is 12.4 fixed-point, not pixels — was advancing cursor ~16x too far, causing only the first character of each word to be visible
|
||||
- Fixed `getCharAdvance()`: same fixed-point conversion needed for space width calculation in word wrapping
|
||||
|
||||
### src/activities/reader/EpubReaderActivity.cpp
|
||||
- Removed `silentIndexNextChapterIfNeeded()` call from `loop()` (line 385) — this blocked the UI before render, preventing the indicator from ever showing. The backup branch only called it from `render()`, after the page was drawn.
|
||||
- Fixed indexing icon Y position: changed `textY - kIndexingIconSize + 2` to `textY + 2` to align within the status bar alongside battery/text
|
||||
- Passed `consumeFirstRelease` flag when creating chapter selection activity from long-press path
|
||||
|
||||
### src/activities/reader/EpubReaderChapterSelectionActivity.h/.cpp
|
||||
- Added `ignoreNextConfirmRelease` member and `consumeFirstRelease` constructor parameter
|
||||
- In `loop()`, consumes the first Confirm release when opened via long-press, preventing immediate selection of the first TOC item
|
||||
|
||||
### src/activities/home/HomeActivity.cpp
|
||||
- In `openManageMenu()` callback: reset `ignoreNextConfirmRelease = false` to fix double-press bug
|
||||
- Replaced `recentBooks.clear()` with `loadRecentBooks()` to reload remaining books from persistent store after deletion
|
||||
|
||||
## Follow-up Items
|
||||
|
||||
- Test all four fixes on device to verify correct behavior
|
||||
- Verify placeholder covers render full title/author text at both thumbnail and full-cover sizes
|
||||
62
chat-summaries/2026-03-08_05-01-summary.md
Normal file
62
chat-summaries/2026-03-08_05-01-summary.md
Normal file
@@ -0,0 +1,62 @@
|
||||
# Port 5 Upstream PRs (#1329, #1143, #1172, #1320, #1325)
|
||||
|
||||
**Date:** 2026-03-08
|
||||
**Task:** Port unmerged upstream PRs to the mod across 4 feature branches
|
||||
|
||||
## Summary
|
||||
|
||||
Ported 5 upstream PRs from `crosspoint-reader/crosspoint-reader` to the mod codebase, organized across 4 phased feature branches:
|
||||
|
||||
### Phase 1: PR #1329 — ReaderUtils refactor
|
||||
**Branch:** `port/1329-reader-utils`
|
||||
- Created `src/activities/reader/ReaderUtils.h` with shared reader utilities: `GO_HOME_MS`, `applyOrientation()`, `detectPageTurn()`, `displayWithRefreshCycle()`, `renderAntiAliased()`
|
||||
- Refactored `EpubReaderActivity.cpp` and `TxtReaderActivity.cpp` to use ReaderUtils
|
||||
- Applied CodeRabbit's `storeBwBuffer()` null-check suggestion
|
||||
|
||||
### Phase 2: PRs #1143 + #1172 — TOC fragment navigation + multi-spine TOC
|
||||
**Branch:** `port/1143-1172-toc-navigation`
|
||||
- Extended `Section.h/.cpp` with TOC boundary tracking (`tocBoundaries`, `buildTocBoundaries()`, page lookup methods)
|
||||
- Added TOC anchor page breaks to `ChapterHtmlSlimParser` (chapters start on fresh pages)
|
||||
- Added TOC-aware navigation to `EpubReaderActivity` (long-press walks TOC entries, status bar shows subchapter title)
|
||||
- Updated `EpubReaderChapterSelectionActivity` to pass and accept `tocIndex`
|
||||
- Added multi-spine chapter caching (`cacheMultiSpineChapter()`)
|
||||
- Incremented `SECTION_FILE_VERSION` from 18 to 19
|
||||
- Preserved mod's footnote support, image rendering options, and Activity base class
|
||||
|
||||
### Phase 3: PR #1320 — JPEG resource cleanup
|
||||
**Branch:** `port/1320-jpeg-cleanup`
|
||||
- Added `ScopedCleanup` RAII struct to `JpegToBmpConverter.cpp`
|
||||
- Removed scattered `free()`/`delete` calls
|
||||
- Changed `rowCount` from `uint16_t*` to `uint32_t*` to prevent overflow
|
||||
|
||||
### Phase 4: PR #1325 — Settings tab label
|
||||
**Branch:** `port/1325-settings-label`
|
||||
- Dynamic confirm label in `SettingsActivity.cpp` shows next category name when tab bar is focused
|
||||
|
||||
## Files changed
|
||||
|
||||
| File | Phase | Change type |
|
||||
|------|-------|-------------|
|
||||
| `src/activities/reader/ReaderUtils.h` | 1 | New file |
|
||||
| `src/activities/reader/EpubReaderActivity.cpp` | 1, 2 | Refactored |
|
||||
| `src/activities/reader/EpubReaderActivity.h` | 2 | Extended |
|
||||
| `src/activities/reader/TxtReaderActivity.cpp` | 1 | Refactored |
|
||||
| `lib/Epub/Epub/Section.h` | 2 | Extended |
|
||||
| `lib/Epub/Epub/Section.cpp` | 2 | Extended |
|
||||
| `lib/Epub/Epub/parsers/ChapterHtmlSlimParser.h` | 2 | Extended |
|
||||
| `lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp` | 2 | Extended |
|
||||
| `src/activities/ActivityResult.h` | 2 | Extended |
|
||||
| `src/activities/reader/EpubReaderChapterSelectionActivity.h` | 2 | Extended |
|
||||
| `src/activities/reader/EpubReaderChapterSelectionActivity.cpp` | 2 | Extended |
|
||||
| `lib/JpegToBmpConverter/JpegToBmpConverter.cpp` | 3 | Refactored |
|
||||
| `src/activities/settings/SettingsActivity.cpp` | 4 | Modified |
|
||||
| `mod/prs/MERGED.md` | Housekeeping | Updated |
|
||||
| `mod/docs/upstream-sync.md` | Housekeeping | Updated |
|
||||
|
||||
## Follow-up items
|
||||
|
||||
- All 4 branches need hardware testing before merging to `mod/master`
|
||||
- Test TOC navigation with multi-chapter spine EPUBs (short story collections, academic texts)
|
||||
- Test JPEG resource cleanup with large image-heavy EPUBs
|
||||
- Verify `SECTION_FILE_VERSION` bump invalidates old caches properly (delete `.crosspoint/` on SD card)
|
||||
- When upstream merges these PRs, these ports should be dropped during the next sync
|
||||
31
chat-summaries/2026-03-08_12-00-summary.md
Normal file
31
chat-summaries/2026-03-08_12-00-summary.md
Normal file
@@ -0,0 +1,31 @@
|
||||
# Fix Three Reader Bugs on mod/master
|
||||
|
||||
**Date**: 2026-03-08
|
||||
**Branch**: mod/master
|
||||
**Commit**: 422cad7
|
||||
|
||||
## Task
|
||||
|
||||
Fixed three bugs reported after merging the upstream PR ports:
|
||||
|
||||
## Changes
|
||||
|
||||
### Bug 1: Confirm button ignored after TOC navigation
|
||||
- **File**: `src/activities/reader/EpubReaderActivity.cpp`
|
||||
- **Root cause**: `ignoreNextConfirmRelease` was set to `true` on long-press but never cleared after being transferred to the child `EpubReaderChapterSelectionActivity`. The parent's flag remained `true`, causing the next Confirm press to be silently consumed.
|
||||
- **Fix**: Added `ignoreNextConfirmRelease = false;` immediately after capturing the value into `consumeRelease`.
|
||||
|
||||
### Bug 2: Footnotes inaccessible from reader menu
|
||||
- **File**: `src/activities/reader/EpubReaderMenuActivity.cpp`
|
||||
- **Root cause**: `buildMenuItems()` accepted a `hasFootnotes` parameter but never used it to add the `MenuAction::FOOTNOTES` entry.
|
||||
- **Fix**: Added conditional `FOOTNOTES` menu item after DICTIONARY when `hasFootnotes` is true.
|
||||
|
||||
### Bug 3: Phantom screen re-render at idle
|
||||
- **File**: `src/main.cpp`
|
||||
- **Root cause**: The clock update logic in `loop()` called `activityManager.requestUpdate()` every minute when the displayed minute changed. Reader activities don't show the clock, but still received full page re-renders, causing visible e-ink flashes while idle.
|
||||
- **Fix**: Guarded the clock block with `!activityManager.isReaderActivity()`.
|
||||
|
||||
## Follow-up
|
||||
|
||||
- Hardware testing needed for all three fixes
|
||||
- Bug 3 fix means the clock won't update while reading; it resumes updating when returning to non-reader activities
|
||||
28
chat-summaries/2026-03-08_14-00-summary.md
Normal file
28
chat-summaries/2026-03-08_14-00-summary.md
Normal file
@@ -0,0 +1,28 @@
|
||||
# Restore Preferred Orientation Settings and Long-Press Sub-Menu
|
||||
|
||||
**Date**: 2026-03-08
|
||||
**Branch**: mod/master
|
||||
**Commit**: 0d8a3fd
|
||||
|
||||
## Task
|
||||
|
||||
Restore two orientation preference features lost during the upstream PR resync:
|
||||
1. Settings UI entries for preferred portrait/landscape modes
|
||||
2. Long-press sub-menu on the reader menu's orientation toggle
|
||||
|
||||
## Changes
|
||||
|
||||
### Settings UI and Persistence
|
||||
- **`src/SettingsList.h`**: Added `DynamicEnum` entries for `preferredPortrait` (Portrait/Inverted) and `preferredLandscape` (Landscape CW/Landscape CCW) in the Reader settings category. Uses getter/setter lambdas to map between sequential indices and non-sequential orientation enum values.
|
||||
- **`src/JsonSettingsIO.cpp`**: Added manual JSON save/load for both fields with validation (rejects invalid orientation values, falls back to defaults).
|
||||
|
||||
### Long-Press Orientation Sub-Menu
|
||||
- **`src/activities/reader/EpubReaderMenuActivity.h`**: Added `orientationSubMenuOpen`, `orientationSubMenuIndex`, and `ignoreNextConfirmRelease` state flags.
|
||||
- **`src/activities/reader/EpubReaderMenuActivity.cpp`**:
|
||||
- `loop()`: Long-press (700ms) on Confirm when the orientation item is selected opens the sub-menu. Sub-menu handles its own Up/Down/Confirm/Back input. Added `ignoreNextConfirmRelease` guard to prevent the long-press release from immediately selecting.
|
||||
- `render()`: When sub-menu is open, renders a centered list of all 4 orientations with the current one marked with `*`. Uses the same gutter/hint layout as the main menu.
|
||||
|
||||
## Follow-up
|
||||
|
||||
- Hardware testing needed for both features
|
||||
- Translations for `STR_PREFERRED_PORTRAIT` and `STR_PREFERRED_LANDSCAPE` only exist in English; other languages fall back automatically
|
||||
43
chat-summaries/2026-03-08_17-30-summary.md
Normal file
43
chat-summaries/2026-03-08_17-30-summary.md
Normal file
@@ -0,0 +1,43 @@
|
||||
# Port Upstream PRs #1311, #1322 + Verify #1329
|
||||
|
||||
**Date:** 2026-03-08
|
||||
**Task:** Port two upstream PRs and verify alignment of a previously-ported PR that was recently merged.
|
||||
|
||||
## Changes Made
|
||||
|
||||
### PR #1311 -- Fix inter-word spacing rounding error (UNMERGED, ported as mod feature)
|
||||
|
||||
Replaced `getSpaceKernAdjust()` with `getSpaceAdvance()` which combines space glyph advance and flanking kern values into a single fixed-point sum before pixel snapping, fixing +/-1 px rounding drift in inter-word spacing.
|
||||
|
||||
**Files modified:**
|
||||
- `lib/GfxRenderer/GfxRenderer.h` -- replaced declaration
|
||||
- `lib/GfxRenderer/GfxRenderer.cpp` -- replaced implementation (single-snap pattern)
|
||||
- `lib/Epub/Epub/ParsedText.h` -- removed `spaceWidth` parameter from 3 internal functions
|
||||
- `lib/Epub/Epub/ParsedText.cpp` -- updated all 4 call sites to use `getSpaceAdvance()`
|
||||
|
||||
### PR #1322 -- Early exit on fillUncompressedSizes (MERGED, ported for immediate use)
|
||||
|
||||
Added `targetCount` variable and early `break` when all ZIP central-directory targets are matched.
|
||||
|
||||
**Files modified:**
|
||||
- `lib/ZipFile/ZipFile.cpp` -- 5-line addition
|
||||
|
||||
### PR #1329 -- Reader utils refactor (MERGED, verification only)
|
||||
|
||||
Confirmed our existing port matches the upstream merged version (commit `cd508d2`) line-for-line. No code changes needed.
|
||||
|
||||
### Tracking documentation updated
|
||||
|
||||
- `mod/docs/upstream-sync.md` -- added #1311, #1322; updated #1329 status to MERGED
|
||||
- `mod/prs/MERGED.md` -- added detailed entries for #1311 and #1322; updated #1329 author and status
|
||||
|
||||
## Build Result
|
||||
|
||||
SUCCESS -- zero compiler errors/warnings from our changes. Only pre-existing i18n translation warnings.
|
||||
|
||||
## Follow-up Items
|
||||
|
||||
- #1311: Will be dropped during next sync if/when merged upstream
|
||||
- #1322: Will be dropped during next sync (already merged upstream)
|
||||
- #1329: Will be dropped during next sync (already merged upstream)
|
||||
- Hardware testing recommended: verify text layout rendering after spacing fix (#1311)
|
||||
99
docs/contributing/koreader-sync-xpath-mapping.md
Normal file
99
docs/contributing/koreader-sync-xpath-mapping.md
Normal file
@@ -0,0 +1,99 @@
|
||||
# KOReader Sync XPath Mapping
|
||||
|
||||
This note documents how CrossPoint maps reading positions to and from KOReader sync payloads.
|
||||
|
||||
## Problem
|
||||
|
||||
CrossPoint internally stores position as:
|
||||
|
||||
- `spineIndex` (chapter index, 0-based)
|
||||
- `pageNumber` + `totalPages`
|
||||
|
||||
KOReader sync payload stores:
|
||||
|
||||
- `progress` (XPath-like location)
|
||||
- `percentage` (overall progress)
|
||||
|
||||
A direct 1:1 mapping is not guaranteed because page layout differs between engines/devices.
|
||||
|
||||
## DocFragment Index Convention
|
||||
|
||||
KOReader uses **1-based** XPath predicates throughout, following standard XPath conventions.
|
||||
The first EPUB spine item is `DocFragment[1]`, the second is `DocFragment[2]`, and so on.
|
||||
|
||||
CrossPoint stores spine items as 0-based indices internally. The conversion is:
|
||||
|
||||
- **Generating XPath (to KOReader):** `DocFragment[spineIndex + 1]`
|
||||
- **Parsing XPath (from KOReader):** `spineIndex = DocFragment[N] - 1`
|
||||
|
||||
Reference: [koreader/koreader#11585](https://github.com/koreader/koreader/issues/11585) confirms this
|
||||
via a KOReader contributor mapping spine items to DocFragment numbers.
|
||||
|
||||
## Current Strategy
|
||||
|
||||
### CrossPoint -> KOReader
|
||||
|
||||
Implemented in `ProgressMapper::toKOReader`.
|
||||
|
||||
1. Compute overall `percentage` from chapter/page.
|
||||
2. Attempt to compute a real element-level XPath via `ChapterXPathIndexer::findXPathForProgress`.
|
||||
3. If XPath extraction fails, fallback to synthetic chapter path:
|
||||
- `/body/DocFragment[spineIndex + 1]/body`
|
||||
|
||||
### KOReader -> CrossPoint
|
||||
|
||||
Implemented in `ProgressMapper::toCrossPoint`.
|
||||
|
||||
1. Attempt to parse `DocFragment[N]` from incoming XPath; convert N to 0-based `spineIndex = N - 1`.
|
||||
2. If valid, attempt XPath-to-offset mapping via `ChapterXPathIndexer::findProgressForXPath`.
|
||||
3. Convert resolved intra-spine progress to page estimate.
|
||||
4. If XPath path is invalid/unresolvable, fallback to percentage-based chapter/page estimation.
|
||||
|
||||
## ChapterXPathIndexer Design
|
||||
|
||||
The module reparses **one spine XHTML** on demand using Expat and builds temporary anchors:
|
||||
|
||||
Source-of-truth note: XPath anchors are built from the original EPUB spine XHTML bytes (zip item contents), not from CrossPoint's distilled section render cache. This is intentional to preserve KOReader XPath compatibility.
|
||||
|
||||
- anchor: `<xpath, textOffset>`
|
||||
- `textOffset` counts non-whitespace bytes
|
||||
- When multiple anchors exist for the same path, the one with the **smallest** textOffset is used
|
||||
(start of element), not the latest periodic anchor.
|
||||
|
||||
Forward lookup (CrossPoint → XPath): uses `upper_bound` to find the last anchor at or before the
|
||||
target text offset, ensuring the returned XPath corresponds to the element the user is currently
|
||||
inside rather than the next element.
|
||||
|
||||
Matching for reverse lookup:
|
||||
|
||||
1. exact path match — reported as `exact=yes`
|
||||
2. index-insensitive path match (`div[2]` vs `div[3]` tolerated) — reported as `exact=no`
|
||||
3. ancestor fallback — reported as `exact=no`
|
||||
|
||||
If no match is found, caller must fallback to percentage.
|
||||
|
||||
## Memory / Safety Constraints (ESP32-C3)
|
||||
|
||||
The implementation intentionally avoids full DOM storage.
|
||||
|
||||
- Parse one chapter only.
|
||||
- Keep anchors in transient vectors only for duration of call.
|
||||
- Free XML parser and chapter byte buffer on all success/failure paths.
|
||||
- No persistent cache structures are introduced by this module.
|
||||
|
||||
## Known Limitations
|
||||
|
||||
- Page number on reverse mapping is still an estimate (renderer differences).
|
||||
- XPath mapping intentionally uses original spine XHTML while pagination comes from distilled renderer output, so minor roundtrip page drift is expected.
|
||||
- Image-only/low-text chapters may yield coarse anchors.
|
||||
- Extremely malformed XHTML can force fallback behavior.
|
||||
|
||||
## Operational Logging
|
||||
|
||||
`ProgressMapper` logs mapping source in reverse direction:
|
||||
|
||||
- `xpath` when XPath mapping path was used
|
||||
- `percentage` when fallback path was used
|
||||
|
||||
It also logs exactness (`exact=yes/no`) for XPath matches. Note that `exact=yes` is only set for
|
||||
a full path match with correct indices; index-insensitive and ancestor matches always log `exact=no`.
|
||||
@@ -15,6 +15,7 @@ This guide explains the multi-language support system in CrossPoint Reader.
|
||||
- Ukrainian
|
||||
- Polish
|
||||
- Danish
|
||||
- Turkish
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Translators
|
||||
|
||||
Below is a list of users and languages CrossPoint may support in the future.
|
||||
Below is a list of users and languages CrossPoint may support in the future.
|
||||
Note because a language is below does not mean there is official support for the language at this time.
|
||||
|
||||
## Contributing
|
||||
@@ -35,9 +35,11 @@ If you'd like to add your name to this list, please open a PR adding yourself an
|
||||
- [yeyeto2788](https://github.com/yeyeto2788)
|
||||
- [Skrzakk](https://github.com/Skrzakk)
|
||||
- [pablohc](https://github.com/pablohc)
|
||||
- [DaniPhii](https://github.com/DaniPhii)
|
||||
|
||||
## Swedish
|
||||
- [dawiik](https://github.com/dawiik)
|
||||
- [steka](https://github.com/steka)
|
||||
|
||||
## Romanian
|
||||
- [ariel-lindemann](https://github.com/ariel-lindemann)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#include "Epub.h"
|
||||
|
||||
#include <FsHelpers.h>
|
||||
#include <HalDisplay.h>
|
||||
#include <HalStorage.h>
|
||||
#include <JpegToBmpConverter.h>
|
||||
#include <Logging.h>
|
||||
@@ -103,14 +104,11 @@ bool Epub::parseContentOpf(BookMetadataCache::BookMetadata& bookMetadata) {
|
||||
pos += strlen(pattern);
|
||||
const auto endPos = coverPageHtml.find('"', pos);
|
||||
if (endPos != std::string::npos) {
|
||||
const auto ref = coverPageHtml.substr(pos, endPos - pos);
|
||||
const auto ref = std::string_view{coverPageHtml}.substr(pos, endPos - pos);
|
||||
// Check if it's an image file
|
||||
if (ref.length() >= 4) {
|
||||
const auto ext = ref.substr(ref.length() - 4);
|
||||
if (ext == ".png" || ext == ".jpg" || ext == "jpeg" || ext == ".gif") {
|
||||
imageRef = ref;
|
||||
break;
|
||||
}
|
||||
if (FsHelpers::hasPngExtension(ref) || FsHelpers::hasJpgExtension(ref) || FsHelpers::hasGifExtension(ref)) {
|
||||
imageRef = ref;
|
||||
break;
|
||||
}
|
||||
}
|
||||
pos = coverPageHtml.find(pattern, pos);
|
||||
@@ -541,8 +539,7 @@ bool Epub::generateCoverBmp(bool cropped) const {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (coverImageHref.substr(coverImageHref.length() - 4) == ".jpg" ||
|
||||
coverImageHref.substr(coverImageHref.length() - 5) == ".jpeg") {
|
||||
if (FsHelpers::hasJpgExtension(coverImageHref)) {
|
||||
LOG_DBG("EBP", "Generating BMP from JPG cover image (%s mode)", cropped ? "cropped" : "fit");
|
||||
const auto coverJpgTempPath = getCachePath() + "/.cover.jpg";
|
||||
|
||||
@@ -575,7 +572,7 @@ bool Epub::generateCoverBmp(bool cropped) const {
|
||||
return success;
|
||||
}
|
||||
|
||||
if (coverImageHref.substr(coverImageHref.length() - 4) == ".png") {
|
||||
if (FsHelpers::hasPngExtension(coverImageHref)) {
|
||||
LOG_DBG("EBP", "Generating BMP from PNG cover image (%s mode)", cropped ? "cropped" : "fit");
|
||||
const auto coverPngTempPath = getCachePath() + "/.cover.png";
|
||||
|
||||
@@ -629,8 +626,7 @@ bool Epub::generateThumbBmp(int height) const {
|
||||
const auto coverImageHref = bookMetadataCache->coreMetadata.coverItemHref;
|
||||
if (coverImageHref.empty()) {
|
||||
LOG_DBG("EBP", "No known cover image for thumbnail");
|
||||
} else if (coverImageHref.substr(coverImageHref.length() - 4) == ".jpg" ||
|
||||
coverImageHref.substr(coverImageHref.length() - 5) == ".jpeg") {
|
||||
} else if (FsHelpers::hasJpgExtension(coverImageHref)) {
|
||||
LOG_DBG("EBP", "Generating thumb BMP from JPG cover image");
|
||||
const auto coverJpgTempPath = getCachePath() + "/.cover.jpg";
|
||||
|
||||
@@ -666,7 +662,7 @@ bool Epub::generateThumbBmp(int height) const {
|
||||
}
|
||||
LOG_DBG("EBP", "Generated thumb BMP from JPG cover image, success: %s", success ? "yes" : "no");
|
||||
return success;
|
||||
} else if (coverImageHref.substr(coverImageHref.length() - 4) == ".png") {
|
||||
} else if (FsHelpers::hasPngExtension(coverImageHref)) {
|
||||
LOG_DBG("EBP", "Generating thumb BMP from PNG cover image");
|
||||
const auto coverPngTempPath = getCachePath() + "/.cover.png";
|
||||
|
||||
@@ -711,6 +707,171 @@ bool Epub::generateThumbBmp(int height) const {
|
||||
return false;
|
||||
}
|
||||
|
||||
bool Epub::isValidThumbnailBmp(const std::string& bmpPath) {
|
||||
if (!Storage.exists(bmpPath.c_str())) {
|
||||
return false;
|
||||
}
|
||||
FsFile file = Storage.open(bmpPath.c_str());
|
||||
if (!file) {
|
||||
LOG_ERR("EBP", "Failed to open thumbnail BMP at path: %s", bmpPath.c_str());
|
||||
return false;
|
||||
}
|
||||
size_t fileSize = file.size();
|
||||
if (fileSize == 0) {
|
||||
LOG_DBG("EBP", "Thumbnail BMP is empty (no cover marker) at path: %s", bmpPath.c_str());
|
||||
file.close();
|
||||
return false;
|
||||
}
|
||||
uint8_t header[2];
|
||||
size_t bytesRead = file.read(header, 2);
|
||||
if (bytesRead != 2) {
|
||||
LOG_ERR("EBP", "Failed to read thumbnail BMP header at path: %s", bmpPath.c_str());
|
||||
file.close();
|
||||
return false;
|
||||
}
|
||||
file.close();
|
||||
return header[0] == 'B' && header[1] == 'M';
|
||||
}
|
||||
|
||||
bool Epub::generateInvalidFormatThumbBmp(int height) const {
|
||||
const int width = height * 0.6;
|
||||
const int rowBytes = ((width + 31) / 32) * 4;
|
||||
const int imageSize = rowBytes * height;
|
||||
const int fileSize = 14 + 40 + 8 + imageSize;
|
||||
const int dataOffset = 14 + 40 + 8;
|
||||
|
||||
FsFile thumbBmp;
|
||||
if (!Storage.openFileForWrite("EBP", getThumbBmpPath(height), thumbBmp)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
thumbBmp.write('B');
|
||||
thumbBmp.write('M');
|
||||
thumbBmp.write(reinterpret_cast<const uint8_t*>(&fileSize), 4);
|
||||
uint32_t reserved = 0;
|
||||
thumbBmp.write(reinterpret_cast<const uint8_t*>(&reserved), 4);
|
||||
thumbBmp.write(reinterpret_cast<const uint8_t*>(&dataOffset), 4);
|
||||
|
||||
uint32_t dibHeaderSize = 40;
|
||||
thumbBmp.write(reinterpret_cast<const uint8_t*>(&dibHeaderSize), 4);
|
||||
int32_t bmpWidth = width;
|
||||
thumbBmp.write(reinterpret_cast<const uint8_t*>(&bmpWidth), 4);
|
||||
int32_t bmpHeight = -height;
|
||||
thumbBmp.write(reinterpret_cast<const uint8_t*>(&bmpHeight), 4);
|
||||
uint16_t planes = 1;
|
||||
thumbBmp.write(reinterpret_cast<const uint8_t*>(&planes), 2);
|
||||
uint16_t bitsPerPixel = 1;
|
||||
thumbBmp.write(reinterpret_cast<const uint8_t*>(&bitsPerPixel), 2);
|
||||
uint32_t compression = 0;
|
||||
thumbBmp.write(reinterpret_cast<const uint8_t*>(&compression), 4);
|
||||
thumbBmp.write(reinterpret_cast<const uint8_t*>(&imageSize), 4);
|
||||
int32_t ppmX = 2835;
|
||||
thumbBmp.write(reinterpret_cast<const uint8_t*>(&ppmX), 4);
|
||||
int32_t ppmY = 2835;
|
||||
thumbBmp.write(reinterpret_cast<const uint8_t*>(&ppmY), 4);
|
||||
uint32_t colorsUsed = 2;
|
||||
thumbBmp.write(reinterpret_cast<const uint8_t*>(&colorsUsed), 4);
|
||||
uint32_t colorsImportant = 2;
|
||||
thumbBmp.write(reinterpret_cast<const uint8_t*>(&colorsImportant), 4);
|
||||
|
||||
uint8_t black[4] = {0x00, 0x00, 0x00, 0x00};
|
||||
thumbBmp.write(black, 4);
|
||||
uint8_t white[4] = {0xFF, 0xFF, 0xFF, 0x00};
|
||||
thumbBmp.write(white, 4);
|
||||
|
||||
for (int y = 0; y < height; y++) {
|
||||
std::vector<uint8_t> rowData(rowBytes, 0xFF);
|
||||
const int scaledY = (y * width) / height;
|
||||
const int thickness = 2;
|
||||
for (int x = 0; x < width; x++) {
|
||||
bool drawPixel = false;
|
||||
if (std::abs(x - scaledY) <= thickness) drawPixel = true;
|
||||
if (std::abs(x - (width - 1 - scaledY)) <= thickness) drawPixel = true;
|
||||
if (drawPixel) {
|
||||
const int byteIndex = x / 8;
|
||||
const int bitIndex = 7 - (x % 8);
|
||||
rowData[byteIndex] &= static_cast<uint8_t>(~(1 << bitIndex));
|
||||
}
|
||||
}
|
||||
thumbBmp.write(rowData.data(), rowBytes);
|
||||
}
|
||||
|
||||
thumbBmp.close();
|
||||
LOG_DBG("EBP", "Generated invalid format thumbnail BMP");
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Epub::generateInvalidFormatCoverBmp(bool cropped) const {
|
||||
const int hwW = static_cast<int>(HalDisplay::DISPLAY_WIDTH);
|
||||
const int hwH = static_cast<int>(HalDisplay::DISPLAY_HEIGHT);
|
||||
const int width = std::min(hwW, hwH);
|
||||
const int height = std::max(hwW, hwH);
|
||||
const int rowBytes = ((width + 31) / 32) * 4;
|
||||
const int imageSize = rowBytes * height;
|
||||
const int fileSize = 14 + 40 + 8 + imageSize;
|
||||
const int dataOffset = 14 + 40 + 8;
|
||||
|
||||
FsFile coverBmp;
|
||||
if (!Storage.openFileForWrite("EBP", getCoverBmpPath(cropped), coverBmp)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
coverBmp.write('B');
|
||||
coverBmp.write('M');
|
||||
coverBmp.write(reinterpret_cast<const uint8_t*>(&fileSize), 4);
|
||||
uint32_t reserved = 0;
|
||||
coverBmp.write(reinterpret_cast<const uint8_t*>(&reserved), 4);
|
||||
coverBmp.write(reinterpret_cast<const uint8_t*>(&dataOffset), 4);
|
||||
|
||||
uint32_t dibHeaderSize = 40;
|
||||
coverBmp.write(reinterpret_cast<const uint8_t*>(&dibHeaderSize), 4);
|
||||
int32_t bmpWidth = width;
|
||||
coverBmp.write(reinterpret_cast<const uint8_t*>(&bmpWidth), 4);
|
||||
int32_t bmpHeight = -height;
|
||||
coverBmp.write(reinterpret_cast<const uint8_t*>(&bmpHeight), 4);
|
||||
uint16_t planes = 1;
|
||||
coverBmp.write(reinterpret_cast<const uint8_t*>(&planes), 2);
|
||||
uint16_t bitsPerPixel = 1;
|
||||
coverBmp.write(reinterpret_cast<const uint8_t*>(&bitsPerPixel), 2);
|
||||
uint32_t compression = 0;
|
||||
coverBmp.write(reinterpret_cast<const uint8_t*>(&compression), 4);
|
||||
coverBmp.write(reinterpret_cast<const uint8_t*>(&imageSize), 4);
|
||||
int32_t ppmX = 2835;
|
||||
coverBmp.write(reinterpret_cast<const uint8_t*>(&ppmX), 4);
|
||||
int32_t ppmY = 2835;
|
||||
coverBmp.write(reinterpret_cast<const uint8_t*>(&ppmY), 4);
|
||||
uint32_t colorsUsed = 2;
|
||||
coverBmp.write(reinterpret_cast<const uint8_t*>(&colorsUsed), 4);
|
||||
uint32_t colorsImportant = 2;
|
||||
coverBmp.write(reinterpret_cast<const uint8_t*>(&colorsImportant), 4);
|
||||
|
||||
uint8_t black[4] = {0x00, 0x00, 0x00, 0x00};
|
||||
coverBmp.write(black, 4);
|
||||
uint8_t white[4] = {0xFF, 0xFF, 0xFF, 0x00};
|
||||
coverBmp.write(white, 4);
|
||||
|
||||
for (int y = 0; y < height; y++) {
|
||||
std::vector<uint8_t> rowData(rowBytes, 0xFF);
|
||||
const int scaledY = (y * width) / height;
|
||||
const int thickness = 6;
|
||||
for (int x = 0; x < width; x++) {
|
||||
bool drawPixel = false;
|
||||
if (std::abs(x - scaledY) <= thickness) drawPixel = true;
|
||||
if (std::abs(x - (width - 1 - scaledY)) <= thickness) drawPixel = true;
|
||||
if (drawPixel) {
|
||||
const int byteIndex = x / 8;
|
||||
const int bitIndex = 7 - (x % 8);
|
||||
rowData[byteIndex] &= static_cast<uint8_t>(~(1 << bitIndex));
|
||||
}
|
||||
}
|
||||
coverBmp.write(rowData.data(), rowBytes);
|
||||
}
|
||||
|
||||
coverBmp.close();
|
||||
LOG_DBG("EBP", "Generated invalid format cover BMP");
|
||||
return true;
|
||||
}
|
||||
|
||||
uint8_t* Epub::readItemContentsToBytes(const std::string& itemHref, size_t* size, const bool trailingNullByte) const {
|
||||
if (itemHref.empty()) {
|
||||
LOG_DBG("EBP", "Failed to read item, empty href");
|
||||
|
||||
@@ -56,6 +56,9 @@ class Epub {
|
||||
std::string getThumbBmpPath() const;
|
||||
std::string getThumbBmpPath(int height) const;
|
||||
bool generateThumbBmp(int height) const;
|
||||
bool generateInvalidFormatCoverBmp(bool cropped = false) const;
|
||||
bool generateInvalidFormatThumbBmp(int height) const;
|
||||
static bool isValidThumbnailBmp(const std::string& bmpPath);
|
||||
uint8_t* readItemContentsToBytes(const std::string& itemHref, size_t* size = nullptr,
|
||||
bool trailingNullByte = false) const;
|
||||
bool readItemContentsToStream(const std::string& itemHref, Print& out, size_t chunkSize) const;
|
||||
|
||||
@@ -274,11 +274,13 @@ bool BookMetadataCache::buildBookBin(const std::string& epubPath, const BookMeta
|
||||
}
|
||||
|
||||
bool BookMetadataCache::cleanupTmpFiles() const {
|
||||
if (Storage.exists((cachePath + tmpSpineBinFile).c_str())) {
|
||||
Storage.remove((cachePath + tmpSpineBinFile).c_str());
|
||||
const auto spineBinFile = cachePath + tmpSpineBinFile;
|
||||
if (Storage.exists(spineBinFile.c_str())) {
|
||||
Storage.remove(spineBinFile.c_str());
|
||||
}
|
||||
if (Storage.exists((cachePath + tmpTocBinFile).c_str())) {
|
||||
Storage.remove((cachePath + tmpTocBinFile).c_str());
|
||||
const auto tocBinFile = cachePath + tmpTocBinFile;
|
||||
if (Storage.exists(tocBinFile.c_str())) {
|
||||
Storage.remove(tocBinFile.c_str());
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <cstring>
|
||||
#include <functional>
|
||||
#include <limits>
|
||||
#include <vector>
|
||||
@@ -74,6 +75,80 @@ uint16_t measureWordWidth(const GfxRenderer& renderer, const int fontId, const s
|
||||
return renderer.getTextAdvanceX(fontId, sanitized.c_str(), style);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Direct-mapped word-width cache
|
||||
//
|
||||
// Avoids redundant getTextAdvanceX calls when the same (word, style, fontId)
|
||||
// triple appears across paragraphs. A fixed-size static array is used so
|
||||
// that heap allocation and fragmentation are both zero.
|
||||
//
|
||||
// Eviction policy: hash-direct mapping — a word always occupies the single
|
||||
// slot determined by its hash; a collision simply overwrites that slot.
|
||||
// This gives O(1) lookup (one hash + one memcmp) regardless of how full the
|
||||
// cache is, avoiding the O(n) linear-scan overhead that causes a regression
|
||||
// on corpora with many unique words (e.g. German compound-heavy text).
|
||||
//
|
||||
// Words longer than 23 bytes bypass the cache entirely — they are uncommon,
|
||||
// unlikely to repeat verbatim, and exceed the fixed-width key buffer.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
struct WordWidthCacheEntry {
|
||||
char word[24]; // NUL-terminated; 23 usable bytes + terminator
|
||||
int fontId;
|
||||
uint16_t width;
|
||||
uint8_t style; // EpdFontFamily::Style narrowed to one byte
|
||||
bool valid; // false = slot empty (BSS-initialised to 0)
|
||||
};
|
||||
|
||||
// Power-of-two size → slot selection via fast bitmask AND.
|
||||
// 128 entries × 32 bytes = 4 KB in BSS; covers typical paragraph vocabulary
|
||||
// with a low collision rate even for German compound-heavy prose.
|
||||
static constexpr uint32_t WORD_WIDTH_CACHE_SIZE = 128;
|
||||
static constexpr uint32_t WORD_WIDTH_CACHE_MASK = WORD_WIDTH_CACHE_SIZE - 1;
|
||||
static WordWidthCacheEntry s_wordWidthCache[WORD_WIDTH_CACHE_SIZE];
|
||||
|
||||
// FNV-1a over the word bytes, then XOR-folded with fontId and style.
|
||||
static uint32_t wordWidthCacheHash(const char* str, const size_t len, const int fontId, const uint8_t style) {
|
||||
uint32_t h = 2166136261u; // FNV offset basis
|
||||
for (size_t i = 0; i < len; ++i) {
|
||||
h ^= static_cast<uint8_t>(str[i]);
|
||||
h *= 16777619u; // FNV prime
|
||||
}
|
||||
h ^= static_cast<uint32_t>(fontId);
|
||||
h *= 16777619u;
|
||||
h ^= style;
|
||||
return h;
|
||||
}
|
||||
|
||||
// Returns the cached width for (word, style, fontId), measuring and caching
|
||||
// on a miss. Appending a hyphen is not supported — those measurements are
|
||||
// word-fragment lookups that will not repeat and must not pollute the cache.
|
||||
static uint16_t cachedMeasureWordWidth(const GfxRenderer& renderer, const int fontId, const std::string& word,
|
||||
const EpdFontFamily::Style style) {
|
||||
const size_t len = word.size();
|
||||
if (len >= 24) {
|
||||
return measureWordWidth(renderer, fontId, word, style);
|
||||
}
|
||||
|
||||
const uint8_t styleByte = static_cast<uint8_t>(style);
|
||||
const char* const wordCStr = word.c_str();
|
||||
|
||||
const uint32_t slot = wordWidthCacheHash(wordCStr, len, fontId, styleByte) & WORD_WIDTH_CACHE_MASK;
|
||||
auto& e = s_wordWidthCache[slot];
|
||||
|
||||
if (e.valid && e.fontId == fontId && e.style == styleByte && memcmp(e.word, wordCStr, len + 1) == 0) {
|
||||
return e.width; // O(1) cache hit
|
||||
}
|
||||
|
||||
const uint16_t w = measureWordWidth(renderer, fontId, word, style);
|
||||
memcpy(e.word, wordCStr, len + 1);
|
||||
e.fontId = fontId;
|
||||
e.width = w;
|
||||
e.style = styleByte;
|
||||
e.valid = true;
|
||||
return w;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void ParsedText::addWord(std::string word, const EpdFontFamily::Style fontStyle, const bool underline,
|
||||
@@ -101,20 +176,18 @@ void ParsedText::layoutAndExtractLines(const GfxRenderer& renderer, const int fo
|
||||
applyParagraphIndent();
|
||||
|
||||
const int pageWidth = viewportWidth;
|
||||
const int spaceWidth = renderer.getSpaceWidth(fontId, EpdFontFamily::REGULAR);
|
||||
auto wordWidths = calculateWordWidths(renderer, fontId);
|
||||
|
||||
std::vector<size_t> lineBreakIndices;
|
||||
if (hyphenationEnabled) {
|
||||
// Use greedy layout that can split words mid-loop when a hyphenated prefix fits.
|
||||
lineBreakIndices = computeHyphenatedLineBreaks(renderer, fontId, pageWidth, spaceWidth, wordWidths, wordContinues);
|
||||
lineBreakIndices = computeHyphenatedLineBreaks(renderer, fontId, pageWidth, wordWidths, wordContinues);
|
||||
} else {
|
||||
lineBreakIndices = computeLineBreaks(renderer, fontId, pageWidth, spaceWidth, wordWidths, wordContinues);
|
||||
lineBreakIndices = computeLineBreaks(renderer, fontId, pageWidth, wordWidths, wordContinues);
|
||||
}
|
||||
const size_t lineCount = includeLastLine ? lineBreakIndices.size() : lineBreakIndices.size() - 1;
|
||||
|
||||
for (size_t i = 0; i < lineCount; ++i) {
|
||||
extractLine(i, pageWidth, spaceWidth, wordWidths, wordContinues, lineBreakIndices, processLine, renderer, fontId);
|
||||
extractLine(i, pageWidth, wordWidths, wordContinues, lineBreakIndices, processLine, renderer, fontId);
|
||||
}
|
||||
|
||||
// Remove consumed words so size() reflects only remaining words
|
||||
@@ -131,22 +204,25 @@ std::vector<uint16_t> ParsedText::calculateWordWidths(const GfxRenderer& rendere
|
||||
wordWidths.reserve(words.size());
|
||||
|
||||
for (size_t i = 0; i < words.size(); ++i) {
|
||||
wordWidths.push_back(measureWordWidth(renderer, fontId, words[i], wordStyles[i]));
|
||||
wordWidths.push_back(cachedMeasureWordWidth(renderer, fontId, words[i], wordStyles[i]));
|
||||
}
|
||||
|
||||
return wordWidths;
|
||||
}
|
||||
|
||||
std::vector<size_t> ParsedText::computeLineBreaks(const GfxRenderer& renderer, const int fontId, const int pageWidth,
|
||||
const int spaceWidth, std::vector<uint16_t>& wordWidths,
|
||||
std::vector<uint16_t>& wordWidths,
|
||||
std::vector<bool>& continuesVec) {
|
||||
if (words.empty()) {
|
||||
return {};
|
||||
}
|
||||
|
||||
// Calculate first line indent (only for left/justified text without extra paragraph spacing)
|
||||
// Calculate first line indent (only for left/justified text).
|
||||
// Positive text-indent (paragraph indent) is suppressed when extraParagraphSpacing is on.
|
||||
// Negative text-indent (hanging indent, e.g. margin-left:3em; text-indent:-1em) always applies —
|
||||
// it is structural (positions the bullet/marker), not decorative.
|
||||
const int firstLineIndent =
|
||||
blockStyle.textIndent > 0 && !extraParagraphSpacing &&
|
||||
blockStyle.textIndentDefined && (blockStyle.textIndent < 0 || !extraParagraphSpacing) &&
|
||||
(blockStyle.alignment == CssTextAlign::Justify || blockStyle.alignment == CssTextAlign::Left)
|
||||
? blockStyle.textIndent
|
||||
: 0;
|
||||
@@ -184,9 +260,8 @@ std::vector<size_t> ParsedText::computeLineBreaks(const GfxRenderer& renderer, c
|
||||
// Add space before word j, unless it's the first word on the line or a continuation
|
||||
int gap = 0;
|
||||
if (j > static_cast<size_t>(i) && !continuesVec[j]) {
|
||||
gap = spaceWidth;
|
||||
gap += renderer.getSpaceKernAdjust(fontId, lastCodepoint(words[j - 1]), firstCodepoint(words[j]),
|
||||
wordStyles[j - 1]);
|
||||
gap = renderer.getSpaceAdvance(fontId, lastCodepoint(words[j - 1]), firstCodepoint(words[j]),
|
||||
wordStyles[j - 1]);
|
||||
} else if (j > static_cast<size_t>(i) && continuesVec[j]) {
|
||||
// Cross-boundary kerning for continuation words (e.g. nonbreaking spaces, attached punctuation)
|
||||
gap = renderer.getKerning(fontId, lastCodepoint(words[j - 1]), firstCodepoint(words[j]), wordStyles[j - 1]);
|
||||
@@ -238,6 +313,7 @@ std::vector<size_t> ParsedText::computeLineBreaks(const GfxRenderer& renderer, c
|
||||
|
||||
// Stores the index of the word that starts the next line (last_word_index + 1)
|
||||
std::vector<size_t> lineBreakIndices;
|
||||
lineBreakIndices.reserve(totalWordCount / 8 + 1);
|
||||
size_t currentWordIndex = 0;
|
||||
|
||||
while (currentWordIndex < totalWordCount) {
|
||||
@@ -272,12 +348,15 @@ void ParsedText::applyParagraphIndent() {
|
||||
|
||||
// Builds break indices while opportunistically splitting the word that would overflow the current line.
|
||||
std::vector<size_t> ParsedText::computeHyphenatedLineBreaks(const GfxRenderer& renderer, const int fontId,
|
||||
const int pageWidth, const int spaceWidth,
|
||||
const int pageWidth,
|
||||
std::vector<uint16_t>& wordWidths,
|
||||
std::vector<bool>& continuesVec) {
|
||||
// Calculate first line indent (only for left/justified text without extra paragraph spacing)
|
||||
// Calculate first line indent (only for left/justified text).
|
||||
// Positive text-indent (paragraph indent) is suppressed when extraParagraphSpacing is on.
|
||||
// Negative text-indent (hanging indent, e.g. margin-left:3em; text-indent:-1em) always applies —
|
||||
// it is structural (positions the bullet/marker), not decorative.
|
||||
const int firstLineIndent =
|
||||
blockStyle.textIndent > 0 && !extraParagraphSpacing &&
|
||||
blockStyle.textIndentDefined && (blockStyle.textIndent < 0 || !extraParagraphSpacing) &&
|
||||
(blockStyle.alignment == CssTextAlign::Justify || blockStyle.alignment == CssTextAlign::Left)
|
||||
? blockStyle.textIndent
|
||||
: 0;
|
||||
@@ -298,9 +377,8 @@ std::vector<size_t> ParsedText::computeHyphenatedLineBreaks(const GfxRenderer& r
|
||||
const bool isFirstWord = currentIndex == lineStart;
|
||||
int spacing = 0;
|
||||
if (!isFirstWord && !continuesVec[currentIndex]) {
|
||||
spacing = spaceWidth;
|
||||
spacing += renderer.getSpaceKernAdjust(fontId, lastCodepoint(words[currentIndex - 1]),
|
||||
firstCodepoint(words[currentIndex]), wordStyles[currentIndex - 1]);
|
||||
spacing = renderer.getSpaceAdvance(fontId, lastCodepoint(words[currentIndex - 1]),
|
||||
firstCodepoint(words[currentIndex]), wordStyles[currentIndex - 1]);
|
||||
} else if (!isFirstWord && continuesVec[currentIndex]) {
|
||||
// Cross-boundary kerning for continuation words (e.g. nonbreaking spaces, attached punctuation)
|
||||
spacing = renderer.getKerning(fontId, lastCodepoint(words[currentIndex - 1]),
|
||||
@@ -370,8 +448,11 @@ bool ParsedText::hyphenateWordAtIndex(const size_t wordIndex, const int availabl
|
||||
size_t chosenOffset = 0;
|
||||
int chosenWidth = -1;
|
||||
bool chosenNeedsHyphen = true;
|
||||
std::string prefix;
|
||||
prefix.reserve(word.size());
|
||||
|
||||
// Iterate over each legal breakpoint and retain the widest prefix that still fits.
|
||||
// Breakpoints are in ascending order, so once a prefix is too wide, all subsequent ones will be too.
|
||||
for (const auto& info : breakInfos) {
|
||||
const size_t offset = info.byteOffset;
|
||||
if (offset == 0 || offset >= word.size()) {
|
||||
@@ -379,9 +460,13 @@ bool ParsedText::hyphenateWordAtIndex(const size_t wordIndex, const int availabl
|
||||
}
|
||||
|
||||
const bool needsHyphen = info.requiresInsertedHyphen;
|
||||
const int prefixWidth = measureWordWidth(renderer, fontId, word.substr(0, offset), style, needsHyphen);
|
||||
if (prefixWidth > availableWidth || prefixWidth <= chosenWidth) {
|
||||
continue; // Skip if too wide or not an improvement
|
||||
prefix.assign(word, 0, offset);
|
||||
const int prefixWidth = measureWordWidth(renderer, fontId, prefix, style, needsHyphen);
|
||||
if (prefixWidth > availableWidth) {
|
||||
break; // Ascending order: all subsequent breakpoints yield wider prefixes
|
||||
}
|
||||
if (prefixWidth <= chosenWidth) {
|
||||
continue; // Not an improvement
|
||||
}
|
||||
|
||||
chosenWidth = prefixWidth;
|
||||
@@ -434,19 +519,21 @@ bool ParsedText::hyphenateWordAtIndex(const size_t wordIndex, const int availabl
|
||||
return true;
|
||||
}
|
||||
|
||||
void ParsedText::extractLine(const size_t breakIndex, const int pageWidth, const int spaceWidth,
|
||||
const std::vector<uint16_t>& wordWidths, const std::vector<bool>& continuesVec,
|
||||
const std::vector<size_t>& lineBreakIndices,
|
||||
void ParsedText::extractLine(const size_t breakIndex, const int pageWidth, const std::vector<uint16_t>& wordWidths,
|
||||
const std::vector<bool>& continuesVec, const std::vector<size_t>& lineBreakIndices,
|
||||
const std::function<void(std::shared_ptr<TextBlock>)>& processLine,
|
||||
const GfxRenderer& renderer, const int fontId) {
|
||||
const size_t lineBreak = lineBreakIndices[breakIndex];
|
||||
const size_t lastBreakAt = breakIndex > 0 ? lineBreakIndices[breakIndex - 1] : 0;
|
||||
const size_t lineWordCount = lineBreak - lastBreakAt;
|
||||
|
||||
// Calculate first line indent (only for left/justified text without extra paragraph spacing)
|
||||
// Calculate first line indent (only for left/justified text).
|
||||
// Positive text-indent (paragraph indent) is suppressed when extraParagraphSpacing is on.
|
||||
// Negative text-indent (hanging indent, e.g. margin-left:3em; text-indent:-1em) always applies —
|
||||
// it is structural (positions the bullet/marker), not decorative.
|
||||
const bool isFirstLine = breakIndex == 0;
|
||||
const int firstLineIndent =
|
||||
isFirstLine && blockStyle.textIndent > 0 && !extraParagraphSpacing &&
|
||||
isFirstLine && blockStyle.textIndentDefined && (blockStyle.textIndent < 0 || !extraParagraphSpacing) &&
|
||||
(blockStyle.alignment == CssTextAlign::Justify || blockStyle.alignment == CssTextAlign::Left)
|
||||
? blockStyle.textIndent
|
||||
: 0;
|
||||
@@ -462,8 +549,7 @@ void ParsedText::extractLine(const size_t breakIndex, const int pageWidth, const
|
||||
// Count gaps: each word after the first creates a gap, unless it's a continuation
|
||||
if (wordIdx > 0 && !continuesVec[lastBreakAt + wordIdx]) {
|
||||
actualGapCount++;
|
||||
int naturalGap = spaceWidth;
|
||||
naturalGap += renderer.getSpaceKernAdjust(fontId, lastCodepoint(words[lastBreakAt + wordIdx - 1]),
|
||||
int naturalGap = renderer.getSpaceAdvance(fontId, lastCodepoint(words[lastBreakAt + wordIdx - 1]),
|
||||
firstCodepoint(words[lastBreakAt + wordIdx]),
|
||||
wordStyles[lastBreakAt + wordIdx - 1]);
|
||||
totalNaturalGaps += naturalGap;
|
||||
@@ -485,8 +571,9 @@ void ParsedText::extractLine(const size_t breakIndex, const int pageWidth, const
|
||||
? spareSpace / static_cast<int>(actualGapCount)
|
||||
: 0;
|
||||
|
||||
// Calculate initial x position (first line starts at indent for left/justified text)
|
||||
auto xpos = static_cast<uint16_t>(firstLineIndent);
|
||||
// Calculate initial x position (first line starts at indent for left/justified text;
|
||||
// may be negative for hanging indents, e.g. margin-left:3em; text-indent:-1em).
|
||||
auto xpos = static_cast<int16_t>(firstLineIndent);
|
||||
if (blockStyle.alignment == CssTextAlign::Right) {
|
||||
xpos = effectivePageWidth - lineWordWidthSum - totalNaturalGaps;
|
||||
} else if (blockStyle.alignment == CssTextAlign::Center) {
|
||||
@@ -495,7 +582,7 @@ void ParsedText::extractLine(const size_t breakIndex, const int pageWidth, const
|
||||
|
||||
// Pre-calculate X positions for words
|
||||
// Continuation words attach to the previous word with no space before them
|
||||
std::vector<uint16_t> lineXPos;
|
||||
std::vector<int16_t> lineXPos;
|
||||
lineXPos.reserve(lineWordCount);
|
||||
|
||||
for (size_t wordIdx = 0; wordIdx < lineWordCount; wordIdx++) {
|
||||
@@ -510,12 +597,11 @@ void ParsedText::extractLine(const size_t breakIndex, const int pageWidth, const
|
||||
firstCodepoint(words[lastBreakAt + wordIdx + 1]), wordStyles[lastBreakAt + wordIdx]);
|
||||
xpos += advance;
|
||||
} else {
|
||||
int gap = spaceWidth;
|
||||
if (wordIdx + 1 < lineWordCount) {
|
||||
gap += renderer.getSpaceKernAdjust(fontId, lastCodepoint(words[lastBreakAt + wordIdx]),
|
||||
firstCodepoint(words[lastBreakAt + wordIdx + 1]),
|
||||
wordStyles[lastBreakAt + wordIdx]);
|
||||
}
|
||||
int gap = wordIdx + 1 < lineWordCount
|
||||
? renderer.getSpaceAdvance(fontId, lastCodepoint(words[lastBreakAt + wordIdx]),
|
||||
firstCodepoint(words[lastBreakAt + wordIdx + 1]),
|
||||
wordStyles[lastBreakAt + wordIdx])
|
||||
: renderer.getSpaceWidth(fontId, wordStyles[lastBreakAt + wordIdx]);
|
||||
if (blockStyle.alignment == CssTextAlign::Justify && !isLastLine) {
|
||||
gap += justifyExtra;
|
||||
}
|
||||
|
||||
@@ -21,14 +21,14 @@ class ParsedText {
|
||||
bool hyphenationEnabled;
|
||||
|
||||
void applyParagraphIndent();
|
||||
std::vector<size_t> computeLineBreaks(const GfxRenderer& renderer, int fontId, int pageWidth, int spaceWidth,
|
||||
std::vector<size_t> computeLineBreaks(const GfxRenderer& renderer, int fontId, int pageWidth,
|
||||
std::vector<uint16_t>& wordWidths, std::vector<bool>& continuesVec);
|
||||
std::vector<size_t> computeHyphenatedLineBreaks(const GfxRenderer& renderer, int fontId, int pageWidth,
|
||||
int spaceWidth, std::vector<uint16_t>& wordWidths,
|
||||
std::vector<uint16_t>& wordWidths,
|
||||
std::vector<bool>& continuesVec);
|
||||
bool hyphenateWordAtIndex(size_t wordIndex, int availableWidth, const GfxRenderer& renderer, int fontId,
|
||||
std::vector<uint16_t>& wordWidths, bool allowFallbackBreaks);
|
||||
void extractLine(size_t breakIndex, int pageWidth, int spaceWidth, const std::vector<uint16_t>& wordWidths,
|
||||
void extractLine(size_t breakIndex, int pageWidth, const std::vector<uint16_t>& wordWidths,
|
||||
const std::vector<bool>& continuesVec, const std::vector<size_t>& lineBreakIndices,
|
||||
const std::function<void(std::shared_ptr<TextBlock>)>& processLine, const GfxRenderer& renderer,
|
||||
int fontId);
|
||||
|
||||
@@ -4,16 +4,19 @@
|
||||
#include <Logging.h>
|
||||
#include <Serialization.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <set>
|
||||
|
||||
#include "Epub/css/CssParser.h"
|
||||
#include "Page.h"
|
||||
#include "hyphenation/Hyphenator.h"
|
||||
#include "parsers/ChapterHtmlSlimParser.h"
|
||||
|
||||
namespace {
|
||||
constexpr uint8_t SECTION_FILE_VERSION = 14;
|
||||
constexpr uint8_t SECTION_FILE_VERSION = 19;
|
||||
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(uint32_t);
|
||||
sizeof(uint8_t) + sizeof(uint32_t) + sizeof(uint32_t);
|
||||
} // namespace
|
||||
|
||||
uint32_t Section::onPageComplete(std::unique_ptr<Page> page) {
|
||||
@@ -36,7 +39,7 @@ uint32_t Section::onPageComplete(std::unique_ptr<Page> page) {
|
||||
void Section::writeSectionFileHeader(const int fontId, const float lineCompression, const bool extraParagraphSpacing,
|
||||
const uint8_t paragraphAlignment, const uint16_t viewportWidth,
|
||||
const uint16_t viewportHeight, const bool hyphenationEnabled,
|
||||
const bool embeddedStyle) {
|
||||
const bool embeddedStyle, const uint8_t imageRendering) {
|
||||
if (!file) {
|
||||
LOG_DBG("SCT", "File not open for writing header");
|
||||
return;
|
||||
@@ -44,7 +47,7 @@ void Section::writeSectionFileHeader(const int fontId, const float lineCompressi
|
||||
static_assert(HEADER_SIZE == sizeof(SECTION_FILE_VERSION) + sizeof(fontId) + sizeof(lineCompression) +
|
||||
sizeof(extraParagraphSpacing) + sizeof(paragraphAlignment) + sizeof(viewportWidth) +
|
||||
sizeof(viewportHeight) + sizeof(pageCount) + sizeof(hyphenationEnabled) +
|
||||
sizeof(embeddedStyle) + sizeof(uint32_t),
|
||||
sizeof(embeddedStyle) + sizeof(imageRendering) + sizeof(uint32_t) + sizeof(uint32_t),
|
||||
"Header size mismatch");
|
||||
serialization::writePod(file, SECTION_FILE_VERSION);
|
||||
serialization::writePod(file, fontId);
|
||||
@@ -55,13 +58,16 @@ void Section::writeSectionFileHeader(const int fontId, const float lineCompressi
|
||||
serialization::writePod(file, viewportHeight);
|
||||
serialization::writePod(file, hyphenationEnabled);
|
||||
serialization::writePod(file, embeddedStyle);
|
||||
serialization::writePod(file, pageCount); // Placeholder for page count (will be initially 0 when written)
|
||||
serialization::writePod(file, static_cast<uint32_t>(0)); // Placeholder for LUT offset
|
||||
serialization::writePod(file, imageRendering);
|
||||
serialization::writePod(file, pageCount); // Placeholder for page count (will be initially 0, patched later)
|
||||
serialization::writePod(file, static_cast<uint32_t>(0)); // Placeholder for LUT offset (patched later)
|
||||
serialization::writePod(file, static_cast<uint32_t>(0)); // Placeholder for anchor map offset (patched later)
|
||||
}
|
||||
|
||||
bool Section::loadSectionFile(const int fontId, const float lineCompression, const bool extraParagraphSpacing,
|
||||
const uint8_t paragraphAlignment, const uint16_t viewportWidth,
|
||||
const uint16_t viewportHeight, const bool hyphenationEnabled, const bool embeddedStyle) {
|
||||
const uint16_t viewportHeight, const bool hyphenationEnabled, const bool embeddedStyle,
|
||||
const uint8_t imageRendering) {
|
||||
if (!Storage.openFileForRead("SCT", filePath, file)) {
|
||||
return false;
|
||||
}
|
||||
@@ -84,6 +90,7 @@ bool Section::loadSectionFile(const int fontId, const float lineCompression, con
|
||||
uint8_t fileParagraphAlignment;
|
||||
bool fileHyphenationEnabled;
|
||||
bool fileEmbeddedStyle;
|
||||
uint8_t fileImageRendering;
|
||||
serialization::readPod(file, fileFontId);
|
||||
serialization::readPod(file, fileLineCompression);
|
||||
serialization::readPod(file, fileExtraParagraphSpacing);
|
||||
@@ -92,11 +99,13 @@ bool Section::loadSectionFile(const int fontId, const float lineCompression, con
|
||||
serialization::readPod(file, fileViewportHeight);
|
||||
serialization::readPod(file, fileHyphenationEnabled);
|
||||
serialization::readPod(file, fileEmbeddedStyle);
|
||||
serialization::readPod(file, fileImageRendering);
|
||||
|
||||
if (fontId != fileFontId || lineCompression != fileLineCompression ||
|
||||
extraParagraphSpacing != fileExtraParagraphSpacing || paragraphAlignment != fileParagraphAlignment ||
|
||||
viewportWidth != fileViewportWidth || viewportHeight != fileViewportHeight ||
|
||||
hyphenationEnabled != fileHyphenationEnabled || embeddedStyle != fileEmbeddedStyle) {
|
||||
hyphenationEnabled != fileHyphenationEnabled || embeddedStyle != fileEmbeddedStyle ||
|
||||
imageRendering != fileImageRendering) {
|
||||
file.close();
|
||||
LOG_ERR("SCT", "Deserialization failed: Parameters do not match");
|
||||
clearCache();
|
||||
@@ -107,6 +116,7 @@ bool Section::loadSectionFile(const int fontId, const float lineCompression, con
|
||||
serialization::readPod(file, pageCount);
|
||||
file.close();
|
||||
LOG_DBG("SCT", "Deserialization succeeded: %d pages", pageCount);
|
||||
buildTocBoundaries(readAnchorMap(filePath));
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -129,7 +139,7 @@ bool Section::clearCache() const {
|
||||
bool Section::createSectionFile(const int fontId, const float lineCompression, const bool extraParagraphSpacing,
|
||||
const uint8_t paragraphAlignment, const uint16_t viewportWidth,
|
||||
const uint16_t viewportHeight, const bool hyphenationEnabled, const bool embeddedStyle,
|
||||
const std::function<void()>& popupFn) {
|
||||
const uint8_t imageRendering, const std::function<void()>& popupFn) {
|
||||
const auto localPath = epub->getSpineItem(spineIndex).href;
|
||||
const auto tmpHtmlPath = epub->getCachePath() + "/.tmp_" + std::to_string(spineIndex) + ".html";
|
||||
|
||||
@@ -179,7 +189,7 @@ bool Section::createSectionFile(const int fontId, const float lineCompression, c
|
||||
return false;
|
||||
}
|
||||
writeSectionFileHeader(fontId, lineCompression, extraParagraphSpacing, paragraphAlignment, viewportWidth,
|
||||
viewportHeight, hyphenationEnabled, embeddedStyle);
|
||||
viewportHeight, hyphenationEnabled, embeddedStyle, imageRendering);
|
||||
std::vector<uint32_t> lut = {};
|
||||
|
||||
// Derive the content base directory and image cache path prefix for the parser
|
||||
@@ -197,11 +207,24 @@ bool Section::createSectionFile(const int fontId, const float lineCompression, c
|
||||
}
|
||||
}
|
||||
|
||||
// Collect TOC anchors for this spine so the parser can insert page breaks at chapter boundaries
|
||||
std::set<std::string> tocAnchors;
|
||||
const int startTocIndex = epub->getTocIndexForSpineIndex(spineIndex);
|
||||
if (startTocIndex >= 0) {
|
||||
for (int i = startTocIndex; i < epub->getTocItemsCount(); i++) {
|
||||
auto entry = epub->getTocItem(i);
|
||||
if (entry.spineIndex != spineIndex) break;
|
||||
if (!entry.anchor.empty()) {
|
||||
tocAnchors.insert(std::move(entry.anchor));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ChapterHtmlSlimParser visitor(
|
||||
epub, tmpHtmlPath, renderer, fontId, lineCompression, extraParagraphSpacing, paragraphAlignment, viewportWidth,
|
||||
viewportHeight, hyphenationEnabled,
|
||||
[this, &lut](std::unique_ptr<Page> page) { lut.emplace_back(this->onPageComplete(std::move(page))); },
|
||||
embeddedStyle, contentBase, imageBasePath, popupFn, cssParser);
|
||||
embeddedStyle, contentBase, imageBasePath, imageRendering, std::move(tocAnchors), popupFn, cssParser);
|
||||
Hyphenator::setPreferredLanguage(epub->getLanguage());
|
||||
success = visitor.parseAndBuildPages();
|
||||
|
||||
@@ -234,14 +257,31 @@ bool Section::createSectionFile(const int fontId, const float lineCompression, c
|
||||
return false;
|
||||
}
|
||||
|
||||
// Go back and write LUT offset
|
||||
file.seek(HEADER_SIZE - sizeof(uint32_t) - sizeof(pageCount));
|
||||
// Write anchor-to-page map for fragment navigation (footnotes + TOC)
|
||||
const uint32_t anchorMapOffset = file.position();
|
||||
const auto& anchors = visitor.getAnchors();
|
||||
serialization::writePod(file, static_cast<uint16_t>(anchors.size()));
|
||||
for (const auto& [anchor, page] : anchors) {
|
||||
serialization::writeString(file, anchor);
|
||||
serialization::writePod(file, page);
|
||||
}
|
||||
|
||||
// Patch header with final pageCount, lutOffset, and anchorMapOffset
|
||||
file.seek(HEADER_SIZE - sizeof(uint32_t) * 2 - sizeof(pageCount));
|
||||
serialization::writePod(file, pageCount);
|
||||
serialization::writePod(file, lutOffset);
|
||||
serialization::writePod(file, anchorMapOffset);
|
||||
file.close();
|
||||
if (cssParser) {
|
||||
cssParser->clear();
|
||||
}
|
||||
|
||||
// Convert anchor vector to map for buildTocBoundaries
|
||||
std::map<std::string, uint16_t> anchorMap;
|
||||
for (const auto& [a, p] : anchors) {
|
||||
anchorMap.emplace(a, p);
|
||||
}
|
||||
buildTocBoundaries(anchorMap);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -250,7 +290,7 @@ std::unique_ptr<Page> Section::loadPageFromSectionFile() {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
file.seek(HEADER_SIZE - sizeof(uint32_t));
|
||||
file.seek(HEADER_SIZE - sizeof(uint32_t) * 2);
|
||||
uint32_t lutOffset;
|
||||
serialization::readPod(file, lutOffset);
|
||||
file.seek(lutOffset + sizeof(uint32_t) * currentPage);
|
||||
@@ -262,3 +302,170 @@ std::unique_ptr<Page> Section::loadPageFromSectionFile() {
|
||||
file.close();
|
||||
return page;
|
||||
}
|
||||
|
||||
std::optional<uint16_t> Section::getPageForAnchor(const std::string& anchor) const {
|
||||
FsFile f;
|
||||
if (!Storage.openFileForRead("SCT", filePath, f)) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
const uint32_t fileSize = f.size();
|
||||
f.seek(HEADER_SIZE - sizeof(uint32_t));
|
||||
uint32_t anchorMapOffset;
|
||||
serialization::readPod(f, anchorMapOffset);
|
||||
if (anchorMapOffset == 0 || anchorMapOffset >= fileSize) {
|
||||
f.close();
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
f.seek(anchorMapOffset);
|
||||
uint16_t count;
|
||||
serialization::readPod(f, count);
|
||||
for (uint16_t i = 0; i < count; i++) {
|
||||
std::string key;
|
||||
uint16_t page;
|
||||
serialization::readString(f, key);
|
||||
serialization::readPod(f, page);
|
||||
if (key == anchor) {
|
||||
f.close();
|
||||
return page;
|
||||
}
|
||||
}
|
||||
|
||||
f.close();
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
std::map<std::string, uint16_t> Section::readAnchorMap(const std::string& sectionPath) {
|
||||
FsFile f;
|
||||
if (!Storage.openFileForRead("SCT", sectionPath, f)) {
|
||||
return {};
|
||||
}
|
||||
|
||||
f.seek(HEADER_SIZE - sizeof(uint32_t));
|
||||
uint32_t anchorMapOffset;
|
||||
serialization::readPod(f, anchorMapOffset);
|
||||
if (anchorMapOffset == 0) {
|
||||
f.close();
|
||||
return {};
|
||||
}
|
||||
|
||||
f.seek(anchorMapOffset);
|
||||
uint16_t count;
|
||||
serialization::readPod(f, count);
|
||||
std::map<std::string, uint16_t> result;
|
||||
for (uint16_t i = 0; i < count; i++) {
|
||||
std::string key;
|
||||
uint16_t page;
|
||||
serialization::readString(f, key);
|
||||
serialization::readPod(f, page);
|
||||
result.emplace(std::move(key), page);
|
||||
}
|
||||
|
||||
f.close();
|
||||
return result;
|
||||
}
|
||||
|
||||
void Section::buildTocBoundaries(const std::map<std::string, uint16_t>& anchorMap) {
|
||||
tocBoundaries.clear();
|
||||
const int startTocIndex = epub->getTocIndexForSpineIndex(spineIndex);
|
||||
if (startTocIndex < 0) return;
|
||||
|
||||
const int tocCount = epub->getTocItemsCount();
|
||||
for (int i = startTocIndex; i < tocCount; i++) {
|
||||
const auto entry = epub->getTocItem(i);
|
||||
if (entry.spineIndex != spineIndex) break;
|
||||
uint16_t page = 0;
|
||||
if (!entry.anchor.empty()) {
|
||||
auto it = anchorMap.find(entry.anchor);
|
||||
if (it != anchorMap.end()) page = it->second;
|
||||
}
|
||||
tocBoundaries.push_back({i, page});
|
||||
}
|
||||
std::sort(tocBoundaries.begin(), tocBoundaries.end(),
|
||||
[](const TocBoundary& a, const TocBoundary& b) { return a.startPage < b.startPage; });
|
||||
}
|
||||
|
||||
int Section::getTocIndexForPage(const int page) const {
|
||||
if (tocBoundaries.empty()) {
|
||||
return epub->getTocIndexForSpineIndex(spineIndex);
|
||||
}
|
||||
|
||||
auto it = std::upper_bound(tocBoundaries.begin(), tocBoundaries.end(), static_cast<uint16_t>(page),
|
||||
[](uint16_t p, const TocBoundary& boundary) { return p < boundary.startPage; });
|
||||
if (it == tocBoundaries.begin()) {
|
||||
return tocBoundaries[0].tocIndex;
|
||||
}
|
||||
return std::prev(it)->tocIndex;
|
||||
}
|
||||
|
||||
std::optional<int> Section::getPageForTocIndex(const int tocIndex) const {
|
||||
for (const auto& boundary : tocBoundaries) {
|
||||
if (boundary.tocIndex == tocIndex) {
|
||||
return boundary.startPage;
|
||||
}
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
std::optional<Section::TocPageRange> Section::getPageRangeForTocIndex(const int tocIndex) const {
|
||||
for (size_t i = 0; i < tocBoundaries.size(); i++) {
|
||||
if (tocBoundaries[i].tocIndex == tocIndex) {
|
||||
const int startPage = tocBoundaries[i].startPage;
|
||||
const int endPage = (i + 1 < tocBoundaries.size()) ? static_cast<int>(tocBoundaries[i + 1].startPage) : pageCount;
|
||||
return TocPageRange{startPage, endPage};
|
||||
}
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
std::optional<uint16_t> Section::readCachedPageCount(const std::string& cachePath, const int spineIndex,
|
||||
const int fontId, const float lineCompression,
|
||||
const bool extraParagraphSpacing, const uint8_t paragraphAlignment,
|
||||
const uint16_t viewportWidth, const uint16_t viewportHeight,
|
||||
const bool hyphenationEnabled, const bool embeddedStyle,
|
||||
const uint8_t imageRendering) {
|
||||
const std::string path = cachePath + "/sections/" + std::to_string(spineIndex) + ".bin";
|
||||
FsFile f;
|
||||
if (!Storage.openFileForRead("SCT", path, f)) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
uint8_t version;
|
||||
serialization::readPod(f, version);
|
||||
if (version != SECTION_FILE_VERSION) {
|
||||
f.close();
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
int fileFontId;
|
||||
float fileLineCompression;
|
||||
bool fileExtraParagraphSpacing;
|
||||
uint8_t fileParagraphAlignment;
|
||||
uint16_t fileViewportWidth, fileViewportHeight;
|
||||
bool fileHyphenationEnabled, fileEmbeddedStyle;
|
||||
uint8_t fileImageRendering;
|
||||
serialization::readPod(f, fileFontId);
|
||||
serialization::readPod(f, fileLineCompression);
|
||||
serialization::readPod(f, fileExtraParagraphSpacing);
|
||||
serialization::readPod(f, fileParagraphAlignment);
|
||||
serialization::readPod(f, fileViewportWidth);
|
||||
serialization::readPod(f, fileViewportHeight);
|
||||
serialization::readPod(f, fileHyphenationEnabled);
|
||||
serialization::readPod(f, fileEmbeddedStyle);
|
||||
serialization::readPod(f, fileImageRendering);
|
||||
|
||||
if (fontId != fileFontId || lineCompression != fileLineCompression ||
|
||||
extraParagraphSpacing != fileExtraParagraphSpacing || paragraphAlignment != fileParagraphAlignment ||
|
||||
viewportWidth != fileViewportWidth || viewportHeight != fileViewportHeight ||
|
||||
hyphenationEnabled != fileHyphenationEnabled || embeddedStyle != fileEmbeddedStyle ||
|
||||
imageRendering != fileImageRendering) {
|
||||
f.close();
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
uint16_t count;
|
||||
serialization::readPod(f, count);
|
||||
f.close();
|
||||
return count;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
#pragma once
|
||||
#include <functional>
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "Epub.h"
|
||||
|
||||
@@ -16,9 +20,18 @@ class Section {
|
||||
|
||||
void writeSectionFileHeader(int fontId, float lineCompression, bool extraParagraphSpacing, uint8_t paragraphAlignment,
|
||||
uint16_t viewportWidth, uint16_t viewportHeight, bool hyphenationEnabled,
|
||||
bool embeddedStyle);
|
||||
bool embeddedStyle, uint8_t imageRendering);
|
||||
uint32_t onPageComplete(std::unique_ptr<Page> page);
|
||||
|
||||
struct TocBoundary {
|
||||
int tocIndex = 0;
|
||||
uint16_t startPage = 0;
|
||||
};
|
||||
std::vector<TocBoundary> tocBoundaries;
|
||||
|
||||
static std::map<std::string, uint16_t> readAnchorMap(const std::string& sectionPath);
|
||||
void buildTocBoundaries(const std::map<std::string, uint16_t>& anchorMap);
|
||||
|
||||
public:
|
||||
uint16_t pageCount = 0;
|
||||
int currentPage = 0;
|
||||
@@ -30,10 +43,31 @@ class Section {
|
||||
filePath(epub->getCachePath() + "/sections/" + std::to_string(spineIndex) + ".bin") {}
|
||||
~Section() = default;
|
||||
bool loadSectionFile(int fontId, float lineCompression, bool extraParagraphSpacing, uint8_t paragraphAlignment,
|
||||
uint16_t viewportWidth, uint16_t viewportHeight, bool hyphenationEnabled, bool embeddedStyle);
|
||||
uint16_t viewportWidth, uint16_t viewportHeight, bool hyphenationEnabled, bool embeddedStyle,
|
||||
uint8_t imageRendering);
|
||||
bool clearCache() const;
|
||||
bool createSectionFile(int fontId, float lineCompression, bool extraParagraphSpacing, uint8_t paragraphAlignment,
|
||||
uint16_t viewportWidth, uint16_t viewportHeight, bool hyphenationEnabled, bool embeddedStyle,
|
||||
const std::function<void()>& popupFn = nullptr);
|
||||
uint8_t imageRendering, const std::function<void()>& popupFn = nullptr);
|
||||
std::unique_ptr<Page> loadPageFromSectionFile();
|
||||
|
||||
// Look up the page number for an anchor id from the section cache file (used for footnotes).
|
||||
std::optional<uint16_t> getPageForAnchor(const std::string& anchor) const;
|
||||
|
||||
// TOC boundary navigation: maps TOC entries to page ranges within this section.
|
||||
int getTocIndexForPage(int page) const;
|
||||
std::optional<int> getPageForTocIndex(int tocIndex) const;
|
||||
|
||||
struct TocPageRange {
|
||||
int startPage;
|
||||
int endPage;
|
||||
};
|
||||
std::optional<TocPageRange> getPageRangeForTocIndex(int tocIndex) const;
|
||||
|
||||
// Reads just the pageCount from an existing section cache file without loading the full section.
|
||||
static std::optional<uint16_t> readCachedPageCount(const std::string& cachePath, int spineIndex, int fontId,
|
||||
float lineCompression, bool extraParagraphSpacing,
|
||||
uint8_t paragraphAlignment, uint16_t viewportWidth,
|
||||
uint16_t viewportHeight, bool hyphenationEnabled,
|
||||
bool embeddedStyle, uint8_t imageRendering);
|
||||
};
|
||||
|
||||
29
lib/Epub/Epub/TableData.h
Normal file
29
lib/Epub/Epub/TableData.h
Normal file
@@ -0,0 +1,29 @@
|
||||
#pragma once
|
||||
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
|
||||
#include "ParsedText.h"
|
||||
#include "css/CssStyle.h"
|
||||
|
||||
/// A single cell in a table row.
|
||||
struct TableCell {
|
||||
std::unique_ptr<ParsedText> content;
|
||||
bool isHeader = false; // true for <th>, false for <td>
|
||||
int colspan = 1; // number of logical columns this cell spans
|
||||
CssLength widthHint; // width hint from HTML attribute or CSS (if hasWidthHint)
|
||||
bool hasWidthHint = false;
|
||||
};
|
||||
|
||||
/// A single row in a table.
|
||||
struct TableRow {
|
||||
std::vector<TableCell> cells;
|
||||
};
|
||||
|
||||
/// Buffered table data collected during SAX parsing.
|
||||
/// The entire table must be buffered before layout because column widths
|
||||
/// depend on content across all rows.
|
||||
struct TableData {
|
||||
std::vector<TableRow> rows;
|
||||
std::vector<CssLength> colWidthHints; // width hints from <col> tags, indexed by logical column
|
||||
};
|
||||
@@ -74,7 +74,7 @@ bool TextBlock::serialize(FsFile& file) const {
|
||||
std::unique_ptr<TextBlock> TextBlock::deserialize(FsFile& file) {
|
||||
uint16_t wc;
|
||||
std::vector<std::string> words;
|
||||
std::vector<uint16_t> wordXpos;
|
||||
std::vector<int16_t> wordXpos;
|
||||
std::vector<EpdFontFamily::Style> wordStyles;
|
||||
BlockStyle blockStyle;
|
||||
|
||||
|
||||
@@ -13,12 +13,12 @@
|
||||
class TextBlock final : public Block {
|
||||
private:
|
||||
std::vector<std::string> words;
|
||||
std::vector<uint16_t> wordXpos;
|
||||
std::vector<int16_t> wordXpos;
|
||||
std::vector<EpdFontFamily::Style> wordStyles;
|
||||
BlockStyle blockStyle;
|
||||
|
||||
public:
|
||||
explicit TextBlock(std::vector<std::string> words, std::vector<uint16_t> word_xpos,
|
||||
explicit TextBlock(std::vector<std::string> words, std::vector<int16_t> word_xpos,
|
||||
std::vector<EpdFontFamily::Style> word_styles, const BlockStyle& blockStyle = BlockStyle())
|
||||
: words(std::move(words)),
|
||||
wordXpos(std::move(word_xpos)),
|
||||
@@ -28,6 +28,7 @@ class TextBlock final : public Block {
|
||||
void setBlockStyle(const BlockStyle& blockStyle) { this->blockStyle = blockStyle; }
|
||||
const BlockStyle& getBlockStyle() const { return blockStyle; }
|
||||
const std::vector<std::string>& getWords() const { return words; }
|
||||
const std::vector<int16_t>& getWordXpos() const { return wordXpos; }
|
||||
bool isEmpty() override { return words.empty(); }
|
||||
size_t wordCount() const { return words.size(); }
|
||||
// given a renderer works out where to break the words into lines
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#include "JpegToFramebufferConverter.h"
|
||||
|
||||
#include <FsHelpers.h>
|
||||
#include <GfxRenderer.h>
|
||||
#include <HalStorage.h>
|
||||
#include <JPEGDEC.h>
|
||||
@@ -486,9 +487,5 @@ bool JpegToFramebufferConverter::decodeToFramebuffer(const std::string& imagePat
|
||||
}
|
||||
|
||||
bool JpegToFramebufferConverter::supportsFormat(const std::string& extension) {
|
||||
std::string ext = extension;
|
||||
for (auto& c : ext) {
|
||||
c = tolower(c);
|
||||
}
|
||||
return (ext == ".jpg" || ext == ".jpeg");
|
||||
return FsHelpers::hasJpgExtension(extension);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#include "PngToFramebufferConverter.h"
|
||||
|
||||
#include <FsHelpers.h>
|
||||
#include <GfxRenderer.h>
|
||||
#include <HalStorage.h>
|
||||
#include <Logging.h>
|
||||
@@ -391,9 +392,5 @@ bool PngToFramebufferConverter::decodeToFramebuffer(const std::string& imagePath
|
||||
}
|
||||
|
||||
bool PngToFramebufferConverter::supportsFormat(const std::string& extension) {
|
||||
std::string ext = extension;
|
||||
for (auto& c : ext) {
|
||||
c = tolower(c);
|
||||
}
|
||||
return (ext == ".png");
|
||||
return FsHelpers::hasPngExtension(extension);
|
||||
}
|
||||
|
||||
@@ -95,8 +95,6 @@ bool isPunctuation(const uint32_t cp) {
|
||||
case '}':
|
||||
case '[':
|
||||
case ']':
|
||||
case '/':
|
||||
case 0x2039: // ‹
|
||||
case 0x203A: // ›
|
||||
case 0x2026: // …
|
||||
return true;
|
||||
@@ -109,6 +107,7 @@ bool isAsciiDigit(const uint32_t cp) { return cp >= '0' && cp <= '9'; }
|
||||
|
||||
bool isExplicitHyphen(const uint32_t cp) {
|
||||
switch (cp) {
|
||||
case '/':
|
||||
case '-':
|
||||
case 0x00AD: // soft hyphen
|
||||
case 0x058A: // Armenian hyphen
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user