mirror of
https://github.com/crosspoint-reader/crosspoint-reader.git
synced 2026-08-13 14:21:57 -07:00
Compare commits
20
Commits
No files matched your search
@@ -63,7 +63,8 @@ jobs:
|
||||
- name: Run cppcheck
|
||||
run: pio check --fail-on-defect low --fail-on-defect medium --fail-on-defect high
|
||||
|
||||
build:
|
||||
build-default:
|
||||
name: Build default
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
@@ -90,14 +91,10 @@ jobs:
|
||||
key: pio-${{ runner.os }}-${{ hashFiles('platformio.ini') }}
|
||||
restore-keys: pio-${{ runner.os }}-
|
||||
|
||||
- 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.
|
||||
- name: Build default firmware
|
||||
run: |
|
||||
set -euo pipefail
|
||||
pio run -e default -e sticky | tee pio.log
|
||||
pio run -e default | tee pio.log
|
||||
|
||||
- name: Extract firmware stats
|
||||
|
||||
@@ -120,6 +117,52 @@ 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:
|
||||
@@ -152,7 +195,8 @@ jobs:
|
||||
test-status:
|
||||
name: Test Status
|
||||
needs:
|
||||
- build
|
||||
- build-default
|
||||
- build-sticky
|
||||
- clang-format
|
||||
- cppcheck
|
||||
- unit-tests
|
||||
|
||||
@@ -1,61 +0,0 @@
|
||||
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
|
||||
@@ -7,13 +7,6 @@ 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:
|
||||
@@ -39,27 +32,23 @@ jobs:
|
||||
key: pio-${{ runner.os }}-${{ hashFiles('platformio.ini') }}
|
||||
restore-keys: pio-${{ runner.os }}-
|
||||
|
||||
- name: Extract short SHA
|
||||
- name: Extract env
|
||||
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 ${{ 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"
|
||||
run: pio run -e gh_release_rc
|
||||
|
||||
- name: Upload Artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: ${{ matrix.artifact_name }}-rc+${{ env.SHORT_SHA }}
|
||||
name: CrossPoint-RC-${{ env.BRANCH_SUFFIX }}
|
||||
path: |
|
||||
.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
|
||||
.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
|
||||
+2
-2
@@ -29,6 +29,6 @@ lib/EpdFont/scripts/output/
|
||||
/.dummy
|
||||
/CMakeLists.txt
|
||||
/dependencies.lock
|
||||
/sdkconfig.default
|
||||
/sdkconfig.defaults
|
||||
/sdkconfig.*
|
||||
sdkconfig.sticky
|
||||
flake.*
|
||||
+179
-68
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1 @@
|
||||
/home/urit/Documents/CrossPoint/crosspoint-reader/.skills/SKILL.md
|
||||
+6
-4
@@ -10,9 +10,10 @@ 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: `.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.
|
||||
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.
|
||||
|
||||
## Setting Up a Dictionary
|
||||
|
||||
@@ -35,13 +36,14 @@ 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 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.
|
||||
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.
|
||||
|
||||
### How Lookup Works
|
||||
|
||||
1. **Direct match** — the word is found as-is (case-insensitive) in the dictionary index. Surrounding punctuation is ignored.
|
||||
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.
|
||||
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.
|
||||
|
||||
## The Definition Screen
|
||||
|
||||
|
||||
+10
-3
@@ -90,12 +90,19 @@ if (parsedSize != fileSize) {
|
||||
|
||||
## `section.bin`
|
||||
|
||||
### Version 35
|
||||
### Version 37
|
||||
|
||||
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.
|
||||
|
||||
@@ -135,10 +142,10 @@ import std.mem;
|
||||
import std.string;
|
||||
import std.core;
|
||||
|
||||
#define EXPECTED_VERSION 35
|
||||
#define EXPECTED_VERSION 37
|
||||
#define MAX_STRING_LENGTH 65535
|
||||
#define FOOTNOTE_NUMBER_LEN 32
|
||||
#define FOOTNOTE_HREF_LEN 96
|
||||
#define FOOTNOTE_HREF_LEN 256
|
||||
|
||||
struct String {
|
||||
u32 length [[hidden, comment("String byte length")]];
|
||||
|
||||
@@ -153,6 +153,7 @@ 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,6 +49,7 @@ 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)],
|
||||
|
||||
@@ -35,7 +35,7 @@ families:
|
||||
|
||||
- name: Literata
|
||||
description: "Screen-optimized serif (Latin, Greek, Cyrillic)"
|
||||
intervals: latin-ext,greek,cyrillic
|
||||
intervals: latin-ext,greek,cyrillic,ipa-chars
|
||||
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
|
||||
intervals: latin-ext,greek,cyrillic,ipa-chars
|
||||
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
|
||||
intervals: latin-ext,greek,cyrillic,ipa-chars
|
||||
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
|
||||
intervals: latin-ext,cyrillic,ipa-chars
|
||||
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
|
||||
intervals: latin-ext,cyrillic,ipa-chars
|
||||
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
|
||||
intervals: latin-ext,greek,cyrillic,ipa-chars
|
||||
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
|
||||
intervals: latin-ext,greek,cyrillic,ipa-chars
|
||||
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
|
||||
intervals: latin-ext,cyrillic,ipa-chars
|
||||
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
|
||||
intervals: latin-ext,greek,cyrillic,ipa-chars
|
||||
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
|
||||
intervals: latin-ext,ipa-chars
|
||||
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
|
||||
intervals: latin-ext,greek,cyrillic,ipa-chars
|
||||
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
|
||||
intervals: latin-ext,greek,cyrillic,georgian,armenian,ethiopic,ipa-chars
|
||||
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
|
||||
intervals: latin-ext,greek,cyrillic,ipa-chars
|
||||
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
|
||||
intervals: latin-ext,greek,cyrillic,ipa-chars
|
||||
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
|
||||
intervals: latin-ext,greek,cyrillic,ipa-chars
|
||||
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
|
||||
intervals: latin-ext,greek,cyrillic,ipa-chars
|
||||
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
|
||||
intervals: latin-ext,greek,cyrillic,ipa-chars
|
||||
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
|
||||
intervals: latin-ext,ipa-chars
|
||||
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
|
||||
intervals: latin-ext,ipa-chars
|
||||
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
|
||||
intervals: latin-ext,ipa-chars
|
||||
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
|
||||
intervals: latin-ext,ipa-chars
|
||||
sizes: [12, 14, 16, 18]
|
||||
styles:
|
||||
regular: {url: "https://raw.githubusercontent.com/jacobxperez/lexica-ultralegible/main/fonts/ttf/LexicaUltralegible-Regular.ttf"}
|
||||
|
||||
+6
-1
@@ -802,7 +802,12 @@ int Epub::getSpineItemsCount() const {
|
||||
return bookMetadataCache->getSpineCount();
|
||||
}
|
||||
|
||||
size_t Epub::getCumulativeSpineItemSize(const int spineIndex) const { return getSpineItem(spineIndex).cumulativeSize; }
|
||||
size_t Epub::getCumulativeSpineItemSize(const int spineIndex) const {
|
||||
if (!bookMetadataCache || !bookMetadataCache->isLoaded()) {
|
||||
return 0;
|
||||
}
|
||||
return bookMetadataCache->getCumulativeSize(spineIndex);
|
||||
}
|
||||
|
||||
BookMetadataCache::SpineEntry Epub::getSpineItem(const int spineIndex) const {
|
||||
if (!bookMetadataCache || !bookMetadataCache->isLoaded()) {
|
||||
|
||||
@@ -481,11 +481,30 @@ 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");
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
#include <deque>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
class BookMetadataCache {
|
||||
public:
|
||||
@@ -62,6 +63,11 @@ 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
|
||||
@@ -113,6 +119,9 @@ 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; }
|
||||
|
||||
@@ -3,7 +3,10 @@
|
||||
#include <cstring>
|
||||
|
||||
#define FOOTNOTE_NUMBER_LEN 32
|
||||
#define FOOTNOTE_HREF_LEN 96
|
||||
#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).
|
||||
|
||||
struct FootnoteEntry {
|
||||
char number[FOOTNOTE_NUMBER_LEN];
|
||||
|
||||
@@ -681,6 +681,76 @@ 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());
|
||||
@@ -691,9 +761,6 @@ 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;
|
||||
@@ -730,9 +797,10 @@ 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] += g.leftOverlap;
|
||||
wordWidths[g.start - 1] += g.leftOverlap;
|
||||
} else {
|
||||
wordWidths[g.start] += std::max(0, g.leftOverlap - maxOverhang);
|
||||
const int maxLeftOverhang = wordWidths[g.start - 1] / 2;
|
||||
wordWidths[g.start - 1] += std::max(0, g.leftOverlap - maxLeftOverhang);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -752,7 +820,8 @@ std::vector<uint16_t> ParsedText::calculateWordWidths(const GfxRenderer& rendere
|
||||
if (isCjkIdeograph(cpNext)) {
|
||||
wordWidths[g.start + g.count - 1] += g.rightOverlap;
|
||||
} else {
|
||||
wordWidths[g.start + g.count - 1] += std::max(0, g.rightOverlap - maxOverhang);
|
||||
const int maxRightOverhang = wordWidths[nextIdx] / 2;
|
||||
wordWidths[g.start + g.count - 1] += std::max(0, g.rightOverlap - maxRightOverhang);
|
||||
}
|
||||
|
||||
// Check if there is another ruby group further ahead separated only by non-ideographs
|
||||
@@ -769,8 +838,10 @@ std::vector<uint16_t> ParsedText::calculateWordWidths(const GfxRenderer& rendere
|
||||
gapWidth += wordWidths[k];
|
||||
}
|
||||
if (onlyNonIdeographsInBetween) {
|
||||
const int allowedRight = std::min(g.rightOverlap, maxOverhang);
|
||||
const int allowedLeft = std::min(nextG.leftOverlap, maxOverhang);
|
||||
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 touchOverlap = allowedRight + allowedLeft - gapWidth;
|
||||
if (touchOverlap > 0) {
|
||||
wordWidths[g.start + g.count - 1] += touchOverlap;
|
||||
@@ -837,36 +908,7 @@ std::vector<size_t> ParsedText::computeLineBreaks(const GfxRenderer& renderer, c
|
||||
}
|
||||
|
||||
// Calculate extraStartOffset for the first word on the line (i) (protect left margin)
|
||||
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;
|
||||
}
|
||||
}
|
||||
const int extraStartOffset = (j == i) ? calculateRubyExtraStartOffset(i, totalWordCount, renderer, fontId) : 0;
|
||||
|
||||
currlen += wordWidths[j] + gap + (j == i ? extraStartOffset : 0);
|
||||
|
||||
@@ -879,46 +921,7 @@ std::vector<size_t> ParsedText::computeLineBreaks(const GfxRenderer& renderer, c
|
||||
continue;
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
const int extraEndOffset = calculateRubyExtraEndOffset(i, j + 1, renderer, fontId);
|
||||
|
||||
if (currlen + extraEndOffset > effectivePageWidth) {
|
||||
continue; // Cannot split here as it would overflow the right margin
|
||||
@@ -939,7 +942,9 @@ std::vector<size_t> ParsedText::computeLineBreaks(const GfxRenderer& renderer, c
|
||||
}
|
||||
}
|
||||
|
||||
if (cost < dp[i]) {
|
||||
// Favor longer lines when line-breaking costs are equal, to avoid unnecessary short lines in Chinese and Japanese
|
||||
// text.
|
||||
if (cost <= dp[i]) {
|
||||
dp[i] = cost;
|
||||
ans[i] = j; // j is the index of the last word in this optimal line
|
||||
}
|
||||
@@ -1171,6 +1176,9 @@ 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;
|
||||
@@ -1225,8 +1233,9 @@ 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
|
||||
const int spareSpace = effectivePageWidth - lineWordWidthSum - totalNaturalGaps;
|
||||
// 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;
|
||||
const int justifyExtra = (effectiveAlignment == CssTextAlign::Justify && !isLastLine)
|
||||
? computeJustifyExtra(spareSpace, actualGapCount)
|
||||
: 0;
|
||||
@@ -1307,7 +1316,8 @@ void ParsedText::extractLine(const size_t breakIndex, const int pageWidth, const
|
||||
}
|
||||
}
|
||||
|
||||
const int reorderedSpare = effectivePageWidth - reorderedWordWidthSum - reorderedNaturalGaps;
|
||||
const int reorderedSpare =
|
||||
effectivePageWidth - extraStartOffset - extraEndOffset - reorderedWordWidthSum - reorderedNaturalGaps;
|
||||
const int reorderedJustifyExtra = (effectiveAlignment == CssTextAlign::Justify && !isLastLine)
|
||||
? computeJustifyExtra(reorderedSpare, reorderedGapCount)
|
||||
: 0;
|
||||
@@ -1412,7 +1422,7 @@ void ParsedText::extractLine(const size_t breakIndex, const int pageWidth, const
|
||||
}
|
||||
} else {
|
||||
// LTR: position words from left to right
|
||||
int xpos = firstLineIndent;
|
||||
int xpos = firstLineIndent + extraStartOffset;
|
||||
if (effectiveAlignment == CssTextAlign::Right) {
|
||||
xpos = effectivePageWidth - lineWordWidthSum - totalNaturalGaps;
|
||||
} else if (effectiveAlignment == CssTextAlign::Center) {
|
||||
|
||||
@@ -59,6 +59,9 @@ 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,
|
||||
|
||||
@@ -31,7 +31,12 @@ 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.
|
||||
constexpr uint8_t SECTION_FILE_VERSION = 35;
|
||||
// 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;
|
||||
// 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
|
||||
|
||||
@@ -357,6 +357,7 @@ 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
|
||||
}
|
||||
|
||||
@@ -419,6 +420,7 @@ void ImageBlock::render(GfxRenderer& renderer, const int x, const int y) {
|
||||
return;
|
||||
}
|
||||
|
||||
renderer.preserveImagePolarity(x, y, width, height);
|
||||
LOG_DBG("IMG", "Decode successful");
|
||||
}
|
||||
|
||||
|
||||
@@ -135,86 +135,32 @@ void TextBlock::render(const GfxRenderer& renderer, const int fontId, const int
|
||||
const bool scanning = renderer.isFontCacheScanning();
|
||||
const int ascender = renderer.getFontAscenderSize(fontId);
|
||||
|
||||
// Resolve ruby collisions left-to-right to prevent adjacent ruby texts from overlapping
|
||||
// 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.
|
||||
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 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(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;
|
||||
}
|
||||
const int leaderWordX = xposArr[i] + x;
|
||||
const auto baseDir =
|
||||
static_cast<BidiUtils::BidiBaseDir>(BidiUtils::detectParagraphLevel(wordText(i), blockStyle.isRtl ? 1 : 0));
|
||||
rubies[i] = {leaderWordX - (rubyWidth - groupActualWidth) / 2, rubyTexts[i], baseDir};
|
||||
i += groupWordCount - 1;
|
||||
}
|
||||
}
|
||||
@@ -275,7 +221,7 @@ void TextBlock::render(const GfxRenderer& renderer, const int fontId, const int
|
||||
wordY += ascender / 4;
|
||||
}
|
||||
|
||||
const int drawX = wordX + (blockHasRuby ? wordShiftArr[i] : 0);
|
||||
const int drawX = wordX;
|
||||
|
||||
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
Reference in new issue
Block a user