mirror of
https://github.com/crosspoint-reader/crosspoint-reader.git
synced 2026-08-13 14:21:57 -07:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
27c4be8a34 |
No files matched your search
@@ -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
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
name: Compile Release
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- '*'
|
||||
|
||||
jobs:
|
||||
build-release:
|
||||
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 CrossPoint
|
||||
run: pio run -e gh_release
|
||||
|
||||
- name: Upload Artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: CrossPoint-${{ github.ref_name }}
|
||||
path: |
|
||||
.pio/build/gh_release/bootloader.bin
|
||||
.pio/build/gh_release/firmware.bin
|
||||
.pio/build/gh_release/firmware.elf
|
||||
.pio/build/gh_release/firmware.map
|
||||
.pio/build/gh_release/partitions.bin
|
||||
+2
-2
@@ -29,6 +29,6 @@ lib/EpdFont/scripts/output/
|
||||
/.dummy
|
||||
/CMakeLists.txt
|
||||
/dependencies.lock
|
||||
/sdkconfig.*
|
||||
/sdkconfig.default
|
||||
/sdkconfig.defaults
|
||||
sdkconfig.sticky
|
||||
flake.*
|
||||
+68
-179
File diff suppressed because it is too large.
Load diff
@@ -1 +0,0 @@
|
||||
/home/urit/Documents/CrossPoint/crosspoint-reader/.skills/SKILL.md
|
||||
+4
-6
@@ -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
@@ -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")]];
|
||||
|
||||
@@ -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 |
|
||||
|
||||
+74
-19
@@ -99,8 +99,11 @@ void SdCardFont::freeStyleMiniData(PerStyle& s) {
|
||||
s.miniIntervals = nullptr;
|
||||
delete[] s.miniGlyphs;
|
||||
s.miniGlyphs = nullptr;
|
||||
delete[] s.miniBitmap;
|
||||
s.miniBitmap = nullptr;
|
||||
for (auto& chunk : s.miniBitmapChunks) {
|
||||
delete[] chunk;
|
||||
chunk = nullptr;
|
||||
}
|
||||
s.miniBitmapChunkCount = 0;
|
||||
s.miniIntervalCount = 0;
|
||||
s.miniGlyphCount = 0;
|
||||
s.miniIntervalCapacity = 0;
|
||||
@@ -1010,30 +1013,66 @@ int SdCardFont::prewarmStyle(uint8_t styleIdx, const uint32_t* codepoints, uint3
|
||||
totalBitmapSize += s.miniGlyphs[i].dataLength;
|
||||
}
|
||||
|
||||
if (!ensureArrayCapacity(s.miniBitmap, s.miniBitmapCapacity, totalBitmapSize)) {
|
||||
LOG_ERR("SDCF", "Failed to allocate mini bitmap (%u bytes) for style %u", totalBitmapSize, styleIdx);
|
||||
delete[] readOrder;
|
||||
delete[] mappings;
|
||||
freeStyleMiniData(s);
|
||||
return static_cast<int>(cpCount);
|
||||
}
|
||||
s.miniBitmapUsed = totalBitmapSize; // underuse-hysteresis signal for resetStyleMiniData
|
||||
|
||||
// Read bitmap data sorted by file offset
|
||||
std::sort(readOrder, readOrder + validCount,
|
||||
[&](uint32_t a, uint32_t b) { return s.miniGlyphs[a].dataOffset < s.miniGlyphs[b].dataOffset; });
|
||||
|
||||
uint32_t miniBitmapOffset = 0;
|
||||
// Assemble the bitmap arena from on-demand 4 KB chunks (see MINI_BM_CHUNK_*).
|
||||
// `span` is the running virtual offset into the arena; each glyph is placed
|
||||
// wholly within one chunk, skipping past a boundary rather than straddling it.
|
||||
// glyph.dataOffset becomes the virtual offset, decoded at render time by
|
||||
// miniGlyphBitmap(). Chunks are allocated only as span reaches them, so a
|
||||
// sparse page never allocates the full ceiling.
|
||||
uint32_t span = 0;
|
||||
uint32_t lastBitmapEnd = UINT32_MAX;
|
||||
for (uint32_t i = 0; i < validCount; i++) {
|
||||
uint32_t mapIdx = readOrder[i];
|
||||
EpdGlyph& glyph = s.miniGlyphs[mapIdx];
|
||||
const uint32_t len = glyph.dataLength;
|
||||
|
||||
if (glyph.dataLength == 0) {
|
||||
glyph.dataOffset = miniBitmapOffset;
|
||||
if (len == 0) {
|
||||
glyph.dataOffset = span;
|
||||
continue;
|
||||
}
|
||||
if (len > MINI_BM_CHUNK_SIZE) {
|
||||
// A single glyph larger than one chunk cannot be placed. Real fonts never
|
||||
// hit this (a 4 KB 2bpp glyph would be enormous); bail rather than corrupt.
|
||||
LOG_ERR("SDCF", "Prewarm: glyph %u B exceeds chunk %u B (style %u)", len, MINI_BM_CHUNK_SIZE, styleIdx);
|
||||
file.close();
|
||||
delete[] readOrder;
|
||||
delete[] mappings;
|
||||
freeStyleMiniData(s);
|
||||
return static_cast<int>(cpCount);
|
||||
}
|
||||
|
||||
// Skip to the next chunk boundary if this glyph would straddle one.
|
||||
const uint32_t within = span & (MINI_BM_CHUNK_SIZE - 1);
|
||||
if (within != 0 && within + len > MINI_BM_CHUNK_SIZE) {
|
||||
span += MINI_BM_CHUNK_SIZE - within;
|
||||
}
|
||||
const uint32_t chunkIdx = span >> MINI_BM_CHUNK_SHIFT;
|
||||
if (chunkIdx >= MINI_BM_MAX_CHUNKS) {
|
||||
LOG_ERR("SDCF", "Prewarm: mini bitmap needs > %u chunks (style %u)", MINI_BM_MAX_CHUNKS, styleIdx);
|
||||
file.close();
|
||||
delete[] readOrder;
|
||||
delete[] mappings;
|
||||
freeStyleMiniData(s);
|
||||
return static_cast<int>(cpCount);
|
||||
}
|
||||
if (!s.miniBitmapChunks[chunkIdx]) {
|
||||
s.miniBitmapChunks[chunkIdx] = new (std::nothrow) uint8_t[MINI_BM_CHUNK_SIZE];
|
||||
if (!s.miniBitmapChunks[chunkIdx]) {
|
||||
LOG_ERR("SDCF", "Failed to allocate mini bitmap chunk %u (style %u)", chunkIdx, styleIdx);
|
||||
file.close();
|
||||
delete[] readOrder;
|
||||
delete[] mappings;
|
||||
freeStyleMiniData(s);
|
||||
return static_cast<int>(cpCount);
|
||||
}
|
||||
if (chunkIdx + 1 > s.miniBitmapChunkCount) s.miniBitmapChunkCount = chunkIdx + 1;
|
||||
}
|
||||
|
||||
const uint32_t off = span & (MINI_BM_CHUNK_SIZE - 1);
|
||||
uint32_t fileOff = s.bitmapFileOffset + glyph.dataOffset;
|
||||
if (fileOff != lastBitmapEnd) {
|
||||
if (!file.seekSet(fileOff)) {
|
||||
@@ -1046,18 +1085,21 @@ int SdCardFont::prewarmStyle(uint8_t styleIdx, const uint32_t* codepoints, uint3
|
||||
}
|
||||
seekCount++;
|
||||
}
|
||||
if (file.read(s.miniBitmap + miniBitmapOffset, glyph.dataLength) != static_cast<int>(glyph.dataLength)) {
|
||||
if (file.read(s.miniBitmapChunks[chunkIdx] + off, len) != static_cast<int>(len)) {
|
||||
LOG_ERR("SDCF", "Prewarm: short bitmap read (style %u)", styleIdx);
|
||||
delete[] readOrder;
|
||||
delete[] mappings;
|
||||
freeStyleMiniData(s);
|
||||
return static_cast<int>(cpCount);
|
||||
}
|
||||
lastBitmapEnd = fileOff + glyph.dataLength;
|
||||
lastBitmapEnd = fileOff + len;
|
||||
|
||||
glyph.dataOffset = miniBitmapOffset;
|
||||
miniBitmapOffset += glyph.dataLength;
|
||||
glyph.dataOffset = span;
|
||||
span += len;
|
||||
}
|
||||
// Footprint including inter-chunk padding: the underuse-hysteresis signal.
|
||||
s.miniBitmapUsed = span;
|
||||
s.miniBitmapCapacity = s.miniBitmapChunkCount * MINI_BM_CHUNK_SIZE;
|
||||
}
|
||||
|
||||
uint32_t sdTime = millis() - sdStart;
|
||||
@@ -1080,7 +1122,10 @@ int SdCardFont::prewarmStyle(uint8_t styleIdx, const uint32_t* codepoints, uint3
|
||||
s.miniMetadataOnly = metadataOnly;
|
||||
s.miniHysteresisPending = !metadataOnly; // one hysteresis evaluation per rebuild
|
||||
memset(&s.miniData, 0, sizeof(s.miniData));
|
||||
s.miniData.bitmap = s.miniBitmap;
|
||||
// The mini bitmap is chunked (non-contiguous), so there is no single base
|
||||
// pointer. Prewarmed SD glyphs are resolved via SdCardFont::miniGlyphBitmap()
|
||||
// in the renderer; leave bitmap null so any stray &bitmap[dataOffset] faults.
|
||||
s.miniData.bitmap = nullptr;
|
||||
s.miniData.glyph = s.miniGlyphs;
|
||||
s.miniData.intervals = s.miniIntervals;
|
||||
s.miniData.intervalCount = s.miniIntervalCount;
|
||||
@@ -1517,4 +1562,14 @@ const uint8_t* SdCardFont::getOverflowBitmap(const EpdGlyph* glyph) const {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const uint8_t* SdCardFont::miniGlyphBitmap(const void* ctx, uint32_t dataOffset) const {
|
||||
const auto* octx = static_cast<const OverflowContext*>(ctx);
|
||||
const PerStyle& s = styles_[octx->styleIdx];
|
||||
const uint32_t chunkIdx = dataOffset >> MINI_BM_CHUNK_SHIFT;
|
||||
if (chunkIdx >= s.miniBitmapChunkCount) return nullptr;
|
||||
const uint8_t* chunk = s.miniBitmapChunks[chunkIdx];
|
||||
if (!chunk) return nullptr;
|
||||
return chunk + (dataOffset & (MINI_BM_CHUNK_SIZE - 1));
|
||||
}
|
||||
|
||||
SdCardFont* SdCardFont::fromMissCtx(void* ctx) { return static_cast<OverflowContext*>(ctx)->self; }
|
||||
@@ -94,6 +94,12 @@ class SdCardFont {
|
||||
// Returns the bitmap for an on-demand-loaded (overflow) glyph.
|
||||
const uint8_t* getOverflowBitmap(const EpdGlyph* glyph) const;
|
||||
|
||||
// Resolves a prewarmed mini glyph's chunked bitmap. `ctx` is the glyphMissCtx
|
||||
// (an OverflowContext identifying the style); `dataOffset` is the glyph's
|
||||
// virtual offset into the style's chunked arena. Returns nullptr if the chunk
|
||||
// is absent or out of range. Called by GfxRenderer::getGlyphBitmap().
|
||||
const uint8_t* miniGlyphBitmap(const void* ctx, uint32_t dataOffset) const;
|
||||
|
||||
// Extract SdCardFont* from an opaque glyphMissCtx pointer.
|
||||
// Used by GfxRenderer::getGlyphBitmap() to recover the SdCardFont from EpdFontData::glyphMissCtx.
|
||||
static SdCardFont* fromMissCtx(void* ctx);
|
||||
@@ -129,6 +135,18 @@ class SdCardFont {
|
||||
uint8_t ligaturePairCount = 0;
|
||||
};
|
||||
|
||||
// The per-style mini bitmap arena is stored as a list of fixed-size chunks
|
||||
// rather than one contiguous block. A whole page's 2bpp glyph bitmaps can run
|
||||
// tens of KB; on a fragmented heap a single contiguous allocation of that size
|
||||
// fails even when the same bytes are available as several smaller free blocks.
|
||||
// Chunking lets the arena be assembled from blocks the allocator can actually
|
||||
// provide. Each glyph's bitmap is placed wholly within one chunk (never
|
||||
// straddling a boundary), so a glyph is addressed by a virtual offset that
|
||||
// maps to (chunk index, offset-in-chunk) via miniGlyphBitmap().
|
||||
static constexpr uint32_t MINI_BM_CHUNK_SHIFT = 12; // 4 KB chunks
|
||||
static constexpr uint32_t MINI_BM_CHUNK_SIZE = 1u << MINI_BM_CHUNK_SHIFT;
|
||||
static constexpr uint32_t MINI_BM_MAX_CHUNKS = 24; // 96 KB ceiling per style/page
|
||||
|
||||
// All per-style data: file offsets, intervals, kern/lig, prewarm cache, EpdFont
|
||||
struct PerStyle {
|
||||
CpFontHeader header{};
|
||||
@@ -188,7 +206,10 @@ class SdCardFont {
|
||||
EpdFontData miniData{};
|
||||
EpdUnicodeInterval* miniIntervals = nullptr;
|
||||
EpdGlyph* miniGlyphs = nullptr;
|
||||
uint8_t* miniBitmap = nullptr;
|
||||
// Chunked mini bitmap arena (see MINI_BM_CHUNK_* above). Chunks are allocated
|
||||
// on demand during prewarm; miniBitmapChunkCount is how many are live.
|
||||
uint8_t* miniBitmapChunks[MINI_BM_MAX_CHUNKS] = {};
|
||||
uint32_t miniBitmapChunkCount = 0;
|
||||
uint32_t miniIntervalCount = 0;
|
||||
uint32_t miniGlyphCount = 0;
|
||||
uint32_t miniIntervalCapacity = 0;
|
||||
|
||||
@@ -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)],
|
||||
|
||||
@@ -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
@@ -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()) {
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
|
||||
Loaded 20 of 103 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user