Compare commits

..
Author SHA1 Message Date
Julia Nguyen af9d262097 ci: update release artifacts 2026-08-05 11:02:21 -04:00
105 changed files with 1337 additions and 3513 deletions

No files matched your search

+8 -52
View File
@@ -63,8 +63,7 @@ jobs:
- name: Run cppcheck
run: pio check --fail-on-defect low --fail-on-defect medium --fail-on-defect high
build-default:
name: Build default
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
@@ -91,10 +90,14 @@ jobs:
key: pio-${{ runner.os }}-${{ hashFiles('platformio.ini') }}
restore-keys: pio-${{ runner.os }}-
- name: Build default firmware
- name: Build CrossPoint
# Both envs in ONE pio invocation: a second `pio run` can wipe the whole
# .pio/build tree (project-checksum mismatch after the S3 toolchain
# installs into the restored cache), deleting the default firmware.bin
# before the artifact upload.
run: |
set -euo pipefail
pio run -e default | tee pio.log
pio run -e default -e sticky | tee pio.log
- name: Extract firmware stats
@@ -117,52 +120,6 @@ jobs:
path: .pio/build/default/firmware.bin
if-no-files-found: error
build-sticky:
name: Build sticky
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
with:
submodules: recursive
- uses: actions/setup-python@v6
with:
python-version: '3.14'
- name: Install uv
uses: astral-sh/setup-uv@v7
with:
version: "latest"
enable-cache: false
- name: Install PlatformIO Core
run: uv pip install --system -U https://github.com/pioarduino/platformio-core/archive/refs/tags/v6.1.19.zip
- name: Cache PlatformIO packages
uses: actions/cache@v4
with:
path: ~/.platformio
key: pio-${{ runner.os }}-${{ hashFiles('platformio.ini') }}
restore-keys: pio-${{ runner.os }}-
- name: Build sticky firmware
run: |
set -euo pipefail
pio run -e sticky | tee pio.log
- name: Extract firmware stats
run: |
set -euo pipefail
ram_line="$(grep -E "RAM:\\s" -m1 pio.log || true)"
flash_line="$(grep -E "Flash:\\s" -m1 pio.log || true)"
echo "ram_line=${ram_line}" >> "$GITHUB_OUTPUT"
echo "flash_line=${flash_line}" >> "$GITHUB_OUTPUT"
{
echo "## Sticky firmware build stats"
if [ -n "$ram_line" ]; then echo "- ${ram_line}"; else echo "- RAM: not found"; fi
if [ -n "$flash_line" ]; then echo "- ${flash_line}"; else echo "- Flash: not found"; fi
} >> "$GITHUB_STEP_SUMMARY"
unit-tests:
runs-on: ubuntu-latest
steps:
@@ -195,8 +152,7 @@ jobs:
test-status:
name: Test Status
needs:
- build-default
- build-sticky
- build
- clang-format
- cppcheck
- unit-tests
+61
View File
@@ -0,0 +1,61 @@
name: Compile Release
on:
push:
tags:
- '*'
jobs:
build-release:
runs-on: ubuntu-latest
strategy:
matrix:
include:
- pio_env: gh_release
artifact_name: firmware-x4_x3
- pio_env: sticky
artifact_name: firmware-sticky
steps:
- uses: actions/checkout@v6
with:
submodules: recursive
- uses: actions/setup-python@v6
with:
python-version: '3.14'
- name: Install uv
uses: astral-sh/setup-uv@v7
with:
version: "latest"
enable-cache: false
- name: Install PlatformIO Core
run: uv pip install --system -U https://github.com/pioarduino/platformio-core/archive/refs/tags/v6.1.19.zip
- name: Cache PlatformIO packages
uses: actions/cache@v4
with:
path: ~/.platformio
key: pio-${{ runner.os }}-${{ hashFiles('platformio.ini') }}
restore-keys: pio-${{ runner.os }}-
- name: Build CrossPoint
env:
CROSSPOINT_RELEASE_BUILD: "1"
run: pio run -e ${{ matrix.pio_env }} -j1
- name: Name Release Firmware
run: |
cp ".pio/build/${{ matrix.pio_env }}/firmware.bin" \
".pio/build/${{ matrix.pio_env }}/${{ matrix.artifact_name }}.bin"
- name: Upload Artifacts
uses: actions/upload-artifact@v4
with:
name: ${{ matrix.artifact_name }}
path: |
.pio/build/${{ matrix.pio_env }}/bootloader.bin
.pio/build/${{ matrix.pio_env }}/${{ matrix.artifact_name }}.bin
.pio/build/${{ matrix.pio_env }}/firmware.elf
.pio/build/${{ matrix.pio_env }}/firmware.map
.pio/build/${{ matrix.pio_env }}/partitions.bin
+20 -9
View File
@@ -7,6 +7,13 @@ jobs:
build-release-candidate:
if: startsWith(github.ref_name, 'release/')
runs-on: ubuntu-latest
strategy:
matrix:
include:
- pio_env: gh_release_rc
artifact_name: firmware-x4_x3
- pio_env: sticky
artifact_name: firmware-sticky
steps:
- uses: actions/checkout@v6
with:
@@ -32,23 +39,27 @@ jobs:
key: pio-${{ runner.os }}-${{ hashFiles('platformio.ini') }}
restore-keys: pio-${{ runner.os }}-
- name: Extract env
- name: Extract short SHA
run: |
echo "SHORT_SHA=${GITHUB_SHA::7}" >> $GITHUB_ENV
echo "BRANCH_SUFFIX=${GITHUB_REF_NAME#release/}" >> $GITHUB_ENV
- name: Build CrossPoint Release Candidate
env:
CROSSPOINT_RC_HASH: ${{ env.SHORT_SHA }}
run: pio run -e gh_release_rc
run: pio run -e ${{ matrix.pio_env }} -j1
- name: Name Release Candidate Firmware
run: |
cp ".pio/build/${{ matrix.pio_env }}/firmware.bin" \
".pio/build/${{ matrix.pio_env }}/${{ matrix.artifact_name }}-rc+${{ env.SHORT_SHA }}.bin"
- name: Upload Artifacts
uses: actions/upload-artifact@v4
with:
name: CrossPoint-RC-${{ env.BRANCH_SUFFIX }}
name: ${{ matrix.artifact_name }}-rc+${{ env.SHORT_SHA }}
path: |
.pio/build/gh_release_rc/bootloader.bin
.pio/build/gh_release_rc/firmware.bin
.pio/build/gh_release_rc/firmware.elf
.pio/build/gh_release_rc/firmware.map
.pio/build/gh_release_rc/partitions.bin
.pio/build/${{ matrix.pio_env }}/bootloader.bin
.pio/build/${{ matrix.pio_env }}/${{ matrix.artifact_name }}-rc+${{ env.SHORT_SHA }}.bin
.pio/build/${{ matrix.pio_env }}/firmware.elf
.pio/build/${{ matrix.pio_env }}/firmware.map
.pio/build/${{ matrix.pio_env }}/partitions.bin
+2 -2
View File
@@ -29,6 +29,6 @@ lib/EpdFont/scripts/output/
/.dummy
/CMakeLists.txt
/dependencies.lock
/sdkconfig.*
/sdkconfig.default
/sdkconfig.defaults
sdkconfig.sticky
flake.*
+68 -179
View File
File diff suppressed because it is too large. Load diff
-1
View File
@@ -1 +0,0 @@
/home/urit/Documents/CrossPoint/crosspoint-reader/.skills/SKILL.md
+4 -6
View File
@@ -10,10 +10,9 @@ A dictionary folder must contain:
- `.idx` — word index (required, **must be uncompressed** — a `.idx.gz` will not work; decompress it on your computer with `gzip -d` first)
- `.dict` or `.dict.dz` — definition data (`.dict.dz` is supported as-is; entries are decompressed on the fly during lookup)
- `.syn` — synonym index (optional; maps alternate spellings and irregular forms to their headword)
- `.ifo` — metadata (optional)
Not supported: dictionaries with 64-bit index offsets (`idxoffsetbits=64` in the `.ifo` — rare, and rejected with an error), and HTML-formatted definitions render as raw markup rather than styled text.
Not supported: `.syn` synonym files (ignored), dictionaries with 64-bit index offsets (`idxoffsetbits=64` in the `.ifo` — rare, and rejected with an error), and HTML-formatted definitions render as raw markup rather than styled text.
## Setting Up a Dictionary
@@ -36,14 +35,13 @@ One word on the page becomes highlighted:
2. Press **Confirm** to look up the highlighted word.
3. Press **Back** to return to the reader.
On the very first lookup with a dictionary (and again whenever the `.idx` or `.syn` source file changes), the reader shows *"Indexing dictionary…"* while it builds small sidecar files next to them — a `.qidx` for the word index, and a `.sidx` when a `.syn` synonym file is present. Each sidecar is rebuilt independently, only when its own source changes. This takes a few seconds for large dictionaries and makes all subsequent lookups fast. The sidecars can be deleted safely at any time — they will simply be rebuilt.
On the very first lookup with a dictionary (and again if the dictionary file changes), the reader shows *"Indexing dictionary…"* while it builds a small `.qidx` sidecar file next to the `.idx`. This takes a few seconds for large dictionaries and makes all subsequent lookups fast. The sidecar can be deleted safely at any time — it will simply be rebuilt.
### How Lookup Works
1. **Direct match** — the word is found as-is (case-insensitive) in the dictionary index. Surrounding punctuation is ignored.
2. **Synonyms** — on a miss, if the dictionary ships a `.syn` file, alternate spellings and irregular forms recorded there are resolved to their headword (e.g. `oxen` → `ox`, `colour` → `color`). This step is skipped if the `.sidx` sidecar could not be built (e.g. transient low memory during indexing); the dictionary otherwise stays usable, and the build is retried the next time it is opened.
3. **Stemming** — still no match: common English word forms are retried automatically: possessives and plurals (`dogs` → `dog`, `stories` → `story`) and verb endings (`walked` → `walk`, `running` → `run`, `making` → `make`).
4. **Not found** — a short popup appears and you return to word selection.
2. **Stemming** — on a miss, common English word forms are retried automatically: possessives and plurals (`dogs` → `dog`, `stories` → `story`) and verb endings (`walked` → `walk`, `running` → `run`, `making` → `make`).
3. **Not found** — a short popup appears and you return to word selection.
## The Definition Screen
+3 -10
View File
@@ -90,19 +90,12 @@ if (parsedSize != fileSize) {
## `section.bin`
### Version 37
### Version 35
Each file in `sections/*.bin` stores one laid-out spine section. The header is
also the cache-busting key: if any layout-affecting setting differs from the
current reader settings, the section is discarded and rebuilt.
Version 37 increases the fixed-size footnote href field from 96 to 256 bytes.
This changes each serialized footnote record from 128 to 288 bytes, so older
section caches must be discarded and rebuilt.
Version 36 invalidates cached word positions after ruby and CJK justification
layout changes.
Version 35 adds a header offset and a `uint32_t` entry per page for the
visible-text offset LUT. The other section LUTs remain unchanged.
@@ -142,10 +135,10 @@ import std.mem;
import std.string;
import std.core;
#define EXPECTED_VERSION 37
#define EXPECTED_VERSION 35
#define MAX_STRING_LENGTH 65535
#define FOOTNOTE_NUMBER_LEN 32
#define FOOTNOTE_HREF_LEN 256
#define FOOTNOTE_HREF_LEN 96
struct String {
u32 length [[hidden, comment("String byte length")]];
-1
View File
@@ -153,7 +153,6 @@ To convert your own TrueType/OpenType fonts:
| `armenian` | Armenian |
| `ethiopic` | Ethiopic + Extended |
| `vietnamese` | Vietnamese subset (ơ/ư and combining marks) |
| `ipa-chars` | IPA Extensions + Spacing Modifier Letters (phonetic transcription) |
| `punctuation` | General punctuation (U+2000–U+206F) |
| `cjk` | CJK Unified Ideographs + Hiragana + Katakana + Fullwidth |
| `hangul` | Korean Hangul syllables + Jamo + Compatibility Jamo |
@@ -49,7 +49,6 @@ INTERVAL_PRESETS = {
"armenian": [(0x0530, 0x058F)],
"ethiopic": [(0x1200, 0x137F), (0x1380, 0x139F), (0x2D80, 0x2DDF)],
"vietnamese": [(0x01A0, 0x01B0), (0x1EA0, 0x1EF9)],
"ipa-chars": [(0x0250, 0x02AF), (0x02B0, 0x02FF)],
"punctuation": [(0x2000, 0x206F)],
"cjk": [(0x3000, 0x303F), (0x3040, 0x309F), (0x30A0, 0x30FF),
(0x4E00, 0x9FFF), (0xF900, 0xFAFF), (0xFF00, 0xFFEF)],
+21 -21
View File
@@ -35,7 +35,7 @@ families:
- name: Literata
description: "Screen-optimized serif (Latin, Greek, Cyrillic)"
intervals: latin-ext,greek,cyrillic,ipa-chars
intervals: latin-ext,greek,cyrillic
sizes: [12, 14, 16, 18]
styles:
regular: {url: "https://raw.githubusercontent.com/googlefonts/literata/main/fonts/ttf/Literata-Regular.ttf"}
@@ -45,7 +45,7 @@ families:
- name: SourceSerif4
description: "Adobe transitional serif (Latin, Greek, Cyrillic)"
intervals: latin-ext,greek,cyrillic,ipa-chars
intervals: latin-ext,greek,cyrillic
sizes: [12, 14, 16, 18]
styles:
regular: {url: "https://raw.githubusercontent.com/adobe-fonts/source-serif/release/TTF/SourceSerif4-Regular.ttf"}
@@ -55,7 +55,7 @@ families:
- name: NotoSerifExtended
description: "Serif (Latin, Greek, Cyrillic)"
intervals: latin-ext,greek,cyrillic,ipa-chars
intervals: latin-ext,greek,cyrillic
sizes: [12, 14, 16, 18]
styles:
regular: {url: "https://raw.githubusercontent.com/notofonts/NotoSerif/main/fonts/ttf/unhinted/instance_ttf/NotoSerif-Regular.ttf"}
@@ -65,7 +65,7 @@ families:
- name: Merriweather
description: "Warm serif for long-form reading (Latin, Cyrillic)"
intervals: latin-ext,cyrillic,ipa-chars
intervals: latin-ext,cyrillic
sizes: [12, 14, 16, 18]
styles:
regular: {url: "https://raw.githubusercontent.com/SorkinType/Merriweather/master/fonts/ttf/Merriweather-Regular.ttf"}
@@ -75,7 +75,7 @@ families:
- name: Lora
description: "Calligraphic serif for literary reading (Latin, Cyrillic)"
intervals: latin-ext,cyrillic,ipa-chars
intervals: latin-ext,cyrillic
sizes: [12, 14, 16, 18]
styles:
regular: {url: "https://raw.githubusercontent.com/cyrealtype/Lora-Cyrillic/main/fonts/ttf/Lora-Regular.ttf"}
@@ -85,7 +85,7 @@ families:
- name: GentiumBookPlus
description: "Scholarly serif with wide Unicode coverage (Latin, Greek, Cyrillic, IPA)"
intervals: latin-ext,greek,cyrillic,ipa-chars
intervals: latin-ext,greek,cyrillic
sizes: [12, 14, 16, 18]
styles:
regular: {url: "https://raw.githubusercontent.com/google/fonts/main/ofl/gentiumbookplus/GentiumBookPlus-Regular.ttf"}
@@ -95,7 +95,7 @@ families:
- name: IBMPlexSerif
description: "Professional serif (Latin, Greek, Cyrillic)"
intervals: latin-ext,greek,cyrillic,ipa-chars
intervals: latin-ext,greek,cyrillic
sizes: [12, 14, 16, 18]
styles:
regular: {url: "https://raw.githubusercontent.com/google/fonts/main/ofl/ibmplexserif/IBMPlexSerif-Regular.ttf"}
@@ -105,7 +105,7 @@ families:
- name: Bitter
description: "Slab serif designed for e-ink (Latin, Cyrillic)"
intervals: latin-ext,cyrillic,ipa-chars
intervals: latin-ext,cyrillic
sizes: [12, 14, 16, 18]
styles:
regular: {url: "https://raw.githubusercontent.com/google/fonts/main/ofl/bitter/Bitter%5Bwght%5D.ttf", variable: {wght: 500}}
@@ -115,7 +115,7 @@ families:
- name: Domitian
description: "A humanist serif for literary reading (Latin, Greek, Cyrillic)"
intervals: latin-ext,greek,cyrillic,ipa-chars
intervals: latin-ext,greek,cyrillic
sizes: [12, 14, 16, 18]
styles:
regular: {url: "https://mirrors.ctan.org/fonts/domitian/opentype/Domitian-Roman.otf"}
@@ -125,7 +125,7 @@ families:
- name: LibreBaskerville
description: "A serif reimplementing the classic Baskerville (Latin)"
intervals: latin-ext,ipa-chars
intervals: latin-ext
sizes: [12, 14, 16, 18]
styles:
regular: {url: "https://raw.githubusercontent.com/impallari/Libre-Baskerville/master/fonts/ttf/LibreBaskerville-Regular.ttf"}
@@ -135,7 +135,7 @@ families:
- name: Vollkorn
description: "A serif for bread and butter use by Friedrich Althausen (Latin, Greek, Cyrillic)"
intervals: latin-ext,greek,cyrillic,ipa-chars
intervals: latin-ext,greek,cyrillic
sizes: [12, 14, 16, 18]
styles:
regular: {url: "https://raw.githubusercontent.com/google/fonts/main/ofl/vollkorn/Vollkorn%5Bwght%5D.ttf", variable: {wght: 400}}
@@ -147,7 +147,7 @@ families:
- name: NotoSansExtended
description: "Sans-serif (Latin, Greek, Cyrillic, Georgian, Armenian, Ethiopic)"
intervals: latin-ext,greek,cyrillic,georgian,armenian,ethiopic,ipa-chars
intervals: latin-ext,greek,cyrillic,georgian,armenian,ethiopic
sizes: [12, 14, 16, 18]
styles:
regular: {path: "builtinFonts/source/NotoSans/NotoSans-Regular.ttf"}
@@ -157,7 +157,7 @@ families:
- name: Inter
description: "Modern sans-serif (Latin, Greek, Cyrillic)"
intervals: latin-ext,greek,cyrillic,ipa-chars
intervals: latin-ext,greek,cyrillic
sizes: [12, 14, 16, 18]
styles:
regular: {url: "https://raw.githubusercontent.com/google/fonts/main/ofl/inter/Inter%5Bopsz%2Cwght%5D.ttf", variable: {wght: 400, opsz: 14}}
@@ -167,7 +167,7 @@ families:
- name: SourceSans3
description: "Adobe humanist sans-serif (Latin, Greek, Cyrillic)"
intervals: latin-ext,greek,cyrillic,ipa-chars
intervals: latin-ext,greek,cyrillic
sizes: [12, 14, 16, 18]
styles:
regular: {url: "https://raw.githubusercontent.com/adobe-fonts/source-sans/release/TTF/SourceSans3-Regular.ttf"}
@@ -177,7 +177,7 @@ families:
- name: IBMPlexSans
description: "IBM corporate sans-serif (Latin, Greek, Cyrillic)"
intervals: latin-ext,greek,cyrillic,ipa-chars
intervals: latin-ext,greek,cyrillic
sizes: [12, 14, 16, 18]
styles:
regular: {url: "https://raw.githubusercontent.com/IBM/plex/master/packages/plex-sans/fonts/complete/ttf/IBMPlexSans-Regular.ttf"}
@@ -187,7 +187,7 @@ families:
- name: Alegreya
description: "Calligraphic serif/display (Latin, Greek, Cyrillic)"
intervals: latin-ext,greek,cyrillic,ipa-chars
intervals: latin-ext,greek,cyrillic
sizes: [12, 14, 16, 18]
styles:
regular: {url: "https://raw.githubusercontent.com/google/fonts/main/ofl/alegreya/Alegreya%5Bwght%5D.ttf", variable: {wght: 400}}
@@ -199,7 +199,7 @@ families:
- name: IBMPlexMono
description: "Monospace for code and technical reading (Latin, Greek, Cyrillic)"
intervals: latin-ext,greek,cyrillic,ipa-chars
intervals: latin-ext,greek,cyrillic
sizes: [12, 14, 16, 18]
styles:
regular: {url: "https://raw.githubusercontent.com/google/fonts/main/ofl/ibmplexmono/IBMPlexMono-Regular.ttf"}
@@ -209,7 +209,7 @@ families:
- name: SourceCodePro
description: "Adobe monospace with excellent hinting (Latin)"
intervals: latin-ext,ipa-chars
intervals: latin-ext
sizes: [12, 14, 16, 18]
styles:
regular: {url: "https://raw.githubusercontent.com/adobe-fonts/source-code-pro/release/TTF/SourceCodePro-Regular.ttf"}
@@ -221,7 +221,7 @@ families:
- name: OpenDyslexic
description: "Dyslexia-friendly font (Latin)"
intervals: latin-ext,ipa-chars
intervals: latin-ext
sizes: [8, 10, 12, 14]
styles:
regular: {path: "builtinFonts/source/OpenDyslexic/OpenDyslexic-Regular.otf"}
@@ -233,7 +233,7 @@ families:
- name: AtkinsonHyperlegibleNext
description: "Accessibility font for low vision (Latin)"
intervals: latin-ext,ipa-chars
intervals: latin-ext
sizes: [12, 14, 16, 18]
styles:
regular: {url: "https://raw.githubusercontent.com/googlefonts/atkinson-hyperlegible-next/main/fonts/ttf/AtkinsonHyperlegibleNext-Regular.ttf"}
@@ -243,7 +243,7 @@ families:
- name: LexicaUltralegible
description: "Accessibility font for low vision / dyslexia (Latin)"
intervals: latin-ext,ipa-chars
intervals: latin-ext
sizes: [12, 14, 16, 18]
styles:
regular: {url: "https://raw.githubusercontent.com/jacobxperez/lexica-ultralegible/main/fonts/ttf/LexicaUltralegible-Regular.ttf"}
+1 -6
View File
@@ -802,12 +802,7 @@ int Epub::getSpineItemsCount() const {
return bookMetadataCache->getSpineCount();
}
size_t Epub::getCumulativeSpineItemSize(const int spineIndex) const {
if (!bookMetadataCache || !bookMetadataCache->isLoaded()) {
return 0;
}
return bookMetadataCache->getCumulativeSize(spineIndex);
}
size_t Epub::getCumulativeSpineItemSize(const int spineIndex) const { return getSpineItem(spineIndex).cumulativeSize; }
BookMetadataCache::SpineEntry Epub::getSpineItem(const int spineIndex) const {
if (!bookMetadataCache || !bookMetadataCache->isLoaded()) {
-19
View File
@@ -481,30 +481,11 @@ bool BookMetadataCache::load() {
serialization::readString(bookFile, coreMetadata.coverItemHref);
serialization::readString(bookFile, coreMetadata.textReferenceHref);
// Cache cumulative spine sizes in RAM. The progress bar (every render) and percent
// jumps otherwise pay 2 seeks + a heap-allocating SpineEntry read per access. Spine
// entries are stored contiguously in index order immediately after the LUTs, so read
// them in a single sequential pass.
cumulativeSizes.clear();
cumulativeSizes.reserve(spineCount);
const uint32_t lutSize = (static_cast<uint32_t>(spineCount) + tocCount) * sizeof(uint32_t);
bookFile.seek(lutOffset + lutSize);
for (uint16_t i = 0; i < spineCount; i++) {
cumulativeSizes.push_back(readSpineEntry(bookFile).cumulativeSize);
}
loaded = true;
LOG_DBG("BMC", "Loaded cache data: %d spine, %d TOC entries", spineCount, tocCount);
return true;
}
uint32_t BookMetadataCache::getCumulativeSize(const int index) const {
if (index < 0 || index >= static_cast<int>(cumulativeSizes.size())) {
return 0;
}
return cumulativeSizes[index];
}
BookMetadataCache::SpineEntry BookMetadataCache::getSpineEntry(const int index) {
if (!loaded) {
LOG_ERR("BMC", "getSpineEntry called but cache not loaded");
-9
View File
@@ -7,7 +7,6 @@
#include <deque>
#include <memory>
#include <string>
#include <vector>
class BookMetadataCache {
public:
@@ -63,11 +62,6 @@ class BookMetadataCache {
// wrapper serves whichever pass is active (spine, then toc).
std::unique_ptr<serialization::BufferedFileWriter> passOut;
// Cumulative spine sizes, cached in RAM at load() so progress/percent lookups are
// O(1) instead of 2 seeks + a heap-allocating SpineEntry read per access (4 bytes
// per spine item; <1KB for typical books).
std::vector<uint32_t> cumulativeSizes;
// Index for fast href→spineIndex lookup (used only for large EPUBs)
struct SpineHrefIndexEntry {
uint64_t hrefHash; // FNV-1a 64-bit hash
@@ -119,9 +113,6 @@ class BookMetadataCache {
bool load();
SpineEntry getSpineEntry(int index);
TocEntry getTocEntry(int index);
// Cumulative byte size up to and including the given spine item (0 if out of range
// or not loaded). Backed by the in-RAM cumulativeSizes cache populated in load().
uint32_t getCumulativeSize(int index) const;
int getSpineCount() const { return spineCount; }
int getTocCount() const { return tocCount; }
bool isLoaded() const { return loaded; }
+1 -4
View File
@@ -3,10 +3,7 @@
#include <cstring>
#define FOOTNOTE_NUMBER_LEN 32
#define FOOTNOTE_HREF_LEN 256
// ponytail: bumped from 96 to 256; calibre-generated EPUBs with long filenames
// and URL-encoded characters routinely exceed 96 chars (e.g.
// "Author-Title_split_NNN.html#_ftnN" encoded is ~150 chars).
#define FOOTNOTE_HREF_LEN 96
struct FootnoteEntry {
char number[FOOTNOTE_NUMBER_LEN];
+83 -93
View File
@@ -681,76 +681,6 @@ static inline bool isCjkIdeograph(uint32_t cp) {
(cp >= 0x20000 && cp <= 0x3FFFF);
}
// The first word of a line may have its ruby characters wider than the word (the base text). In that case, we need to
// move the base text to the right a bit so that ruby text doesn't overflow the left border, and it is still centered
// over the base text. This function calculates how much we need to move the base text to the right.
int ParsedText::calculateRubyExtraStartOffset(const size_t wordIdx, const size_t maxWordIdx,
const GfxRenderer& renderer, const int fontId) const {
if (rubyTexts.empty() || wordIdx >= rubyTexts.size() || rubyTexts[wordIdx].empty() ||
(wordStyles[wordIdx] & EpdFontFamily::RUBY_CONTINUE) != 0) {
return 0;
}
size_t groupWordCount = 1;
while (wordIdx + groupWordCount < maxWordIdx &&
(wordStyles[wordIdx + groupWordCount] & EpdFontFamily::RUBY_CONTINUE) != 0) {
groupWordCount++;
}
int groupActualWidth = 0;
for (size_t k = 0; k < groupWordCount; ++k) {
groupActualWidth += measureWordWidth(renderer, fontId, words[wordIdx + k], wordStyles[wordIdx + k]);
}
const int rubyWidth = renderer.getTextAdvanceX(fontId, rubyTexts[wordIdx].c_str(), EpdFontFamily::SUP);
if (rubyWidth <= groupActualWidth) {
return 0;
}
const int leftOverlap = (rubyWidth - groupActualWidth) / 2;
// This function is only ever called for the first word of a line.
// words[wordIdx - 1], if it exists, is always the last word of the *prior* line
// and cannot absorb any left overhang on the current line.
// The full leftOverlap must therefore be reserved as a visual indent so the
// ruby text does not overflow the left margin.
return leftOverlap;
}
// The last ruby group on a line may have its ruby characters wider than the group's base text.
// The right half of that overhang protrudes past the last base character. This function returns
// the amount of right-margin space that must be reserved so the ruby does not overflow the right
// border. It mirrors calculateRubyExtraStartOffset: words[lineBreak] is on the *next* line and
// cannot absorb any of the right overhang on the current line, so the full rightOverlap is returned.
int ParsedText::calculateRubyExtraEndOffset(const size_t lineStartIdx, const size_t lineBreakIdx,
const GfxRenderer& renderer, const int fontId) const {
if (rubyTexts.empty() || lineBreakIdx == 0 || lineStartIdx >= lineBreakIdx) {
return 0;
}
// Walk backwards from the last word to find the leader of the last ruby group on the line.
size_t leaderIdx = lineBreakIdx - 1;
while (leaderIdx > lineStartIdx && (wordStyles[leaderIdx] & EpdFontFamily::RUBY_CONTINUE) != 0) {
leaderIdx--;
}
// leaderIdx must be a ruby group leader (non-empty ruby, no RUBY_CONTINUE flag).
if (leaderIdx >= rubyTexts.size() || rubyTexts[leaderIdx].empty() ||
(wordStyles[leaderIdx] & EpdFontFamily::RUBY_CONTINUE) != 0) {
return 0;
}
// Measure the group.
int groupActualWidth = 0;
for (size_t k = leaderIdx; k < lineBreakIdx; ++k) {
groupActualWidth += measureWordWidth(renderer, fontId, words[k], wordStyles[k]);
}
const int rubyWidth = renderer.getTextAdvanceX(fontId, rubyTexts[leaderIdx].c_str(), EpdFontFamily::SUP);
if (rubyWidth <= groupActualWidth) {
return 0;
}
return (rubyWidth - groupActualWidth) / 2;
}
std::vector<uint16_t> ParsedText::calculateWordWidths(const GfxRenderer& renderer, const int fontId) {
std::vector<uint16_t> wordWidths;
wordWidths.reserve(words.size());
@@ -761,6 +691,9 @@ std::vector<uint16_t> ParsedText::calculateWordWidths(const GfxRenderer& rendere
// Adjust widths for ruby groups to comply with JLReq standards
if (!rubyTexts.empty()) {
const int ascender = renderer.getFontAscenderSize(fontId);
const int maxOverhang = ascender / 2; // Maximum overhang over non-ideographic characters (Kana, punctuation, etc.)
struct RubyGroupInfo {
size_t start;
size_t count;
@@ -797,10 +730,9 @@ std::vector<uint16_t> ParsedText::calculateWordWidths(const GfxRenderer& rendere
if (g.start > 0) {
const uint32_t cpPrev = lastCodepoint(words[g.start - 1]);
if (isCjkIdeograph(cpPrev)) {
wordWidths[g.start - 1] += g.leftOverlap;
wordWidths[g.start] += g.leftOverlap;
} else {
const int maxLeftOverhang = wordWidths[g.start - 1] / 2;
wordWidths[g.start - 1] += std::max(0, g.leftOverlap - maxLeftOverhang);
wordWidths[g.start] += std::max(0, g.leftOverlap - maxOverhang);
}
}
@@ -820,8 +752,7 @@ std::vector<uint16_t> ParsedText::calculateWordWidths(const GfxRenderer& rendere
if (isCjkIdeograph(cpNext)) {
wordWidths[g.start + g.count - 1] += g.rightOverlap;
} else {
const int maxRightOverhang = wordWidths[nextIdx] / 2;
wordWidths[g.start + g.count - 1] += std::max(0, g.rightOverlap - maxRightOverhang);
wordWidths[g.start + g.count - 1] += std::max(0, g.rightOverlap - maxOverhang);
}
// Check if there is another ruby group further ahead separated only by non-ideographs
@@ -838,10 +769,8 @@ std::vector<uint16_t> ParsedText::calculateWordWidths(const GfxRenderer& rendere
gapWidth += wordWidths[k];
}
if (onlyNonIdeographsInBetween) {
const int maxRightOverhang = wordWidths[g.start + g.count - 1] / 2;
const int maxLeftOverhang = wordWidths[nextG.start - 1] / 2;
const int allowedRight = std::min(g.rightOverlap, maxRightOverhang);
const int allowedLeft = std::min(nextG.leftOverlap, maxLeftOverhang);
const int allowedRight = std::min(g.rightOverlap, maxOverhang);
const int allowedLeft = std::min(nextG.leftOverlap, maxOverhang);
const int touchOverlap = allowedRight + allowedLeft - gapWidth;
if (touchOverlap > 0) {
wordWidths[g.start + g.count - 1] += touchOverlap;
@@ -908,7 +837,36 @@ std::vector<size_t> ParsedText::computeLineBreaks(const GfxRenderer& renderer, c
}
// Calculate extraStartOffset for the first word on the line (i) (protect left margin)
const int extraStartOffset = (j == i) ? calculateRubyExtraStartOffset(i, totalWordCount, renderer, fontId) : 0;
int extraStartOffset = 0;
if (j == i && !rubyTexts.empty() && i < rubyTexts.size() && !rubyTexts[i].empty() &&
(wordStyles[i] & EpdFontFamily::RUBY_CONTINUE) == 0) {
int groupWordCount = 1;
while (i + groupWordCount < totalWordCount &&
(wordStyles[i + groupWordCount] & EpdFontFamily::RUBY_CONTINUE) != 0) {
groupWordCount++;
}
int groupActualWidth = 0;
for (int k = 0; k < groupWordCount; ++k) {
groupActualWidth += measureWordWidth(renderer, fontId, words[i + k], wordStyles[i + k]);
}
const int rubyWidth = renderer.getTextAdvanceX(fontId, rubyTexts[i].c_str(), EpdFontFamily::SUP);
if (rubyWidth > groupActualWidth) {
const int leftOverlap = (rubyWidth - groupActualWidth) / 2;
const int ascender = renderer.getFontAscenderSize(fontId);
const int maxOverhang = ascender / 2;
int expansion_added = 0;
if (i > 0) {
const uint32_t cpPrev = lastCodepoint(words[i - 1]);
if (isCjkIdeograph(cpPrev)) {
expansion_added = leftOverlap;
} else {
expansion_added = std::max(0, leftOverlap - maxOverhang);
}
}
extraStartOffset = leftOverlap - expansion_added;
}
}
currlen += wordWidths[j] + gap + (j == i ? extraStartOffset : 0);
@@ -921,7 +879,46 @@ std::vector<size_t> ParsedText::computeLineBreaks(const GfxRenderer& renderer, c
continue;
}
const int extraEndOffset = calculateRubyExtraEndOffset(i, j + 1, renderer, fontId);
// Calculate extraEndOffset if we break after word j (protect right margin)
int extraEndOffset = 0;
if (!rubyTexts.empty() && j < rubyTexts.size()) {
bool isLastOfGroup = false;
size_t leaderIdx = j;
while (true) {
if (!rubyTexts[leaderIdx].empty() && (wordStyles[leaderIdx] & EpdFontFamily::RUBY_CONTINUE) == 0) {
isLastOfGroup = true;
break;
}
if (leaderIdx == 0 || (wordStyles[leaderIdx] & EpdFontFamily::RUBY_CONTINUE) == 0) {
break;
}
leaderIdx--;
}
if (isLastOfGroup && (j + 1 == totalWordCount || (wordStyles[j + 1] & EpdFontFamily::RUBY_CONTINUE) == 0)) {
int groupActualWidth = 0;
for (size_t k = leaderIdx; k <= j; ++k) {
groupActualWidth += measureWordWidth(renderer, fontId, words[k], wordStyles[k]);
}
const int rubyWidth = renderer.getTextAdvanceX(fontId, rubyTexts[leaderIdx].c_str(), EpdFontFamily::SUP);
if (rubyWidth > groupActualWidth) {
const int rightOverlap = (rubyWidth - groupActualWidth) / 2;
const int ascender = renderer.getFontAscenderSize(fontId);
const int maxOverhang = ascender / 2;
int expansion_added = 0;
if (j + 1 < totalWordCount) {
const uint32_t cpNext = firstCodepoint(words[j + 1]);
if (isCjkIdeograph(cpNext)) {
expansion_added = rightOverlap;
} else {
expansion_added = std::max(0, rightOverlap - maxOverhang);
}
}
extraEndOffset = rightOverlap - expansion_added;
}
}
}
if (currlen + extraEndOffset > effectivePageWidth) {
continue; // Cannot split here as it would overflow the right margin
@@ -942,9 +939,7 @@ std::vector<size_t> ParsedText::computeLineBreaks(const GfxRenderer& renderer, c
}
}
// Favor longer lines when line-breaking costs are equal, to avoid unnecessary short lines in Chinese and Japanese
// text.
if (cost <= dp[i]) {
if (cost < dp[i]) {
dp[i] = cost;
ans[i] = j; // j is the index of the last word in this optimal line
}
@@ -1176,9 +1171,6 @@ void ParsedText::extractLine(const size_t breakIndex, const int pageWidth, const
std::copy(rubyTexts.begin() + lastBreakAt, rubyTexts.begin() + lastBreakAt + copyCount, lineRubyTexts.begin());
}
const int extraStartOffset = calculateRubyExtraStartOffset(lastBreakAt, lineBreak, renderer, fontId);
const int extraEndOffset = calculateRubyExtraEndOffset(lastBreakAt, lineBreak, renderer, fontId);
std::vector<std::string> lineWords;
lineWords.reserve(lineWordCount);
std::vector<EpdFontFamily::Style> lineWordStyles;
@@ -1233,9 +1225,8 @@ void ParsedText::extractLine(const size_t breakIndex, const int pageWidth, const
? CssTextAlign::Right
: blockStyle.alignment;
// For justified text, compute per-gap extra to distribute remaining space evenly.
// extraEndOffset reserves space for any ruby group at the right edge of the line.
const int spareSpace = effectivePageWidth - extraStartOffset - extraEndOffset - lineWordWidthSum - totalNaturalGaps;
// For justified text, compute per-gap extra to distribute remaining space evenly
const int spareSpace = effectivePageWidth - lineWordWidthSum - totalNaturalGaps;
const int justifyExtra = (effectiveAlignment == CssTextAlign::Justify && !isLastLine)
? computeJustifyExtra(spareSpace, actualGapCount)
: 0;
@@ -1316,8 +1307,7 @@ void ParsedText::extractLine(const size_t breakIndex, const int pageWidth, const
}
}
const int reorderedSpare =
effectivePageWidth - extraStartOffset - extraEndOffset - reorderedWordWidthSum - reorderedNaturalGaps;
const int reorderedSpare = effectivePageWidth - reorderedWordWidthSum - reorderedNaturalGaps;
const int reorderedJustifyExtra = (effectiveAlignment == CssTextAlign::Justify && !isLastLine)
? computeJustifyExtra(reorderedSpare, reorderedGapCount)
: 0;
@@ -1422,7 +1412,7 @@ void ParsedText::extractLine(const size_t breakIndex, const int pageWidth, const
}
} else {
// LTR: position words from left to right
int xpos = firstLineIndent + extraStartOffset;
int xpos = firstLineIndent;
if (effectiveAlignment == CssTextAlign::Right) {
xpos = effectivePageWidth - lineWordWidthSum - totalNaturalGaps;
} else if (effectiveAlignment == CssTextAlign::Center) {
-3
View File
@@ -59,9 +59,6 @@ class ParsedText {
void pushVisibleOffset(uint32_t offset);
void insertVisibleOffset(size_t wordIndex, uint32_t offset);
void eraseVisibleOffsetPrefix(size_t count);
int calculateRubyExtraStartOffset(size_t wordIdx, size_t maxWordIdx, const GfxRenderer& renderer, int fontId) const;
int calculateRubyExtraEndOffset(size_t lineStartIdx, size_t lineBreakIdx, const GfxRenderer& renderer,
int fontId) const;
int resolveFirstLineIndent(bool isFirstLine, const GfxRenderer& renderer, int fontId) const;
std::vector<size_t> computeLineBreaks(const GfxRenderer& renderer, int fontId, int pageWidth,
std::vector<uint16_t>& wordWidths, std::vector<bool>& continuesVec,
+1 -6
View File
@@ -31,12 +31,7 @@ namespace {
// match. Keeps <br>-per-paragraph books (common CJK formatting) from
// re-adding container spacing at every paragraph.
// v35: Persist a uint32_t visible-text start offset for every page.
// v36: Ruby and CJK justification layout changes invalidate cached word positions.
// v37: Footnote href records grew from 96 to 256 bytes.
// v38: Image top margin is clamped so a full-viewport-height image cannot
// overflow the page bottom; older caches can hold placements that panels
// with no bottom inset refuse to draw.
constexpr uint8_t SECTION_FILE_VERSION = 38;
constexpr uint8_t SECTION_FILE_VERSION = 35;
// Written into the version field while a build is in progress; patched to
// SECTION_FILE_VERSION only when the build is finalized. An abandoned /
// crash-interrupted .bin therefore carries version 0, which loadSectionFile rejects
-2
View File
@@ -357,7 +357,6 @@ void ImageBlock::render(GfxRenderer& renderer, const int x, const int y) {
// Try to render from cache first
std::string cachePath = getCachePath(imagePath);
if (renderFromCache(renderer, cachePath, x, y, width, height)) {
renderer.preserveImagePolarity(x, y, width, height);
return; // Successfully rendered from cache
}
@@ -420,7 +419,6 @@ void ImageBlock::render(GfxRenderer& renderer, const int x, const int y) {
return;
}
renderer.preserveImagePolarity(x, y, width, height);
LOG_DBG("IMG", "Decode successful");
}
+60 -6
View File
@@ -135,32 +135,86 @@ void TextBlock::render(const GfxRenderer& renderer, const int fontId, const int
const bool scanning = renderer.isFontCacheScanning();
const int ascender = renderer.getFontAscenderSize(fontId);
// Resolve ruby positions. Layout (extractLine) has already reserved extraStartOffset on the
// left and extraEndOffset on the right, so the centered rubyX is always within the page margins.
// Resolve ruby collisions left-to-right to prevent adjacent ruby texts from overlapping
struct RubyDrawInfo {
int x;
int width;
std::string text;
BidiUtils::BidiBaseDir baseDir;
};
// hasRuby() is an O(numWords) scan, so resolve it once here rather than per word.
// Both arrays below are only ever read when the line carries ruby, so they stay
// empty (zero allocations) for the ruby-less case, which is every line of a
// non-CJK book. Sized lazily inside the branch.
const bool blockHasRuby = hasRuby();
std::vector<int> wordShiftArr;
std::vector<RubyDrawInfo> rubies;
if (blockHasRuby) {
wordShiftArr.assign(numWords, 0);
rubies.resize(numWords);
int accumulatedShift = 0;
int lastEnd = -9999;
for (uint16_t i = 0; i < numWords; i++) {
wordShiftArr[i] = accumulatedShift;
if (i < rubyTexts.size() && !rubyTexts[i].empty() && (wordStyle(i) & EpdFontFamily::RUBY_CONTINUE) == 0) {
// Find the group size (how many words are part of this ruby annotation)
int groupWordCount = 1;
while (i + groupWordCount < numWords && (wordStyle(i + groupWordCount) & EpdFontFamily::RUBY_CONTINUE) != 0) {
groupWordCount++;
}
// Compute actual width for the group
int groupActualWidth = 0;
for (int k = 0; k < groupWordCount; ++k) {
groupActualWidth += renderer.getTextAdvanceX(fontId, wordText(i + k), wordStyle(i + k));
}
const int rubyWidth = renderer.getTextAdvanceX(fontId, rubyTexts[i].c_str(), EpdFontFamily::SUP);
const char* word = wordText(i);
const int leaderWordX = xposArr[i] + x;
const int leaderWordX_shifted = leaderWordX + accumulatedShift;
const auto baseDir =
static_cast<BidiUtils::BidiBaseDir>(BidiUtils::detectParagraphLevel(wordText(i), blockStyle.isRtl ? 1 : 0));
rubies[i] = {leaderWordX - (rubyWidth - groupActualWidth) / 2, rubyTexts[i], baseDir};
static_cast<BidiUtils::BidiBaseDir>(BidiUtils::detectParagraphLevel(word, blockStyle.isRtl ? 1 : 0));
const int rubyWidth = renderer.getTextAdvanceX(fontId, rubyTexts[i].c_str(), EpdFontFamily::SUP);
const int screenWidth = renderer.getScreenWidth();
int rubyX = 0;
int groupDrawX = 0;
if (rubyWidth > groupActualWidth) {
rubyX = leaderWordX_shifted - (rubyWidth - groupActualWidth) / 2;
if (i == 0) {
rubyX = std::max(leaderWordX_shifted, rubyX);
}
if (rubyX < lastEnd) {
rubyX = lastEnd;
}
groupDrawX = rubyX + (rubyWidth - groupActualWidth) / 2;
} else {
groupDrawX = leaderWordX_shifted;
rubyX = groupDrawX + (groupActualWidth - rubyWidth) / 2;
if (i == 0) {
rubyX = std::max(leaderWordX_shifted, rubyX);
}
if (rubyX < lastEnd) {
const int push = lastEnd - rubyX;
rubyX = lastEnd;
groupDrawX += push;
}
}
rubyX = std::max(0, std::min(rubyX, screenWidth - rubyWidth));
// Keep groupDrawX aligned if rubyX was clamped by screen edges
if (rubyWidth > groupActualWidth) {
groupDrawX = rubyX + (rubyWidth - groupActualWidth) / 2;
}
rubies[i] = {rubyX, rubyWidth, rubyTexts[i], baseDir};
lastEnd = rubyX + rubyWidth;
// Propagate shift to all words in the group and subsequent words
const int groupShift = groupDrawX - leaderWordX;
accumulatedShift = groupShift;
for (int k = 0; k < groupWordCount; ++k) {
wordShiftArr[i + k] = accumulatedShift;
}
i += groupWordCount - 1;
}
}
@@ -221,7 +275,7 @@ void TextBlock::render(const GfxRenderer& renderer, const int fontId, const int
wordY += ascender / 4;
}
const int drawX = wordX;
const int drawX = wordX + (blockHasRuby ? wordShiftArr[i] : 0);
if (boundary > 0) {
// Focus split: draw bold prefix, then the regular suffix at a pre-computed x offset.
Loaded 20 of 105 files, more files were not shown because too many files have changed in this diff. Show more