Files
crosspoint-reader-mod/mod/prs/MERGED.md
cottongin 83aa38d1a2 docs: update tracking for ported PRs #1329, #1143, #1172, #1320, #1325
Add detailed entries to MERGED.md for all 5 ported PRs with context,
changes applied, and differences from upstream. Update upstream-sync.md
tracking table with new entries and sync date.

Made-with: Cursor
2026-03-08 05:02:05 -04:00

359 lines
29 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Merged Upstream PRs
Tracking document for upstream PRs ported into this mod.
- [PR #1038](#pr-1038-replace-stdlist-with-stdvector-in-text-layout) — Replace std::list with std::vector in text layout (znelson)
- [PR #1045](#pr-1045-shorten-forget-wifi-button-labels-to-fit-on-button) — Shorten "Forget Wifi" button labels (lukestein)
- [PR #1037](#pr-1037-fix-hyphenation-and-rendering-of-decomposed-characters) — Fix hyphenation and rendering of decomposed characters (jpirnay)
- [PR #1019](#pr-1019-display-file-extensions-in-file-browser) — Display file extensions in File Browser (CaptainFrito)
- [PR #1055](#pr-1055-byte-level-framebuffer-writes-for-fillrect-and-axis-aligned-drawline) — Byte-level framebuffer writes for fillRect and drawLine (jpirnay)
- [PR #1027](#pr-1027-reduce-parsedtext-layout-time-79-via-word-width-cache-and-hyphenation-early-exit) — Reduce ParsedText layout time 79% via word-width cache and hyphenation early exit (jpirnay)
- [PR #1068](#pr-1068-correct-hyphenation-of-urls) — Correct hyphenation of URLs (Uri-Tauber)
- [PR #1329](#pr-1329-refactor-reader-utils) — Refactor shared reader utilities (upstream)
- [PR #1143 + #1172](#pr-1143--1172-toc-fragment-navigation--multi-spine-toc) — TOC fragment navigation + multi-spine TOC (upstream)
- [PR #1320](#pr-1320-jpeg-resource-cleanup) — RAII JPEG resource cleanup (upstream)
- [PR #1325](#pr-1325-settings-tab-label) — Dynamic settings tab label (upstream)
---
## PR #1038: Replace std::list with std::vector in text layout
- **URL:** [https://github.com/crosspoint-reader/crosspoint-reader/pull/1038](https://github.com/crosspoint-reader/crosspoint-reader/pull/1038)
- **Author:** znelson (revision of BlindBat's #802)
- **Status in upstream:** Open (approved by osteotek, awaiting second review from daveallie)
- **Method:** Manual port (partial -- incremental fixes only)
### Context
The core list-to-vector conversion was already performed in a prior mod sync. This port only brings in two targeted fixes from the PR that were missing locally.
### Changes applied
1. **Erase consumed words in `layoutAndExtractLines`** (`lib/Epub/Epub/ParsedText.cpp`): Added `.erase()` calls after the line extraction loop to remove consumed words from `words`, `wordStyles`, `wordContinues`, and `forceBreakAfter` vectors. Without this, the 750-word early flush threshold fires on every subsequent `addWord` call instead of only ~3 times per large paragraph, causing ~1,670 redundant flushes.
2. **Fix `wordContinues` flag in `hyphenateWordAtIndex`** (`lib/Epub/Epub/ParsedText.cpp`): Corrected the attach-to-previous flag handling when splitting a word at a hyphenation point. The prefix now keeps its original flag and the remainder gets `false`, instead of the previous behavior that cleared the prefix's flag and transferred it to the remainder. This fix was identified by coderabbit review and accepted in commit `9bbe994`.
### Differences from upstream PR
- The `forceBreakAfter` vector erase was added (mod-only vector not present in upstream) alongside the three upstream vectors.
- The upstream PR's full list-to-vector conversion (TextBlock.h/.cpp, ParsedText.h/.cpp) was already done in a prior mod sync. Only the two fixes above were new.
### Notable PR discussion
- znelson posted detailed benchmarks comparing list vs vector on ESP32-C3 with "Intermezzo" by Sally Rooney (chapter 14, 2,420-word paragraph): 11% faster chapter parse time, 89% more heap headroom (~50KB saved), 10-14% faster layout for medium paragraphs.
- The erase cost is ~90-133 microseconds per flush (0.1% of total flush time).
---
## PR #1045: Shorten "Forget Wifi" button labels to fit on button
- **URL:** [https://github.com/crosspoint-reader/crosspoint-reader/pull/1045](https://github.com/crosspoint-reader/crosspoint-reader/pull/1045)
- **Author:** lukestein
- **Status in upstream:** Open (approved by osteotek)
- **Resolves:** upstream issue #1035
- **Method:** Manual port (direct string changes)
### Changes applied
Updated `STR_FORGET_BUTTON` in all 9 translation yaml files under `lib/I18n/translations/`:
| Language | Before | After |
| ---------- | ------------------- | ------------ |
| Czech | "Zapomenout na síť" | "Zapomenout" |
| English | "Forget network" | "Forget" |
| French | "Oublier le réseau" | "Oublier" |
| German | "WLAN entfernen" | "Entfernen" |
| Portuguese | "Esquecer rede" | "Esquecer" |
| Romanian | "Uitaţi reţeaua" | "Uitaţi" |
| Russian | "Забыть сеть" | "Забыть" |
| Spanish | "Olvidar la red" | "Olvidar" |
| Swedish | "Glöm nätverk" | "Glöm" |
### Prerequisite
Romanian translation file (`romanian.yaml`) was pulled from `upstream/master` (merged PR #987 by ariel-lindemann) as it was missing locally.
### Differences from upstream PR
None. Changes are identical to the upstream PR.
### Notable PR discussion
- ariel-lindemann requested the Romanian shortening be included (was added in a follow-up commit by lukestein).
- Translations were verified via Google Translate (lukestein is not a native speaker of non-English languages).
---
## PR #1037: Fix hyphenation and rendering of decomposed characters
- **URL:** [https://github.com/crosspoint-reader/crosspoint-reader/pull/1037](https://github.com/crosspoint-reader/crosspoint-reader/pull/1037)
- **Author:** jpirnay
- **Status in upstream:** Open (no approvals yet; coderabbit reviewed with one actionable comment, resolved)
- **Related to:** upstream issue #998 (Minor hyphenation and text flow issues)
- **Method:** Manual port (4 files)
### Changes applied
1. `**lib/Utf8/Utf8.h`**: Added `utf8IsCombiningMark(uint32_t)` inline function that identifies Unicode combining diacritical marks (ranges: 0x0300-0x036F, 0x1DC0-0x1DFF, 0x20D0-0x20FF, 0xFE20-0xFE2F).
2. `**lib/EpdFont/EpdFont.cpp**`: Added combining mark positioning in `getTextBounds`. Tracks last base glyph state (`lastBaseX`, `lastBaseAdvance`, `lastBaseTop`, `hasBaseGlyph`). Combining marks are centered over the base glyph's advance width and raised with a minimum 1px gap. Cursor does not advance for combining marks.
3. `**lib/Epub/Epub/hyphenation/HyphenationCommon.cpp**`: Added NFC-like precomposition in `collectCodepoints()`. When a combining diacritic follows a base character and a precomposed Latin-1/Latin-Extended scalar exists (grave, acute, circumflex, tilde, diaeresis, cedilla), the base is replaced with the precomposed form and the combining mark is skipped. This allows Liang hyphenation patterns to match decomposed text correctly.
4. `**lib/GfxRenderer/GfxRenderer.cpp**`: Added combining mark handling to `drawText`, `drawTextRotated90CW`, `drawTextRotated90CCW`, and `getTextAdvanceX`. Each text drawing function tracks base glyph state and renders combining marks at adjusted (raised, centered) positions without advancing the cursor. `getTextAdvanceX` skips combining marks entirely.
### Differences from upstream PR
- `**drawTextRotated90CCW**`: This is a mod-only function (not present in upstream). Combining mark handling was added here following the same pattern as the CW rotation, with coordinate adjustments inverted for CCW direction (`+raiseBy` on X, `+lastBaseAdvance/2` on Y instead of negatives).
- The upstream PR initially had a duplicate local `isCombiningMark` function in `EpdFont.cpp` (anonymous namespace) which was flagged by coderabbit and replaced with the shared `utf8IsCombiningMark` from `Utf8.h`. Our port uses the shared function directly throughout.
### Notable PR discussion
- jpirnay noted this is not a 100% bullet-proof implementation -- it maps common base+combining sequences rather than implementing full Unicode NFC normalization.
- The render fix is more universal: it properly x-centers the compound glyph over the previous one and uses at least 1pt visual distance in Y.
- lukestein specifically expressed interest in the fix for hyphenation of already-hyphenated words (e.g., "US-Satellitensystem" was exclusively broken at the existing hyphen).
- osteotek approved the approach of breaking already-hyphenated words at additional Liang pattern points.
---
## PR #1019: Display file extensions in File Browser
- **URL:** [https://github.com/crosspoint-reader/crosspoint-reader/pull/1019](https://github.com/crosspoint-reader/crosspoint-reader/pull/1019)
- **Author:** CaptainFrito
- **Status in upstream:** Open (no approvals; Eloren1 asked about long filename behavior)
- **Method:** Manual port (1 file)
### Changes applied
In `src/activities/home/MyLibraryActivity.cpp`:
1. Added `getFileExtension(std::string)` helper function near the existing `getFileName`. Returns the file extension including the dot (e.g., ".epub"), or empty string for directories or files without extensions.
2. Updated the `GUI.drawList` call to pass the extension lambda as the `rowValue` parameter and `false` for `highlightValue`. The `drawList` signature already supported these parameters with defaults.
### Differences from upstream PR
None. The implementation is functionally identical.
### Notable PR discussion
- CaptainFrito questioned whether the multiple lambda approach (each independently indexing into the files array) is optimal, suggesting a single function returning a struct might be better.
- Eloren1 asked how long filenames look with extensions displayed. This remains an open question in the upstream PR. **See mod enhancement below.**
- The PR was force-pushed to squash commits.
### Mod enhancement: Expandable selected row for long filenames
Addresses Eloren1's concern about long filenames. When the selected row's filename overflows the available text width, the row expands to 2 lines with smart text wrapping. The file extension moves to the bottom-right of the expanded area, baseline-aligned with the last text line. Non-selected rows retain single-line truncation. If the filename still overflows after 2 lines, the second line is truncated with "...".
**Files modified:**
- `src/components/themes/BaseTheme.cpp`: Added `wrapTextToLines` utility function with 3-tier break logic and `truncateWithEllipsis` helper. Modified `drawList` to detect selected-row overflow, draw expanded selection highlight at 2x row height, render wrapped title with row-height line spacing, and position the file extension on line 2 (right-aligned).
- `src/components/themes/lyra/LyraTheme.cpp`: Same helpers and analogous `drawList` modifications with Lyra-specific styling (rounded-rect selection highlight, icon aligned with line 1, scroll bar awareness).
**Design decisions:**
- Only the selected/highlighted row expands; other rows with long filenames continue to truncate normally.
- Expansion triggers when the title overflows the value-reduced text width (i.e., when it would be truncated in single-line mode). For titles that overflow the value area but still fit the full width, the expanded row shows the full title on line 1 with the extension on line 2 below.
- 3-tier text wrapping for natural line breaks:
1. Preferred delimiters: breaks at " -- ", " - ", en-dash, or em-dash separators (common "Title - Author" naming convention). Uses last occurrence to maximize line 1 content.
2. Word boundaries: breaks at last space or hyphen that fits on line 1.
3. Character-level fallback: for long unbroken tokens without spaces or hyphens.
- Each wrapped line is placed at the same Y position as a normal list row (row-height spacing between lines), giving natural visual separation that matches the list's vertical rhythm.
- File extension is always positioned on line 2 of the expanded area (right-aligned), regardless of how many text lines are produced by wrapping.
- Icons in LyraTheme are aligned with line 1 (not centered in the expanded area).
- Pagination uses `effectivePageItems` (pageItems - 1 when expanding) for totalPages, scroll indicators, and page boundaries. This ensures hidden items appear on the next page with proper scroll indicators. To prevent items from the previous page "leaking" into view, the page start is clamped to never go before the original (non-expanded) page boundary. Edge case: when the selected item is the last on the original page, the start shifts forward minimally to keep it visible.
- Boundary item duplication: when navigating to a "real" page boundary (non-expanding path), if the item just before the page start would need expansion when selected, it is included at the top of the current page (the page start is decremented by 1). This prevents the "bumped" item from vanishing when the user navigates from it to the next page. The guard `selectedIndex < pageStartIndex + pageItems - 1` ensures the selected item isn't pushed off the page by this adjustment.
---
## PR #1055: Byte-level framebuffer writes for fillRect and axis-aligned drawLine
- **URL:** [https://github.com/crosspoint-reader/crosspoint-reader/pull/1055](https://github.com/crosspoint-reader/crosspoint-reader/pull/1055)
- **Author:** jpirnay
- **Status in upstream:** Open (coderabbit reviewed, no approvals yet)
- **Method:** Manual port (2 files)
### Context
Eliminates per-pixel `drawPixel` calls for solid fills, axis-aligned lines, and dithered fills by writing directly to the framebuffer at byte granularity. Exploits the fact that one logical dimension always maps to a contiguous physical row in the framebuffer, allowing entire spans to be written with byte masking and `memset` instead of individual read-modify-write cycles per pixel. Measured 232470x speedups on ESP32-C3 hardware for full-screen operations.
### Changes applied
1. **`lib/GfxRenderer/GfxRenderer.h`**: Added two new private methods: `fillPhysicalHSpanByte(int phyY, int phyX_start, int phyX_end, uint8_t patternByte)` for writing a patterned horizontal span with byte-level edge blending, and `fillPhysicalHSpan(int phyY, int phyX_start, int phyX_end, bool state)` as a thin solid-fill wrapper.
2. **`lib/GfxRenderer/GfxRenderer.cpp`**: Added `#include <cstring>` for `memset`. Implemented `fillPhysicalHSpanByte` (bounds clamping, MSB-first bit packing, partial-byte masking at edges, memset for aligned middle) and `fillPhysicalHSpan` wrapper.
3. **`drawLine` (axis-aligned cases)**: Logical vertical lines in Portrait/PortraitInverted now route through `fillPhysicalHSpan` instead of per-pixel loops. Logical horizontal lines in Landscape orientations similarly use `fillPhysicalHSpan`. Bresenham diagonal path is unchanged.
4. **`fillRect`**: Replaced the per-row `drawLine` loop with orientation-specific fast paths. Each orientation iterates over the logical dimension that maps to a constant physical row, writing the perpendicular extent as a single `fillPhysicalHSpan` call.
5. **`fillRectDither`**: Replaced per-pixel `drawPixelDither` loops for DarkGray and LightGray with orientation-aware `fillPhysicalHSpanByte` calls using pre-computed byte patterns. DarkGray uses checkerboard `0xAA`/`0x55` alternating by physical row parity. LightGray uses 1-in-4 pattern with row-skipping optimization for all-white rows.
### Differences from upstream PR
- **`fillPolygon` landscape optimization (coderabbit nitpick)**: The upstream PR's coderabbit review noted that `fillPolygon`'s horizontal scanline inner loop could benefit from `fillPhysicalHSpan` for Landscape orientations. This was noted as a "future optimization opportunity" in the review and not implemented in the PR. We applied it here: for `LandscapeCounterClockwise` and `LandscapeClockwise`, the scanline fill now uses `fillPhysicalHSpan` with appropriate coordinate transforms, falling back to per-pixel for Portrait orientations (where horizontal logical lines map to vertical physical lines).
### Notable PR discussion
- jpirnay posted hardware benchmarks: `fillRect(480×800)` went from 125,577 µs to 519 µs (242× speedup), vertical `drawLine(800px)` from 261 µs to 3 µs (87×), `fillRectDither` DarkGray 234× speedup, LightGray 470× speedup.
- coderabbit review approved all core changes with 8 LGTM comments; the only nitpick was the `fillPolygon` optimization opportunity (applied in this port).
- The `fillRectDither` LightGray change has a subtle semantic difference from upstream: the original `drawPixelDither<LightGray>` only wrote dark pixels (leaving white untouched), while the new span-based approach explicitly writes the full pattern. For `fillRectDither` (which fills a complete rectangle) this is semantically equivalent.
---
## PR #1027: Reduce ParsedText layout time 79% via word-width cache and hyphenation early exit
- **URL:** [https://github.com/crosspoint-reader/crosspoint-reader/pull/1027](https://github.com/crosspoint-reader/crosspoint-reader/pull/1027)
- **Author:** jpirnay
- **Status in upstream:** Open (no approvals; osteotek requested separate benchmarks for cache vs early exit)
- **Method:** Manual port (1 file, adapted for vector-based code)
### Context
Reduces CPU time spent in `ParsedText::layoutAndExtractLines`, the hot path for every page render on the ESP32. Two independent optimizations contribute: a direct-mapped word-width cache and an early exit in the hyphenation breakpoint loop. Benchmarked on device (50 iterations, 474 px justified viewport) showing 59% improvement depending on corpus and layout mode.
### Changes applied
1. **Word-width cache** (`lib/Epub/Epub/ParsedText.cpp`): Added a 128-entry, 4 KB static array (BSS, not heap) that caches `getTextAdvanceX` results keyed by `(word, fontId, style)`. Lookup is O(1) via FNV-1a hash + bitmask slot selection. Words >= 24 bytes bypass the cache (uncommon, unlikely to repeat). Hyphen-fragment measurements (never repeated) skip the cache entirely. `calculateWordWidths` now calls `cachedMeasureWordWidth` instead of `measureWordWidth`.
2. **Hyphenation early exit** (`lib/Epub/Epub/ParsedText.cpp`): In `hyphenateWordAtIndex`, when a candidate prefix is wider than the available space, the loop now `break`s instead of `continue`ing. `Hyphenator::breakOffsets` returns candidates in ascending byte-offset order, so prefix widths are non-decreasing -- all subsequent candidates will also be too wide. A reusable `std::string prefix` buffer replaces per-iteration `substr` allocations.
3. **Reserve hint** (`lib/Epub/Epub/ParsedText.cpp`): Added `lineBreakIndices.reserve(totalWordCount / 8 + 1)` in `computeLineBreaks` to avoid repeated reallocation.
### Differences from upstream PR
- **List-specific optimizations not applicable**: The upstream PR includes `std::list` splice optimizations in `extractLine` and iterator changes (`std::advance` to `std::next`) throughout. Our mod already uses `std::vector` (from PR #1038), so these changes don't apply -- vector index access and move iterators are already in place.
- **`continuesVec` sync removed**: The upstream PR updates a separate `continuesVec` pointer in `hyphenateWordAtIndex`. Our mod modifies `wordContinues` directly (it's already a vector), so this indirection is unnecessary.
- **Benchmark infrastructure excluded**: The PR's final commit removed `ParsedTextLegacy.h/.cpp`, `ParsedTextBenchmark.h/.cpp`, and `main.cpp` benchmark hooks. These were development-only files not part of the deliverable.
### Notable PR discussion
- osteotek noted he had previously tried a word-width cache with "almost zero on-device improvements" and requested separate benchmarks for each optimization.
- jpirnay posted individual contribution data: the cache dominates (78% for DP layout, 34% for greedy) while the early exit contributes 12%. The cache saves a `getTextAdvanceX` call on every word in `calculateWordWidths` (5661 calls/iteration), whereas the early exit only fires on the handful of words per paragraph that trigger hyphenation.
- jpirnay's benchmark table (50 iterations, 474 px justified viewport):
| Scenario | Early exit only | Cache only (derived) | Both combined |
| --------------- | --------------- | -------------------- | ------------- |
| German DP | 1% | ~7% | 8% |
| English DP | 1% | ~8% | 9% |
| Combined DP | 1% | ~8% | 9% |
| German greedy | 2% | ~3% | 5% |
| English greedy | 2% | ~4% | 6% |
| Combined greedy | 2% | ~3% | 5% |
---
## PR #1068: Correct hyphenation of URLs
- **URL:** [https://github.com/crosspoint-reader/crosspoint-reader/pull/1068](https://github.com/crosspoint-reader/crosspoint-reader/pull/1068)
- **Author:** Uri-Tauber
- **Status in upstream:** Open (coderabbit reviewed with one nitpick, no approvals yet)
- **Related to:** upstream issue #1066 (Some epubs crash Crosspoint — long URLs cause rendering issues)
- **Method:** Manual port (2 files)
### Context
Long URLs in EPUBs could not be line-wrapped because `buildExplicitBreakInfos` required alphabetic characters on both sides of an explicit hyphen marker. URLs contain `/`, digits, dots, and other non-alphabetic characters, so path separators were never recognized as break points.
### Changes applied
1. **`lib/Epub/Epub/hyphenation/HyphenationCommon.cpp`**: Added `case '/':` to `isExplicitHyphen`, treating the URL path separator as an explicit hyphen delimiter alongside existing hyphen/dash characters.
2. **`lib/Epub/Epub/hyphenation/Hyphenator.cpp`**: Split the single combined filter in `buildExplicitBreakInfos` into a two-stage check. The `isExplicitHyphen` test is applied first. Then, for `/` and `-` specifically, the strict requirement that both adjacent codepoints must be alphabetic is relaxed — these separators can break next to digits, dots, or other URL characters. All other explicit hyphens retain the original TeX-style alphabetic-surround rule.
### Differences from upstream PR
- **Repeated-separator guard (mod enhancement)**: Added a guard from the coderabbit review nitpick (not yet addressed in the upstream PR) that skips break points between consecutive identical separators. This prevents a line break from being inserted between the two slashes in `http://` or between double hyphens like `--`.
### Notable PR discussion
- coderabbit approved the `isExplicitHyphen` change and flagged the repeated-separator edge case as a nitpick. The PR author has not yet addressed it.
- The PR resolves the URL portion of issue #1066, where MIT Press EPUBs with long URLs caused rendering problems.
---
## PR #1329: Refactor reader utils
- **URL:** [https://github.com/crosspoint-reader/crosspoint-reader/pull/1329](https://github.com/crosspoint-reader/crosspoint-reader/pull/1329)
- **Author:** upstream
- **Status in upstream:** Open (draft)
- **Method:** Manual port (3 files)
### Context
Both `EpubReaderActivity` and `TxtReaderActivity` contained duplicated logic for orientation switching, page-turn detection, refresh cycling, and grayscale anti-aliasing. This PR extracts shared reader utilities into a new `ReaderUtils.h` header.
### Changes applied
1. **`src/activities/reader/ReaderUtils.h`** (new): Created namespace `ReaderUtils` containing `GO_HOME_MS`, `applyOrientation()`, `PageTurnResult` + `detectPageTurn()`, `displayWithRefreshCycle()`, and `renderAntiAliased()` (template to avoid `std::function` overhead). Includes `storeBwBuffer()` null-check from CodeRabbit review.
2. **`src/activities/reader/EpubReaderActivity.cpp`**: Replaced local `applyReaderOrientation()`, `goHomeMs`, manual page-turn detection, and refresh cycling with `ReaderUtils::` equivalents.
3. **`src/activities/reader/TxtReaderActivity.cpp`**: Same delegation to `ReaderUtils::`, removing duplicated orientation switch, page-turn detection, and anti-aliasing code.
### Differences from upstream PR
- **CodeRabbit fix applied**: Added `storeBwBuffer()` return value check in `renderAntiAliased()` that upstream's draft doesn't yet include.
- **Mod's `applyOrientation` preserved**: The mod's `EpubReaderActivity::applyOrientation()` method (which handles settings persistence and section reset) was kept and internally calls `ReaderUtils::applyOrientation()` for the renderer orientation change.
---
## PR #1143 + #1172: TOC fragment navigation + multi-spine TOC
- **URL (1143):** [https://github.com/crosspoint-reader/crosspoint-reader/pull/1143](https://github.com/crosspoint-reader/crosspoint-reader/pull/1143)
- **URL (1172):** [https://github.com/crosspoint-reader/crosspoint-reader/pull/1172](https://github.com/crosspoint-reader/crosspoint-reader/pull/1172)
- **Author:** upstream
- **Status in upstream:** Both open (drafts)
- **Method:** Surgical feature extraction (7 files modified)
### Context
Many EPUBs have spine files containing multiple TOC chapters (e.g., short story collections, academic texts). The status bar showed incorrect chapter titles, chapter skip jumped entire spine items instead of TOC entries, and cross-spine chapter transitions required full re-indexing. These PRs add TOC-aware boundary tracking and multi-spine section caching.
### Changes applied
1. **`lib/Epub/Epub/Section.h`**: Added `TocBoundary` struct, `tocBoundaries` vector, `buildTocBoundaries()`, `getTocIndexForPage()`, `getPageForTocIndex()`, `getPageRangeForTocIndex()`, `readAnchorMap()`, `readCachedPageCount()`.
2. **`lib/Epub/Epub/Section.cpp`**: Implemented all new TOC boundary methods. Incremented `SECTION_FILE_VERSION` from 18 to 19. Integrated `buildTocBoundaries()` into both `loadSectionFile` and `createSectionFile`. Added static `readAnchorMap()` for lightweight section probing.
3. **`lib/Epub/Epub/parsers/ChapterHtmlSlimParser.h/.cpp`**: Added `tocAnchors` set and `tocAnchorPageMap`. Constructor accepts `tocAnchors` parameter. Forced page breaks at TOC anchor boundaries in `startNewTextBlock` so chapters start on fresh pages.
4. **`src/activities/ActivityResult.h`**: Added `tocIndex` to `ChapterResult`.
5. **`src/activities/reader/EpubReaderActivity.h/.cpp`**: Added `pendingTocIndex` for deferred cross-spine TOC navigation. Chapter skip (long-press) now walks TOC entries. Status bar uses `section->getTocIndexForPage()` for accurate subchapter title. Added `cacheMultiSpineChapter()` for proactive indexing of all spines in the same TOC chapter.
6. **`src/activities/reader/EpubReaderChapterSelectionActivity.h/.cpp`**: Added `currentTocIndex` parameter for precise pre-positioning. Returns `tocIndex` alongside `spineIndex` in `ChapterResult`.
### Differences from upstream PRs
- **Mod's footnote support preserved**: Upstream removed footnote navigation; the mod's `getPageForAnchor()`, footnote depth tracking, and `EpubReaderFootnotesActivity` are all retained.
- **Mod's image rendering preserved**: Upstream removed image rendering options; the mod's `imageRendering` parameter chain is preserved throughout Section, parser, and settings.
- **Activity base class retained**: Upstream adopted `ActivityWithSubactivity`; the mod keeps `Activity` as the base class for reader activities.
- **No `STR_INDEXING_PROGRESS` key**: Instead of adding a new translation key, the progress popup reuses `tr(STR_INDEXING)` with `snprintf` to append the numeric `(x/y)` progress, avoiding changes to 17+ YAML files.
- **Section file version**: Mod increments from v18 to v19 (upstream from v13 to v14). The mod's higher version reflects additional fields from previous mod-exclusive changes.
---
## PR #1320: JPEG resource cleanup
- **URL:** [https://github.com/crosspoint-reader/crosspoint-reader/pull/1320](https://github.com/crosspoint-reader/crosspoint-reader/pull/1320)
- **Author:** upstream
- **Status in upstream:** Open
- **Method:** Manual port (1 file)
### Context
`JpegToBmpConverter::convert()` had scattered `free()`/`delete` calls across multiple early-return paths, making it prone to resource leaks on error.
### Changes applied
1. **`lib/JpegToBmpConverter/JpegToBmpConverter.cpp`**: Introduced `ScopedCleanup` RAII struct that manages `rowBuffer`, `mcuRowBuffer`, `atkinsonDitherer`, `fsDitherer`, `atkinson1BitDitherer`, `rowAccum`, and `rowCount`. All pointers are initialized to `nullptr` and freed in the destructor. Removed scattered manual `free()`/`delete` calls. Changed `rowCount` from `uint16_t*` to `uint32_t*` to prevent overflow for wide images.
### Differences from upstream PR
- None -- clean port. The mod's `JpegToBmpConverter.cpp` had the same structure as upstream's pre-PR version.
---
## PR #1325: Settings tab label
- **URL:** [https://github.com/crosspoint-reader/crosspoint-reader/pull/1325](https://github.com/crosspoint-reader/crosspoint-reader/pull/1325)
- **Author:** upstream
- **Status in upstream:** Open
- **Method:** Manual port (1 file)
### Context
The settings screen's "Confirm" button showed a hardcoded "Toggle" label even when the tab bar was focused, where it should indicate the next category name.
### Changes applied
1. **`src/activities/settings/SettingsActivity.cpp`**: When `selectedSettingIndex == 0` (tab bar focused), the confirm button now shows the next category's name using `I18N.get(categoryNames[(selectedCategoryIndex + 1) % categoryCount])`. Otherwise, it shows the standard `tr(STR_TOGGLE)` label.
### Differences from upstream PR
- None -- clean port.