Merge branch 'master' of origin into perf-lut-cache

Resolved conflicts in Section.h and Section.cpp:
- Combined includes (vector for LUT cache + optional/string from master)
- Added imageRendering parameter to loadSectionFile declaration
- Kept non-const clearCache (needs to close file handle for LUT cache)
- Kept in-memory LUT cache in loadPageFromSectionFile (replaces master's
  per-page LUT seek)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
jpirnay
2026-03-25 17:17:51 +01:00
co-authored by Claude Opus 4.6
179 changed files with 194143 additions and 203348 deletions
+16 -16
View File
@@ -110,7 +110,7 @@ These flags in `platformio.ini` fundamentally affect firmware behavior:
- Only ONE framebuffer exists (not double-buffered) - Only ONE framebuffer exists (not double-buffered)
- Grayscale rendering requires temporary buffer allocation (`renderer.storeBwBuffer()`) - Grayscale rendering requires temporary buffer allocation (`renderer.storeBwBuffer()`)
- Must call `renderer.restoreBwBuffer()` to free temporary buffers - Must call `renderer.restoreBwBuffer()` to free temporary buffers
- See [lib/GfxRenderer/GfxRenderer.cpp:439-440](lib/GfxRenderer/GfxRenderer.cpp) for malloc usage - See [lib/GfxRenderer/GfxRenderer.cpp:439-440](../lib/GfxRenderer/GfxRenderer.cpp) for malloc usage
### Directory Structure ### Directory Structure
* lib/: Internal libraries (Epub engine, GfxRenderer, UITheme, I18n) * lib/: Internal libraries (Epub engine, GfxRenderer, UITheme, I18n)
@@ -130,7 +130,7 @@ These flags in `platformio.ini` fundamentally affect firmware behavior:
| `HalGPIO` | `InputManager` | Button input handling | *(none)* | | `HalGPIO` | `InputManager` | Button input handling | *(none)* |
| `HalStorage` | `SDCardManager` | SD card file I/O | `Storage` | | `HalStorage` | `SDCardManager` | SD card file I/O | `Storage` |
**Location**: [lib/hal/](lib/hal/) **Location**: [lib/hal/](../lib/hal/)
**Why HAL?** **Why HAL?**
- Provides consistent error logging per module - Provides consistent error logging per module
@@ -247,7 +247,7 @@ When a template is necessary, limit instantiations: use explicit template instan
### Error Handling Philosophy ### Error Handling Philosophy
**Source**: [src/main.cpp:132-143](src/main.cpp), [lib/GfxRenderer/GfxRenderer.cpp:10](lib/GfxRenderer/GfxRenderer.cpp) **Source**: [src/main.cpp:132-143](../src/main.cpp), [lib/GfxRenderer/GfxRenderer.cpp:10](../lib/GfxRenderer/GfxRenderer.cpp)
**Pattern Hierarchy**: **Pattern Hierarchy**:
1. **LOG_ERR + return false** (90%): `LOG_ERR("MOD", "Failed: %s", reason); return false;` 1. **LOG_ERR + return false** (90%): `LOG_ERR("MOD", "Failed: %s", reason); return false;`
@@ -259,7 +259,7 @@ When a template is necessary, limit instantiations: use explicit template instan
### Acceptable malloc/free Patterns ### Acceptable malloc/free Patterns
**Source**: [src/activities/home/HomeActivity.cpp:166](src/activities/home/HomeActivity.cpp), [lib/GfxRenderer/GfxRenderer.cpp:439-440](lib/GfxRenderer/GfxRenderer.cpp) **Source**: [src/activities/home/HomeActivity.cpp:166](../src/activities/home/HomeActivity.cpp), [lib/GfxRenderer/GfxRenderer.cpp:439-440](../lib/GfxRenderer/GfxRenderer.cpp)
Despite "prefer stack allocation," malloc is acceptable for: Despite "prefer stack allocation," malloc is acceptable for:
1. **Large temporary buffers** (> 256 bytes, won't fit on stack) 1. **Large temporary buffers** (> 256 bytes, won't fit on stack)
@@ -290,10 +290,10 @@ buffer = nullptr;
- **Document size**: Comment why stack allocation was rejected - **Document size**: Comment why stack allocation was rejected
**Examples in codebase**: **Examples in codebase**:
- Cover image buffers: [HomeActivity.cpp:166](src/activities/home/HomeActivity.cpp#L166) - Cover image buffers: [HomeActivity.cpp:166](../src/activities/home/HomeActivity.cpp)
- Text chunk buffers: [TxtReaderActivity.cpp:259](src/activities/reader/TxtReaderActivity.cpp#L259) - Text chunk buffers: [TxtReaderActivity.cpp:259](../src/activities/reader/TxtReaderActivity.cpp)
- Bitmap rendering: [GfxRenderer.cpp:439-440](lib/GfxRenderer/GfxRenderer.cpp#L439-L440) - Bitmap rendering: [GfxRenderer.cpp:439-440](../lib/GfxRenderer/GfxRenderer.cpp)
- OTA update buffer: [OtaUpdater.cpp:40](src/network/OtaUpdater.cpp#L40) - OTA update buffer: [OtaUpdater.cpp:40](../src/network/OtaUpdater.cpp)
--- ---
@@ -305,7 +305,7 @@ buffer = nullptr;
### Logical Button Mapping ### Logical Button Mapping
**Source**: [src/MappedInputManager.cpp:20-55](src/MappedInputManager.cpp) **Source**: [src/MappedInputManager.cpp:20-55](../src/MappedInputManager.cpp)
Constraint: Physical button positions are fixed on hardware, but their logical functions change based on user settings and screen orientation. Constraint: Physical button positions are fixed on hardware, but their logical functions change based on user settings and screen orientation.
@@ -352,7 +352,7 @@ Constraint: Physical button positions are fixed on hardware, but their logical f
### Activity Lifecycle and Memory Management ### Activity Lifecycle and Memory Management
**Source**: [src/main.cpp:132-143](src/main.cpp) **Source**: [src/main.cpp:132-143](../src/main.cpp)
**CRITICAL**: Activities are **heap-allocated** and **deleted on exit**. **CRITICAL**: Activities are **heap-allocated** and **deleted on exit**.
@@ -389,7 +389,7 @@ void onExit() { /* free: vTaskDelete, free buffer, close files */ Activity::on
### FreeRTOS Task Guidelines ### FreeRTOS Task Guidelines
**Source**: [src/activities/util/KeyboardEntryActivity.cpp:45-50](src/activities/util/KeyboardEntryActivity.cpp) **Source**: [src/activities/util/KeyboardEntryActivity.cpp:45-50](../src/activities/util/KeyboardEntryActivity.cpp)
**Pattern**: See Activity Lifecycle above. `xTaskCreate(&taskTrampoline, "Name", stackSize, this, 1, &handle)` **Pattern**: See Activity Lifecycle above. `xTaskCreate(&taskTrampoline, "Name", stackSize, this, 1, &handle)`
@@ -402,7 +402,7 @@ void onExit() { /* free: vTaskDelete, free buffer, close files */ Activity::on
### Global Font Loading ### Global Font Loading
**Source**: [src/main.cpp:40-115](src/main.cpp) **Source**: [src/main.cpp:40-115](../src/main.cpp)
**All fonts are loaded as global static objects** at firmware startup: **All fonts are loaded as global static objects** at firmware startup:
- Bookerly: 12, 14, 16, 18pt (4 styles each: regular, bold, italic, bold-italic) - Bookerly: 12, 14, 16, 18pt (4 styles each: regular, bold, italic, bold-italic)
@@ -423,7 +423,7 @@ void onExit() { /* free: vTaskDelete, free buffer, close files */ Activity::on
- Fonts stored in **Flash** (marked as `static const` in `lib/EpdFont/builtinFonts/`) - Fonts stored in **Flash** (marked as `static const` in `lib/EpdFont/builtinFonts/`)
- Font rendering data cached in **DRAM** when first used - Font rendering data cached in **DRAM** when first used
- `OMIT_FONTS` can reduce binary size for minimal builds - `OMIT_FONTS` can reduce binary size for minimal builds
- Font IDs defined in [src/fontIds.h](src/fontIds.h) - Font IDs defined in [src/fontIds.h](../src/fontIds.h)
**Usage**: **Usage**:
```cpp ```cpp
@@ -517,7 +517,7 @@ clang-format -i src/**/*.cpp src/**/*.h
4. **Corrupt Cache Files**: 4. **Corrupt Cache Files**:
- Delete `.crosspoint/` directory on SD card - Delete `.crosspoint/` directory on SD card
- Forces clean re-parse of all EPUBs - Forces clean re-parse of all EPUBs
- Check file format versions in [docs/file-formats.md](docs/file-formats.md) - Check file format versions in [docs/file-formats.md](../docs/file-formats.md)
5. **Watchdog Timeout**: 5. **Watchdog Timeout**:
- Loop/task blocked for >5 seconds - Loop/task blocked for >5 seconds
@@ -664,7 +664,7 @@ Tested in all 4 orientations with 5MB+ files.
- `lib/I18n/I18nKeys.h`, `lib/I18n/I18nStrings.h`, `lib/I18n/I18nStrings.cpp` - `lib/I18n/I18nKeys.h`, `lib/I18n/I18nStrings.h`, `lib/I18n/I18nStrings.cpp`
- **Source**: YAML translation files in `lib/I18n/translations/` (one per language) - **Source**: YAML translation files in `lib/I18n/translations/` (one per language)
- **To modify**: Edit source YAML files, then run `python scripts/gen_i18n.py lib/I18n/translations lib/I18n/` - **To modify**: Edit source YAML files, then run `python scripts/gen_i18n.py lib/I18n/translations lib/I18n/`
- **Commit**: Source YAML files + `I18nKeys.h` and `I18nStrings.h` (needed for IDE symbol resolution), but NOT `I18nStrings.cpp` - **Commit**: Source YAML files only. All three generated files (`I18nKeys.h`, `I18nStrings.h`, `I18nStrings.cpp`) are in `.gitignore` and regenerated at build time.
3. **Build Artifacts** (in `.gitignore`): 3. **Build Artifacts** (in `.gitignore`):
- `.pio/` - PlatformIO build output - `.pio/` - PlatformIO build output
@@ -686,7 +686,7 @@ Tested in all 4 orientations with 5MB+ files.
- English (`english.yaml`) is the reference; missing keys in other languages fall back to English - English (`english.yaml`) is the reference; missing keys in other languages fall back to English
2. Run generator: `python scripts/gen_i18n.py lib/I18n/translations lib/I18n/` 2. Run generator: `python scripts/gen_i18n.py lib/I18n/translations lib/I18n/`
3. Generated files update: `I18nKeys.h`, `I18nStrings.h`, `I18nStrings.cpp` 3. Generated files update: `I18nKeys.h`, `I18nStrings.h`, `I18nStrings.cpp`
4. **Commit** source YAML files + `I18nKeys.h` and `I18nStrings.h` (IDE needs these for symbol resolution), but NOT `I18nStrings.cpp` 4. **Commit** source YAML files only. All three generated files are in `.gitignore` and regenerated at build time.
**To use translated strings in code**: **To use translated strings in code**:
```cpp ```cpp
+8
View File
@@ -39,6 +39,14 @@ usability over "swiss-army-knife" functionality.
* **Complex Annotation:** No typed out notes. These features are better suited for devices with better input * **Complex Annotation:** No typed out notes. These features are better suited for devices with better input
capabilities and more powerful chips. capabilities and more powerful chips.
### In-scope — Technically Unsupported
*These features align with CrossPoint's goals but are impractical on the current hardware or produce poor UX.*
* **Clock Display:** The ESP32-C3's RTC drifts significantly during deep sleep; making the clock untrustworthy after any sleep cycle. NTP sync could help, but CrossPoint doesn't connect to the internet on every boot.
* **PDF Rendering:** PDFs are fixed-layout documents, so rendering them requires displaying pages as images rather than reflowable text — resulting in constant panning and zooming that makes for a poor reading experience on e-ink.
## 3. Idea Evaluation ## 3. Idea Evaluation
While I appreciate the desire to add new and exciting features to CrossPoint Reader, CrossPoint Reader is designed to be While I appreciate the desire to add new and exciting features to CrossPoint Reader, CrossPoint Reader is designed to be
+23 -5
View File
@@ -85,6 +85,7 @@ The Browse Files screen acts as a file and folder browser.
* **Navigate List:** Use **Left** (or **Volume Up**), or **Right** (or **Volume Down**) to move the selection cursor up and down through folders and books. You can also long-press these buttons to scroll a full page up or down. * **Navigate List:** Use **Left** (or **Volume Up**), or **Right** (or **Volume Down**) to move the selection cursor up and down through folders and books. You can also long-press these buttons to scroll a full page up or down.
* **Open Selection:** Press **Confirm** to open a folder or read a selected book. * **Open Selection:** Press **Confirm** to open a folder or read a selected book.
* **Delete Files:** Hold and release **Confirm** to delete the selected file. You will be given an option to either confirm or cancel deletion. Folder deletion is not supported.
### 3.4 Recent Books Screen ### 3.4 Recent Books Screen
@@ -307,13 +308,30 @@ If you use the HTTPS listener, use `https://<server-ip>:7200` (`curl -k` only fo
### 3.7 Sleep Screen ### 3.7 Sleep Screen
You can customize the sleep screen by placing custom images in specific locations on the SD card: The **Sleep Screen** setting controls what is displayed when the device goes to sleep:
- **Single Image:** Place a file named `sleep.bmp` in the root directory. | Mode | Behavior |
- **Multiple Images:** Create a `sleep` directory in the root of the SD card and place any number of `.bmp` images inside. If images are found in this directory, they will take priority over the `sleep.bmp` file, and one will be randomly selected each time the device sleeps. |------|----------|
| **Dark** (default) | The CrossPoint logo on a dark background. |
| **Light** | The CrossPoint logo on a white background. |
| **Custom** | A custom image from the SD card (see below). Falls back to **Dark** if no custom image is found. |
| **Cover** | The cover of the currently open book. Falls back to **Dark** if no book is open. |
| **Cover + Custom** | The cover of the currently open book. Falls back to **Custom** behavior if no book is open. |
| **None** | A blank screen. |
> [!NOTE] #### Cover settings
> You'll need to set the **Sleep Screen** setting to **Custom** in order to use these images.
When using **Cover** or **Cover + Custom**, two additional settings apply:
- **Sleep Screen Cover Mode**: **Fit** (scale to fit, white borders) or **Crop** (scale and crop to fill the screen).
- **Sleep Screen Cover Filter**: **None** (grayscale), **Contrast** (black & white), or **Inverted** (inverted black & white).
#### Custom images
To use custom sleep images, set the sleep screen mode to **Custom** or **Cover + Custom**, then place images on the SD card:
- **Multiple Images (recommended):** Create a `.sleep` directory in the root of the SD card and place any number of `.bmp` images inside. One will be randomly selected each time the device sleeps. (A directory named `sleep` is also accepted as a fallback.)
- **Single Image:** Place a file named `sleep.bmp` in the root directory. This is used as a fallback if no valid images are found in the `.sleep`/`sleep` directory.
> [!TIP] > [!TIP]
> For best results: > For best results:
+154
View File
@@ -0,0 +1,154 @@
<#
.SYNOPSIS
Runs clang-format -i on project *.cpp and *.h files.
.DESCRIPTION
Formats all C/C++ source and header files in the repository, excluding
generated, vendored, and build directories (open-x4-sdk, builtinFonts,
hyphenation tries, uzlib, .pio, *.generated.h).
The clang-format binary path is resolved once and cached in
bin/clang-format-fix.local. On first run it checks a default path,
then PATH, then common install locations. Edit the .local file to
override manually.
.PARAMETER g
Format only git-modified files (git diff --name-only HEAD) instead of
the full tree.
.PARAMETER h
Show this help text.
.EXAMPLE
.\clang-format-fix.ps1
Format all files.
.EXAMPLE
.\clang-format-fix.ps1 -g
Format only git-modified files.
#>
param(
[switch]$g,
[switch]$h
)
if ($h) {
Get-Help $PSCommandPath -Detailed
return
}
$repoRoot = (Resolve-Path "$PSScriptRoot\..").Path
$configFile = Join-Path $PSScriptRoot 'clang-format-fix.local'
$defaultPath = 'C:\Program Files\LLVM\bin\clang-format.exe'
$candidatePaths = @(
'C:\Program Files\LLVM\bin\clang-format.exe'
'C:\Program Files (x86)\LLVM\bin\clang-format.exe'
'C:\msys64\ucrt64\bin\clang-format.exe'
'C:\msys64\mingw64\bin\clang-format.exe'
"$env:LOCALAPPDATA\LLVM\bin\clang-format.exe"
)
function Find-ClangFormat {
# Try PATH first
$inPath = Get-Command clang-format -ErrorAction SilentlyContinue
if ($inPath) { return $inPath.Source }
# Try candidate paths
foreach ($p in $candidatePaths) {
if (Test-Path $p) { return $p }
}
return $null
}
function Resolve-ClangFormat {
# 1. Read from config if present
if (Test-Path $configFile) {
$saved = (Get-Content $configFile -Raw).Trim()
if ($saved -and (Test-Path $saved)) { return $saved }
Write-Host "Configured path no longer valid: $saved"
}
# 2. Check default
if (Test-Path $defaultPath) {
$defaultPath | Set-Content $configFile
Write-Host "Saved clang-format path to $configFile"
return $defaultPath
}
# 3. Search PATH and candidate locations
$found = Find-ClangFormat
if ($found) {
$found | Set-Content $configFile
Write-Host "Found clang-format at $found - saved to $configFile"
return $found
}
Write-Error "clang-format not found. Install LLVM or add clang-format to PATH."
exit 1
}
$clangFormat = Resolve-ClangFormat
$exclude = @(
'open-x4-sdk'
'lib\EpdFont\builtinFonts'
'lib\Epub\Epub\hyphenation\generated'
'lib\uzlib'
'.pio'
)
function Test-Excluded($fullPath) {
foreach ($ex in $exclude) {
if ($fullPath -like "*\$ex\*") { return $true }
}
if ($fullPath -like '*.generated.h') { return $true }
return $false
}
if ($g) {
# Only git-modified *.cpp / *.h files
# Covers both staged and unstaged changes
$files = @(git -C $repoRoot diff --name-only HEAD) +
@(git -C $repoRoot diff --name-only --cached) |
Sort-Object -Unique |
Where-Object { $_ -match '\.(cpp|h)$' } |
ForEach-Object { Get-Item (Join-Path $repoRoot $_) -ErrorAction SilentlyContinue } |
Where-Object { $_ -and -not (Test-Excluded $_.FullName) }
} else {
$files = Get-ChildItem -Path $repoRoot -Recurse -Include *.cpp, *.h -File |
Where-Object { -not (Test-Excluded $_.FullName) }
}
$files = @($files)
if ($files.Count -eq 0) {
Write-Host 'No files to format.'
return
}
Write-Host "Formatting $($files.Count) files..."
$i = 0
$changed = 0
$failures = 0
foreach ($f in $files) {
$i++
$rel = $f.FullName.Substring($repoRoot.Length + 1)
$hashBefore = (Get-FileHash $f.FullName -Algorithm MD5).Hash
& $clangFormat -i $f.FullName
if ($LASTEXITCODE -ne 0) {
$failures++
Write-Host " [$i/$($files.Count)] $rel (FAILED, exit code $LASTEXITCODE)"
continue
}
$hashAfter = (Get-FileHash $f.FullName -Algorithm MD5).Hash
if ($hashBefore -ne $hashAfter) {
$changed++
Write-Host " [$i/$($files.Count)] $rel (changed)"
} else {
Write-Host " [$i/$($files.Count)] $rel"
}
}
Write-Host "Done. $changed/$($files.Count) files changed, $failures failed."
if ($failures -gt 0) { exit 1 }
+1
View File
@@ -15,6 +15,7 @@ This guide explains the multi-language support system in CrossPoint Reader.
- Ukrainian - Ukrainian
- Polish - Polish
- Danish - Danish
- Turkish
--- ---
+2
View File
@@ -35,9 +35,11 @@ If you'd like to add your name to this list, please open a PR adding yourself an
- [yeyeto2788](https://github.com/yeyeto2788) - [yeyeto2788](https://github.com/yeyeto2788)
- [Skrzakk](https://github.com/Skrzakk) - [Skrzakk](https://github.com/Skrzakk)
- [pablohc](https://github.com/pablohc) - [pablohc](https://github.com/pablohc)
- [DaniPhii](https://github.com/DaniPhii)
## Swedish ## Swedish
- [dawiik](https://github.com/dawiik) - [dawiik](https://github.com/dawiik)
- [steka](https://github.com/steka)
## Romanian ## Romanian
- [ariel-lindemann](https://github.com/ariel-lindemann) - [ariel-lindemann](https://github.com/ariel-lindemann)
-37
View File
@@ -1,37 +0,0 @@
This directory is intended for project header files.
A header file is a file containing C declarations and macro definitions
to be shared between several project source files. You request the use of a
header file in your project source file (C, C++, etc) located in `src` folder
by including it, with the C preprocessing directive `#include'.
```src/main.c
#include "header.h"
int main (void)
{
...
}
```
Including a header file produces the same results as copying the header file
into each source file that needs it. Such copying would be time-consuming
and error-prone. With a header file, the related declarations appear
in only one place. If they need to be changed, they can be changed in one
place, and programs that include the header file will automatically use the
new version when next recompiled. The header file eliminates the labor of
finding and changing all the copies as well as the risk that a failure to
find one copy will result in inconsistencies within a program.
In C, the convention is to give header files names that end with `.h'.
Read more about using header files in official GCC documentation:
* Include Syntax
* Include Operation
* Once-Only Headers
* Computed Includes
https://gcc.gnu.org/onlinedocs/cpp/Header-Files.html
+41 -52
View File
@@ -15,10 +15,9 @@ void EpdFont::getTextBounds(const char* string, const int startX, const int star
return; return;
} }
int cursorX = startX; int32_t cursorXFP = fp4::fromPixel(startX); // 12.4 fixed-point accumulator
const int cursorY = startY;
int lastBaseX = startX; int lastBaseX = startX;
int lastBaseAdvance = 0; int lastBaseAdvanceFP = 0; // 12.4 fixed-point
int lastBaseTop = 0; int lastBaseTop = 0;
constexpr int MIN_COMBINING_GAP_PX = 1; constexpr int MIN_COMBINING_GAP_PX = 1;
uint32_t cp; uint32_t cp;
@@ -32,7 +31,6 @@ void EpdFont::getTextBounds(const char* string, const int startX, const int star
const EpdGlyph* glyph = getGlyph(cp); const EpdGlyph* glyph = getGlyph(cp);
if (!glyph) { if (!glyph) {
// TODO: Better handle this?
prevCp = 0; prevCp = 0;
continue; continue;
} }
@@ -46,11 +44,12 @@ void EpdFont::getTextBounds(const char* string, const int startX, const int star
} }
if (!isCombining && prevCp != 0) { if (!isCombining && prevCp != 0) {
cursorX += getKerning(prevCp, cp); cursorXFP += getKerning(prevCp, cp); // 4.4 fixed-point kern
} }
const int glyphBaseX = isCombining ? (lastBaseX + lastBaseAdvance / 2) : cursorX; const int cursorXPixels = fp4::toPixel(cursorXFP); // snap 12.4 fixed-point to nearest pixel
const int glyphBaseY = cursorY - raiseBy; const int glyphBaseX = isCombining ? (lastBaseX + fp4::toPixel(lastBaseAdvanceFP / 2)) : cursorXPixels;
const int glyphBaseY = startY - raiseBy;
*minX = std::min(*minX, glyphBaseX + glyph->left); *minX = std::min(*minX, glyphBaseX + glyph->left);
*maxX = std::max(*maxX, glyphBaseX + glyph->left + glyph->width); *maxX = std::max(*maxX, glyphBaseX + glyph->left + glyph->width);
@@ -58,10 +57,10 @@ void EpdFont::getTextBounds(const char* string, const int startX, const int star
*maxY = std::max(*maxY, glyphBaseY + glyph->top); *maxY = std::max(*maxY, glyphBaseY + glyph->top);
if (!isCombining) { if (!isCombining) {
lastBaseX = cursorX; lastBaseX = cursorXPixels;
lastBaseAdvance = glyph->advanceX; lastBaseAdvanceFP = glyph->advanceX; // 12.4 fixed-point
lastBaseTop = glyph->top; lastBaseTop = glyph->top;
cursorX += glyph->advanceX; cursorXFP += glyph->advanceX; // 12.4 fixed-point advance
prevCp = cp; prevCp = cp;
} }
} }
@@ -80,21 +79,19 @@ static uint8_t lookupKernClass(const EpdKernClassEntry* entries, const uint16_t
if (!entries || count == 0 || cp > 0xFFFF) { if (!entries || count == 0 || cp > 0xFFFF) {
return 0; return 0;
} }
const auto target = static_cast<uint16_t>(cp); const auto target = static_cast<uint16_t>(cp);
int left = 0; const auto* end = entries + count;
int right = static_cast<int>(count) - 1;
while (left <= right) { // lower_bound: exact-key lookup. Finds the first entry with codepoint >= target,
const int mid = left + (right - left) / 2; // then the equality check confirms an exact match exists.
const uint16_t midCp = entries[mid].codepoint; const auto it = std::lower_bound(
if (midCp == target) { entries, end, target, [](const EpdKernClassEntry& entry, uint16_t value) { return entry.codepoint < value; });
return entries[mid].classId;
} if (it != end && it->codepoint == target) {
if (midCp < target) { return it->classId;
left = mid + 1;
} else {
right = mid - 1;
}
} }
return 0; return 0;
} }
@@ -117,21 +114,17 @@ uint32_t EpdFont::getLigature(const uint32_t leftCp, const uint32_t rightCp) con
} }
const uint32_t key = (leftCp << 16) | rightCp; const uint32_t key = (leftCp << 16) | rightCp;
int left = 0; const auto* end = pairs + count;
int right = static_cast<int>(count) - 1;
while (left <= right) { // lower_bound: exact-key lookup. Finds the first entry with pair >= key,
const int mid = left + (right - left) / 2; // then the equality check confirms an exact match exists.
const uint32_t midKey = pairs[mid].pair; const auto it =
if (midKey == key) { std::lower_bound(pairs, end, key, [](const EpdLigaturePair& pair, uint32_t value) { return pair.pair < value; });
return pairs[mid].ligatureCp;
} if (it != end && it->pair == key) {
if (midKey < key) { return it->ligatureCp;
left = mid + 1;
} else {
right = mid - 1;
}
} }
return 0; return 0;
} }
@@ -154,29 +147,25 @@ uint32_t EpdFont::applyLigatures(uint32_t cp, const char*& text) const {
} }
const EpdGlyph* EpdFont::getGlyph(const uint32_t cp) const { const EpdGlyph* EpdFont::getGlyph(const uint32_t cp) const {
const EpdUnicodeInterval* intervals = data->intervals;
const int count = data->intervalCount; const int count = data->intervalCount;
if (count == 0) return nullptr; if (count == 0) return nullptr;
// Binary search for O(log n) lookup instead of O(n) const EpdUnicodeInterval* intervals = data->intervals;
// Critical for Korean fonts with many unicode intervals const auto* end = intervals + count;
int left = 0;
int right = count - 1;
while (left <= right) { // upper_bound: range lookup. Finds the first interval with first > cp, so the
const int mid = left + (right - left) / 2; // interval just before it is the last one with first <= cp. That's the only
const EpdUnicodeInterval* interval = &intervals[mid]; // candidate that could contain cp. Then we verify cp <= candidate.last.
const auto it = std::upper_bound(
intervals, end, cp, [](uint32_t value, const EpdUnicodeInterval& interval) { return value < interval.first; });
if (cp < interval->first) { if (it != intervals) {
right = mid - 1; const auto& interval = *(it - 1);
} else if (cp > interval->last) { if (cp <= interval.last) {
left = mid + 1; return &data->glyph[interval.offset + (cp - interval.first)];
} else {
// Found: cp >= interval->first && cp <= interval->last
return &data->glyph[interval->offset + (cp - interval->first)];
} }
} }
if (cp != REPLACEMENT_GLYPH) { if (cp != REPLACEMENT_GLYPH) {
return getGlyph(REPLACEMENT_GLYPH); return getGlyph(REPLACEMENT_GLYPH);
} }
+1 -1
View File
@@ -12,7 +12,7 @@ class EpdFont {
const EpdGlyph* getGlyph(uint32_t cp) const; const EpdGlyph* getGlyph(uint32_t cp) const;
/// Returns the kerning adjustment (in pixels) between two codepoints. /// Returns the kerning adjustment (4.4 fixed-point in pixels) between two codepoints.
/// Returns 0 if no kerning data exists for the pair. /// Returns 0 if no kerning data exists for the pair.
int8_t getKerning(uint32_t leftCp, uint32_t rightCp) const; int8_t getKerning(uint32_t leftCp, uint32_t rightCp) const;
+39 -9
View File
@@ -4,11 +4,40 @@
#pragma once #pragma once
#include <cstdint> #include <cstdint>
/// Font metrics use "fixed-point 4" (4 fractional bits, i.e. 1/16-pixel
/// resolution). Both the 12.4 glyph advances (uint16_t) and the 4.4 kern
/// values (int8_t) share the same 4 fractional bits, so they can be freely
/// added into a single int32_t accumulator during text layout. The
/// accumulator is snapped to the nearest whole pixel only at render time,
/// which avoids the per-character rounding errors that plagued integer-only
/// layout.
///
/// The helpers below eliminate the raw bit-shifts that would otherwise be
/// scattered across every layout / measurement call site.
namespace fp4 {
constexpr int FRAC_BITS = 4;
constexpr int32_t HALF = 1 << (FRAC_BITS - 1); // 8, added before shift for round-to-nearest
/// Convert an integer pixel value to 12.4 fixed-point.
constexpr int32_t fromPixel(int px) { return static_cast<int32_t>(px) << FRAC_BITS; }
/// Snap a fixed-point value to the nearest integer pixel.
constexpr int toPixel(int32_t fp) { return static_cast<int>((fp + HALF) >> FRAC_BITS); }
/// Convert a fixed-point value to float (mainly useful for debug logging).
constexpr float toFloat(int32_t fp) { return fp / static_cast<float>(1 << FRAC_BITS); }
} // namespace fp4
/// Fixed-point conventions used by EpdGlyph and EpdFontData:
/// advanceX: 12.4 unsigned fixed-point in uint16_t (use fp4::toPixel)
/// kernMatrix: 4.4 signed fixed-point in int8_t (use fp4::toPixel)
/// Both share 4 fractional bits so they combine directly in an accumulator.
/// Font data stored PER GLYPH /// Font data stored PER GLYPH
typedef struct { typedef struct {
uint8_t width; ///< Bitmap dimensions in pixels uint8_t width; ///< Bitmap dimensions in pixels
uint8_t height; ///< Bitmap dimensions in pixels uint8_t height; ///< Bitmap dimensions in pixels
uint8_t advanceX; ///< Distance to advance cursor (x axis) uint16_t advanceX; ///< Distance to advance cursor (x axis), 12.4 fixed-point in pixels
int16_t left; ///< X dist from cursor pos to UL corner int16_t left; ///< X dist from cursor pos to UL corner
int16_t top; ///< Y dist from cursor pos to UL corner int16_t top; ///< Y dist from cursor pos to UL corner
uint16_t dataLength; ///< Size of the font data. uint16_t dataLength; ///< Size of the font data.
@@ -21,7 +50,7 @@ typedef struct {
uint32_t compressedSize; ///< Compressed DEFLATE stream size uint32_t compressedSize; ///< Compressed DEFLATE stream size
uint32_t uncompressedSize; ///< Decompressed size uint32_t uncompressedSize; ///< Decompressed size
uint16_t glyphCount; ///< Number of glyphs in this group uint16_t glyphCount; ///< Number of glyphs in this group
uint16_t firstGlyphIndex; ///< First glyph index in the global glyph array uint32_t firstGlyphIndex; ///< First glyph index in the global glyph array
} EpdFontGroup; } EpdFontGroup;
/// Glyph interval structure /// Glyph interval structure
@@ -57,13 +86,14 @@ typedef struct {
bool is2Bit; bool is2Bit;
const EpdFontGroup* groups; ///< NULL for uncompressed fonts const EpdFontGroup* groups; ///< NULL for uncompressed fonts
uint16_t groupCount; ///< 0 for uncompressed fonts uint16_t groupCount; ///< 0 for uncompressed fonts
const uint16_t* glyphToGroup; ///< Per-glyph group ID (nullptr for contiguous-group fonts)
const EpdKernClassEntry* kernLeftClasses; ///< Sorted left-side class map (nullptr if none) const EpdKernClassEntry* kernLeftClasses; ///< Sorted left-side class map (nullptr if none)
const EpdKernClassEntry* kernRightClasses; ///< Sorted right-side class map (nullptr if none) const EpdKernClassEntry* kernRightClasses; ///< Sorted right-side class map (nullptr if none)
const int8_t* kernMatrix; ///< Flat leftClassCount x rightClassCount matrix const int8_t* kernMatrix; ///< Flat leftClassCount x rightClassCount matrix, 4.4 fixed-point in pixels
uint16_t kernLeftEntryCount; ///< Entries in kernLeftClasses uint16_t kernLeftEntryCount; ///< Entries in kernLeftClasses
uint16_t kernRightEntryCount; ///< Entries in kernRightClasses uint16_t kernRightEntryCount; ///< Entries in kernRightClasses
uint8_t kernLeftClassCount; ///< Number of distinct left classes (matrix rows) uint8_t kernLeftClassCount; ///< Number of distinct left classes (matrix rows)
uint8_t kernRightClassCount; ///< Number of distinct right classes (matrix cols) uint8_t kernRightClassCount; ///< Number of distinct right classes (matrix cols)
const EpdLigaturePair* ligaturePairs; ///< Sorted ligature pair table (nullptr if none) const EpdLigaturePair* ligaturePairs; ///< Sorted ligature pair table (nullptr if none)
uint32_t ligaturePairCount; ///< Number of entries in ligaturePairs uint32_t ligaturePairCount; ///< Number of entries in ligaturePairs
} EpdFontData; } EpdFontData;
+428 -81
View File
@@ -1,34 +1,55 @@
#include "FontDecompressor.h" #include "FontDecompressor.h"
#include <Arduino.h>
#include <Logging.h> #include <Logging.h>
#include <Utf8.h>
#include <cstdlib> #include <cstdlib>
FontDecompressor::~FontDecompressor() { deinit(); }
bool FontDecompressor::init() { bool FontDecompressor::init() {
clearCache(); clearCache();
return true; return true;
} }
void FontDecompressor::freeAllEntries() { void FontDecompressor::deinit() {
for (auto& entry : cache) { freePageBuffer();
if (entry.data) { freeHotGroup();
free(entry.data);
entry.data = nullptr;
}
entry.valid = false;
}
} }
void FontDecompressor::deinit() { freeAllEntries(); }
void FontDecompressor::clearCache() { void FontDecompressor::clearCache() {
freeAllEntries(); freePageBuffer();
accessCounter = 0; freeHotGroup();
} }
uint16_t FontDecompressor::getGroupIndex(const EpdFontData* fontData, uint16_t glyphIndex) { void FontDecompressor::freePageBuffer() {
for (uint8_t s = 0; s < pageSlotCount; s++) {
free(pageSlots[s].buffer);
free(pageSlots[s].glyphs);
pageSlots[s] = {};
}
pageSlotCount = 0;
}
void FontDecompressor::freeHotGroup() {
hotGroup.clear();
hotGroup.shrink_to_fit();
hotGroupFont = nullptr;
hotGroupIndex = UINT16_MAX;
hotGlyphBuf.clear();
hotGlyphBuf.shrink_to_fit();
}
uint16_t FontDecompressor::getGroupIndex(const EpdFontData* fontData, uint32_t glyphIndex) {
// O(1) path for frequency-grouped fonts with glyphToGroup mapping
if (fontData->glyphToGroup != nullptr) {
return fontData->glyphToGroup[glyphIndex];
}
// Contiguous-group fonts: linear scan
for (uint16_t i = 0; i < fontData->groupCount; i++) { for (uint16_t i = 0; i < fontData->groupCount; i++) {
uint16_t first = fontData->groups[i].firstGlyphIndex; uint32_t first = fontData->groups[i].firstGlyphIndex;
if (glyphIndex >= first && glyphIndex < first + fontData->groups[i].glyphCount) { if (glyphIndex >= first && glyphIndex < first + fontData->groups[i].glyphCount) {
return i; return i;
} }
@@ -36,99 +57,425 @@ uint16_t FontDecompressor::getGroupIndex(const EpdFontData* fontData, uint16_t g
return fontData->groupCount; // sentinel = not found return fontData->groupCount; // sentinel = not found
} }
FontDecompressor::CacheEntry* FontDecompressor::findInCache(const EpdFontData* fontData, uint16_t groupIndex) { bool FontDecompressor::decompressGroup(const EpdFontData* fontData, uint16_t groupIndex, uint8_t* outBuf,
for (auto& entry : cache) { uint32_t outSize) {
if (entry.valid && entry.font == fontData && entry.groupIndex == groupIndex) {
return &entry;
}
}
return nullptr;
}
FontDecompressor::CacheEntry* FontDecompressor::findEvictionCandidate() {
// Find an invalid slot first
for (auto& entry : cache) {
if (!entry.valid) {
return &entry;
}
}
// Otherwise evict LRU
CacheEntry* lru = &cache[0];
for (auto& entry : cache) {
if (entry.lastUsed < lru->lastUsed) {
lru = &entry;
}
}
return lru;
}
bool FontDecompressor::decompressGroup(const EpdFontData* fontData, uint16_t groupIndex, CacheEntry* entry) {
const EpdFontGroup& group = fontData->groups[groupIndex]; const EpdFontGroup& group = fontData->groups[groupIndex];
// Free old buffer if reusing a slot const uint32_t tDecomp = millis();
if (entry->data) {
free(entry->data);
entry->data = nullptr;
}
entry->valid = false;
// Allocate output buffer
auto* outBuf = static_cast<uint8_t*>(malloc(group.uncompressedSize));
if (!outBuf) {
LOG_ERR("FDC", "Failed to allocate %u bytes for group %u", group.uncompressedSize, groupIndex);
return false;
}
inflateReader.init(false); inflateReader.init(false);
inflateReader.setSource(&fontData->bitmap[group.compressedOffset], group.compressedSize); inflateReader.setSource(&fontData->bitmap[group.compressedOffset], group.compressedSize);
if (!inflateReader.read(outBuf, group.uncompressedSize)) { if (!inflateReader.read(outBuf, outSize)) {
stats.decompressTimeMs += millis() - tDecomp;
LOG_ERR("FDC", "Decompression failed for group %u", groupIndex); LOG_ERR("FDC", "Decompression failed for group %u", groupIndex);
free(outBuf);
return false; return false;
} }
stats.decompressTimeMs += millis() - tDecomp;
entry->font = fontData;
entry->groupIndex = groupIndex;
entry->data = outBuf;
entry->dataSize = group.uncompressedSize;
entry->valid = true;
return true; return true;
} }
const uint8_t* FontDecompressor::getBitmap(const EpdFontData* fontData, const EpdGlyph* glyph, uint16_t glyphIndex) { // --- Byte-aligned helpers ---
uint32_t FontDecompressor::getAlignedOffset(const EpdFontData* fontData, uint16_t groupIndex, uint32_t glyphIndex) {
uint32_t offset = 0;
auto accumGlyph = [&](const EpdGlyph& g) {
if (g.width > 0 && g.height > 0) {
offset += ((g.width + 3) / 4) * g.height;
}
};
if (fontData->glyphToGroup) {
// Frequency-grouped: scan glyphs before glyphIndex that belong to this group
for (uint32_t i = 0; i < glyphIndex; i++) {
if (fontData->glyphToGroup[i] == groupIndex) {
accumGlyph(fontData->glyph[i]);
}
}
} else {
// Contiguous-group: sum aligned sizes of preceding glyphs in the group
const EpdFontGroup& group = fontData->groups[groupIndex];
for (uint32_t i = group.firstGlyphIndex; i < glyphIndex; i++) {
accumGlyph(fontData->glyph[i]);
}
}
return offset;
}
void FontDecompressor::compactSingleGlyph(const uint8_t* alignedSrc, uint8_t* packedDst, uint8_t width,
uint8_t height) {
if (width == 0 || height == 0) return;
const uint32_t rowStride = (width + 3) / 4;
if (width % 4 == 0) {
memcpy(packedDst, alignedSrc, rowStride * height);
return;
}
uint8_t outByte = 0, outBits = 0;
uint32_t writeIdx = 0;
for (uint8_t y = 0; y < height; y++) {
for (uint8_t x = 0; x < width; x++) {
outByte = (outByte << 2) | ((alignedSrc[y * rowStride + x / 4] >> ((3 - (x % 4)) * 2)) & 0x3);
outBits += 2;
if (outBits == 8) {
packedDst[writeIdx++] = outByte;
outByte = 0;
outBits = 0;
}
}
}
if (outBits > 0) packedDst[writeIdx] = outByte << (8 - outBits);
}
// --- getBitmap: page buffer → hot group → decompress ---
const uint8_t* FontDecompressor::getBitmap(const EpdFontData* fontData, const EpdGlyph* glyph, uint32_t glyphIndex) {
const uint32_t tStart = micros();
stats.getBitmapCalls++;
if (!fontData->groups || fontData->groupCount == 0) { if (!fontData->groups || fontData->groupCount == 0) {
stats.getBitmapTimeUs += micros() - tStart;
return &fontData->bitmap[glyph->dataOffset]; return &fontData->bitmap[glyph->dataOffset];
} }
// Check page buffer slots (populated by prewarmCache — one slot per font style)
for (uint8_t s = 0; s < pageSlotCount; s++) {
const auto& slot = pageSlots[s];
if (slot.fontData != fontData || slot.glyphCount == 0) continue;
int left = 0, right = slot.glyphCount - 1;
while (left <= right) {
int mid = left + (right - left) / 2;
if (slot.glyphs[mid].glyphIndex == glyphIndex) {
if (slot.glyphs[mid].bufferOffset != UINT32_MAX) {
stats.cacheHits++;
stats.getBitmapTimeUs += micros() - tStart;
return &slot.buffer[slot.glyphs[mid].bufferOffset];
}
break; // Not extracted during prewarm; fall through to hot-group path
}
if (slot.glyphs[mid].glyphIndex < glyphIndex)
left = mid + 1;
else
right = mid - 1;
}
break; // Found the right slot but glyph wasn't in it; don't check other slots
}
// Fallback: hot group slot
uint16_t groupIndex = getGroupIndex(fontData, glyphIndex); uint16_t groupIndex = getGroupIndex(fontData, glyphIndex);
if (groupIndex >= fontData->groupCount) { if (groupIndex >= fontData->groupCount) {
LOG_ERR("FDC", "Glyph %u not found in any group", glyphIndex); LOG_ERR("FDC", "Glyph %u not found in any group", glyphIndex);
stats.getBitmapTimeUs += micros() - tStart;
return nullptr; return nullptr;
} }
// Check cache // Check if hot group already has this group decompressed — if not, decompress it
CacheEntry* entry = findInCache(fontData, groupIndex); if (!(!hotGroup.empty() && hotGroupFont == fontData && hotGroupIndex == groupIndex)) {
if (entry) { stats.cacheMisses++;
entry->lastUsed = ++accessCounter; const EpdFontGroup& group = fontData->groups[groupIndex];
if (glyph->dataOffset + glyph->dataLength > entry->dataSize) {
LOG_ERR("FDC", "dataOffset %u + dataLength %u out of bounds for group %u (size %u)", glyph->dataOffset, hotGroup.resize(group.uncompressedSize);
glyph->dataLength, groupIndex, entry->dataSize); if (hotGroup.empty()) {
LOG_ERR("FDC", "Failed to allocate %u bytes for hot group %u", group.uncompressedSize, groupIndex);
hotGroupFont = nullptr;
hotGroupIndex = UINT16_MAX;
stats.getBitmapTimeUs += micros() - tStart;
return nullptr; return nullptr;
} }
return &entry->data[glyph->dataOffset];
if (!decompressGroup(fontData, groupIndex, hotGroup.data(), group.uncompressedSize)) {
hotGroup.clear();
hotGroup.shrink_to_fit();
hotGroupFont = nullptr;
hotGroupIndex = UINT16_MAX;
stats.getBitmapTimeUs += micros() - tStart;
return nullptr;
}
hotGroupFont = fontData;
hotGroupIndex = groupIndex;
stats.hotGroupBytes = group.uncompressedSize;
} else {
stats.cacheHits++;
} }
// Cache miss - decompress // Compact just the requested glyph from byte-aligned data into scratch buffer
entry = findEvictionCandidate(); if (glyph->dataLength > hotGlyphBuf.size()) {
if (!decompressGroup(fontData, groupIndex, entry)) { hotGlyphBuf.resize(glyph->dataLength);
}
if (hotGlyphBuf.empty()) {
stats.getBitmapTimeUs += micros() - tStart;
return nullptr; return nullptr;
} }
entry->lastUsed = ++accessCounter; uint32_t alignedOff = getAlignedOffset(fontData, groupIndex, glyphIndex);
if (glyph->dataOffset + glyph->dataLength > entry->dataSize) { compactSingleGlyph(&hotGroup[alignedOff], hotGlyphBuf.data(), glyph->width, glyph->height);
LOG_ERR("FDC", "dataOffset %u + dataLength %u out of bounds for group %u (size %u)", glyph->dataOffset, stats.getBitmapTimeUs += micros() - tStart;
glyph->dataLength, groupIndex, entry->dataSize); return hotGlyphBuf.data();
return nullptr; }
// --- Prewarm: pre-decompress glyph bitmaps for a page of text ---
int32_t FontDecompressor::findGlyphIndex(const EpdFontData* fontData, uint32_t codepoint) {
const EpdUnicodeInterval* intervals = fontData->intervals;
const int count = fontData->intervalCount;
if (count == 0) return -1;
// Binary search
int left = 0;
int right = count - 1;
while (left <= right) {
const int mid = left + (right - left) / 2;
const EpdUnicodeInterval* interval = &intervals[mid];
if (codepoint < interval->first) {
right = mid - 1;
} else if (codepoint > interval->last) {
left = mid + 1;
} else {
return static_cast<int32_t>(interval->offset + (codepoint - interval->first));
}
} }
return &entry->data[glyph->dataOffset];
return -1;
}
int FontDecompressor::prewarmCache(const EpdFontData* fontData, const char* utf8Text) {
if (!fontData || !fontData->groups || !utf8Text) return 0;
// Allocate the next available slot (caller must call freePageBuffer/clearCache to reset)
if (pageSlotCount >= MAX_PAGE_SLOTS) {
LOG_ERR("FDC", "All %u page buffer slots full, cannot prewarm fontData=%p", MAX_PAGE_SLOTS, (void*)fontData);
return -1;
}
PageSlot& slot = pageSlots[pageSlotCount];
// Step 1: Collect unique glyph indices needed for this page
uint32_t neededGlyphs[MAX_PAGE_GLYPHS];
uint16_t glyphCount = 0;
bool glyphCapWarned = false;
const unsigned char* p = reinterpret_cast<const unsigned char*>(utf8Text);
while (*p) {
uint32_t cp = utf8NextCodepoint(&p);
if (cp == 0) break;
int32_t glyphIdx = findGlyphIndex(fontData, cp);
if (glyphIdx < 0) continue;
// Deduplicate
bool found = false;
for (uint16_t i = 0; i < glyphCount; i++) {
if (neededGlyphs[i] == static_cast<uint32_t>(glyphIdx)) {
found = true;
break;
}
}
if (!found) {
if (glyphCount < MAX_PAGE_GLYPHS) {
neededGlyphs[glyphCount++] = static_cast<uint32_t>(glyphIdx);
} else if (!glyphCapWarned) {
LOG_DBG("FDC", "Glyph cap (%u) reached during prewarm; excess glyphs will use hot-group fallback",
MAX_PAGE_GLYPHS);
glyphCapWarned = true;
}
}
}
if (glyphCount == 0) return 0;
// Step 2: Compute total buffer size and collect unique groups
uint32_t totalBytes = 0;
uint16_t neededGroups[128];
uint8_t groupCount = 0;
bool groupCapWarned = false;
for (uint16_t i = 0; i < glyphCount; i++) {
totalBytes += fontData->glyph[neededGlyphs[i]].dataLength;
uint16_t gi = getGroupIndex(fontData, neededGlyphs[i]);
bool found = false;
for (uint8_t j = 0; j < groupCount; j++) {
if (neededGroups[j] == gi) {
found = true;
break;
}
}
if (!found) {
if (groupCount < 128) {
neededGroups[groupCount++] = gi;
} else if (!groupCapWarned) {
LOG_DBG("FDC", "Group cap (128) reached during prewarm; some groups will use hot-group fallback");
groupCapWarned = true;
}
}
}
stats.uniqueGroupsAccessed = groupCount;
// Step 3: Allocate page buffer and lookup table for this slot
slot.buffer = static_cast<uint8_t*>(malloc(totalBytes));
slot.glyphs = static_cast<PageGlyphEntry*>(malloc(glyphCount * sizeof(PageGlyphEntry)));
if (!slot.buffer || !slot.glyphs) {
LOG_ERR("FDC", "Failed to allocate page buffer (%u bytes, %u glyphs)", totalBytes, glyphCount);
free(slot.buffer);
free(slot.glyphs);
slot = {};
return glyphCount;
}
stats.pageBufferBytes += totalBytes;
stats.pageGlyphsBytes += glyphCount * sizeof(PageGlyphEntry);
slot.fontData = fontData;
slot.glyphCount = glyphCount;
pageSlotCount++;
// Initialize lookup entries (bufferOffset = UINT32_MAX means not yet extracted)
for (uint16_t i = 0; i < glyphCount; i++) {
slot.glyphs[i] = {neededGlyphs[i], UINT32_MAX, 0};
}
// Sort by glyphIndex for binary search in getBitmap()
for (uint16_t i = 1; i < glyphCount; i++) {
PageGlyphEntry key = slot.glyphs[i];
int j = i - 1;
while (j >= 0 && slot.glyphs[j].glyphIndex > key.glyphIndex) {
slot.glyphs[j + 1] = slot.glyphs[j];
j--;
}
slot.glyphs[j + 1] = key;
}
// Step 3b: Pre-scan to compute each needed glyph's byte-aligned offset within its group.
// This avoids recomputing aligned offsets per group during extraction in step 4.
uint32_t groupAlignedTracker[128] = {}; // running byte-aligned offset for each needed group
if (fontData->glyphToGroup) {
// Frequency-grouped: single O(totalGlyphs) pass through glyphToGroup
const auto& lastInterval = fontData->intervals[fontData->intervalCount - 1];
const uint32_t totalGlyphs = lastInterval.offset + (lastInterval.last - lastInterval.first + 1);
for (uint32_t i = 0; i < totalGlyphs; i++) {
const uint16_t gi = fontData->glyphToGroup[i];
// Find this glyph's group position in neededGroups
uint8_t gpPos = groupCount;
for (uint8_t j = 0; j < groupCount; j++) {
if (neededGroups[j] == gi) {
gpPos = j;
break;
}
}
if (gpPos == groupCount) continue; // not a needed group
const EpdGlyph& glyph = fontData->glyph[i];
// Binary search in sorted slot.glyphs to find if glyph i is needed
int left = 0, right = (int)slot.glyphCount - 1;
while (left <= right) {
const int mid = left + (right - left) / 2;
if (slot.glyphs[mid].glyphIndex == i) {
slot.glyphs[mid].alignedOffset = groupAlignedTracker[gpPos];
break;
}
if (slot.glyphs[mid].glyphIndex < i)
left = mid + 1;
else
right = mid - 1;
}
if (glyph.width > 0 && glyph.height > 0) {
groupAlignedTracker[gpPos] += ((glyph.width + 3) / 4) * glyph.height;
}
}
} else {
// Contiguous-group: iterate each needed group's glyphs directly
for (uint8_t g = 0; g < groupCount; g++) {
const EpdFontGroup& group = fontData->groups[neededGroups[g]];
uint32_t alignedOff = 0;
for (uint16_t j = 0; j < group.glyphCount; j++) {
const uint32_t glyphI = group.firstGlyphIndex + j;
const EpdGlyph& glyph = fontData->glyph[glyphI];
int left = 0, right = (int)slot.glyphCount - 1;
while (left <= right) {
const int mid = left + (right - left) / 2;
if (slot.glyphs[mid].glyphIndex == glyphI) {
slot.glyphs[mid].alignedOffset = alignedOff;
break;
}
if (slot.glyphs[mid].glyphIndex < glyphI)
left = mid + 1;
else
right = mid - 1;
}
if (glyph.width > 0 && glyph.height > 0) {
alignedOff += ((glyph.width + 3) / 4) * glyph.height;
}
}
}
}
// Step 4: For each unique group, decompress to temp buffer and extract needed glyphs
uint32_t writeOffset = 0;
int missed = 0;
for (uint8_t g = 0; g < groupCount; g++) {
uint16_t groupIdx = neededGroups[g];
const EpdFontGroup& group = fontData->groups[groupIdx];
auto* tempBuf = static_cast<uint8_t*>(malloc(group.uncompressedSize));
if (!tempBuf) {
LOG_ERR("FDC", "Failed to allocate temp buffer (%u bytes) for group %u", group.uncompressedSize, groupIdx);
missed++;
continue;
}
if (group.uncompressedSize > stats.peakTempBytes) {
stats.peakTempBytes = group.uncompressedSize;
}
if (!decompressGroup(fontData, groupIdx, tempBuf, group.uncompressedSize)) {
free(tempBuf);
missed++;
continue;
}
// Extract needed glyphs directly from the byte-aligned temp buffer, compacting on the fly.
// alignedOffset was pre-computed in step 3b — no full-group compact scan needed.
for (uint16_t i = 0; i < slot.glyphCount; i++) {
if (slot.glyphs[i].bufferOffset != UINT32_MAX) continue; // already extracted
if (getGroupIndex(fontData, slot.glyphs[i].glyphIndex) != groupIdx) continue;
const EpdGlyph& glyph = fontData->glyph[slot.glyphs[i].glyphIndex];
compactSingleGlyph(&tempBuf[slot.glyphs[i].alignedOffset], &slot.buffer[writeOffset], glyph.width, glyph.height);
slot.glyphs[i].bufferOffset = writeOffset;
writeOffset += glyph.dataLength;
}
free(tempBuf);
}
LOG_DBG("FDC", "Prewarm: %u glyphs in %u bytes from %u groups (%d missed)", glyphCount, writeOffset, groupCount,
missed);
return missed;
}
// --- Stats ---
void FontDecompressor::resetStats() { stats = Stats{}; }
void FontDecompressor::logStats(const char* label) {
const uint32_t total = stats.cacheHits + stats.cacheMisses;
LOG_DBG("FDC", "[%s] hits=%lu misses=%lu (%.1f%% hit rate)", label, stats.cacheHits, stats.cacheMisses,
total > 0 ? 100.0f * stats.cacheHits / total : 0.0f);
LOG_DBG("FDC", "[%s] decompress=%lums groups_accessed=%u", label, stats.decompressTimeMs, stats.uniqueGroupsAccessed);
LOG_DBG("FDC", "[%s] mem: pageBuf=%lu pageGlyphs=%lu hotGroup=%lu peakTemp=%lu", label, stats.pageBufferBytes,
stats.pageGlyphsBytes, stats.hotGroupBytes, stats.peakTempBytes);
if (stats.getBitmapCalls > 0) {
LOG_DBG("FDC", "[%s] getBitmap: %lu calls, %luus total, %luus/call avg", label, stats.getBitmapCalls,
stats.getBitmapTimeUs, stats.getBitmapTimeUs / stats.getBitmapCalls);
}
resetStats();
} }
+64 -19
View File
@@ -2,39 +2,84 @@
#include <InflateReader.h> #include <InflateReader.h>
#include <vector>
#include "EpdFontData.h" #include "EpdFontData.h"
class FontDecompressor { class FontDecompressor {
public: public:
static constexpr uint16_t MAX_PAGE_GLYPHS = 512;
static constexpr uint8_t MAX_PAGE_SLOTS = 4; // One per font style (R/B/I/BI)
FontDecompressor() = default;
~FontDecompressor();
bool init(); bool init();
void deinit(); void deinit();
// Returns pointer to decompressed bitmap data for the given glyph. // Returns pointer to decompressed bitmap data for the given glyph.
// Valid until LRU eviction (safe for the duration of one glyph render). // Checks the page buffer (from prewarm) first, then falls back to the hot group slot.
const uint8_t* getBitmap(const EpdFontData* fontData, const EpdGlyph* glyph, uint16_t glyphIndex); const uint8_t* getBitmap(const EpdFontData* fontData, const EpdGlyph* glyph, uint32_t glyphIndex);
// Evict all cached decompressed groups (call between pages for within-page-only caching). // Free all cached data (page buffer + hot group).
void clearCache(); void clearCache();
private: // Pre-scan UTF-8 text and extract needed glyph bitmaps into a flat page buffer.
static constexpr uint8_t CACHE_SLOTS = 4; // Each group is decompressed once into a temp buffer; only needed glyphs are kept.
// Returns the number of glyphs that couldn't be loaded (0 on full success).
int prewarmCache(const EpdFontData* fontData, const char* utf8Text);
struct CacheEntry { struct Stats {
const EpdFontData* font = nullptr; uint32_t cacheHits = 0;
uint16_t groupIndex = 0; uint32_t cacheMisses = 0;
uint8_t* data = nullptr; uint32_t decompressTimeMs = 0;
uint32_t dataSize = 0; uint16_t uniqueGroupsAccessed = 0;
uint32_t lastUsed = 0; uint32_t pageBufferBytes = 0; // pageBuffer allocation
bool valid = false; uint32_t pageGlyphsBytes = 0; // pageGlyphs lookup table allocation
uint32_t hotGroupBytes = 0; // current hot group allocation
uint32_t peakTempBytes = 0; // largest temp buffer in prewarm
uint32_t getBitmapTimeUs = 0; // cumulative getBitmap time (micros)
uint32_t getBitmapCalls = 0; // number of getBitmap calls
}; };
void logStats(const char* label = "FDC");
void resetStats();
const Stats& getStats() const { return stats; }
private:
Stats stats;
InflateReader inflateReader; InflateReader inflateReader;
CacheEntry cache[CACHE_SLOTS] = {};
uint32_t accessCounter = 0;
void freeAllEntries(); // Page buffer slots: each style gets its own flat glyph buffer with sorted lookup.
uint16_t getGroupIndex(const EpdFontData* fontData, uint16_t glyphIndex); // Up to MAX_PAGE_SLOTS (4) styles can be prewarmed simultaneously.
CacheEntry* findInCache(const EpdFontData* fontData, uint16_t groupIndex); struct PageGlyphEntry {
CacheEntry* findEvictionCandidate(); uint32_t glyphIndex;
bool decompressGroup(const EpdFontData* fontData, uint16_t groupIndex, CacheEntry* entry); uint32_t bufferOffset;
uint32_t alignedOffset; // byte-aligned offset within its decompressed group (set during prewarm pre-scan)
};
struct PageSlot {
uint8_t* buffer = nullptr;
const EpdFontData* fontData = nullptr;
PageGlyphEntry* glyphs = nullptr;
uint16_t glyphCount = 0;
};
PageSlot pageSlots[MAX_PAGE_SLOTS] = {};
uint8_t pageSlotCount = 0;
// Hot group: last decompressed group (byte-aligned) for non-prewarmed fallback path.
// Kept in byte-aligned format; individual glyphs are compacted on demand into hotGlyphBuf.
const EpdFontData* hotGroupFont = nullptr;
uint16_t hotGroupIndex = UINT16_MAX;
std::vector<uint8_t> hotGroup;
// Scratch buffer for compacting a single glyph from the hot group.
// Valid until the next getBitmap() call.
std::vector<uint8_t> hotGlyphBuf;
void freePageBuffer();
void freeHotGroup();
uint16_t getGroupIndex(const EpdFontData* fontData, uint32_t glyphIndex);
uint32_t getAlignedOffset(const EpdFontData* fontData, uint16_t groupIndex, uint32_t glyphIndex);
bool decompressGroup(const EpdFontData* fontData, uint16_t groupIndex, uint8_t* outBuf, uint32_t outSize);
static void compactSingleGlyph(const uint8_t* alignedSrc, uint8_t* packedDst, uint8_t width, uint8_t height);
static int32_t findGlyphIndex(const EpdFontData* fontData, uint32_t codepoint);
}; };
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -14,7 +14,7 @@ for size in ${BOOKERLY_FONT_SIZES[@]}; do
font_name="bookerly_${size}_$(echo $style | tr '[:upper:]' '[:lower:]')" font_name="bookerly_${size}_$(echo $style | tr '[:upper:]' '[:lower:]')"
font_path="../builtinFonts/source/Bookerly/Bookerly-${style}.ttf" font_path="../builtinFonts/source/Bookerly/Bookerly-${style}.ttf"
output_path="../builtinFonts/${font_name}.h" output_path="../builtinFonts/${font_name}.h"
python fontconvert.py $font_name $size $font_path --2bit --compress --force-autohint > $output_path python fontconvert.py $font_name $size $font_path --2bit --compress > $output_path
echo "Generated $output_path" echo "Generated $output_path"
done done
done done
+77 -14
View File
@@ -137,6 +137,33 @@ def norm_floor(val):
def norm_ceil(val): def norm_ceil(val):
return int(math.ceil(val / (1 << 6))) return int(math.ceil(val / (1 << 6)))
# Fixed-point (fp4) output conventions (must match EpdFontData.h / fp4 namespace):
#
# advanceX 12.4 unsigned fixed-point (uint16_t).
# 12 integer bits, 4 fractional bits = 1/16-pixel resolution.
# Encoded from FreeType's 16.16 linearHoriAdvance.
#
# kernMatrix 4.4 signed fixed-point (int8_t).
# 4 integer bits, 4 fractional bits = 1/16-pixel resolution.
# Range: -8.0 to +7.9375 pixels.
# Encoded from font design-unit kerning values.
#
# Both share 4 fractional bits so the renderer can add them directly into a
# single int32_t accumulator and defer rounding until pixel placement.
def fp4_from_ft16_16(val):
"""Convert FreeType 16.16 fixed-point to 12.4 fixed-point with rounding."""
return (val + (1 << 11)) >> 12
def fp4_from_design_units(du, scale):
"""Convert a font design-unit value to 4.4 fixed-point, clamped to int8_t.
Multiplies by scale (ppem / units_per_em) and shifts into 4 fractional
bits. The result is rounded to nearest and clamped to [-128, 127].
"""
raw = round(du * scale * 16)
return max(-128, min(127, raw))
def chunks(l, n): def chunks(l, n):
for i in range(0, len(l), n): for i in range(0, len(l), n):
yield l[i:i + n] yield l[i:i + n]
@@ -270,7 +297,9 @@ for i_start, i_end in intervals:
glyph = GlyphProps( glyph = GlyphProps(
width = bitmap.width, width = bitmap.width,
height = bitmap.rows, height = bitmap.rows,
advance_x = norm_floor(face.glyph.advance.x), # We use linearHoriAdvance (16.16 fixed-point, unhinted) instead of
# advance.x (26.6 fixed-point, grid-fitted to whole pixels by hinter)
advance_x = fp4_from_ft16_16(face.glyph.linearHoriAdvance),
left = face.glyph.bitmap_left, left = face.glyph.bitmap_left,
top = face.glyph.bitmap_top, top = face.glyph.bitmap_top,
data_length = len(packed), data_length = len(packed),
@@ -406,15 +435,14 @@ def extract_kerning_fonttools(font_path, codepoints, ppem):
font.close() font.close()
# Scale design-unit values to pixels # Scale design-unit kerning values to 4.4 fixed-point pixels.
scale = ppem / units_per_em scale = ppem / units_per_em
result = {} # (leftCp, rightCp) -> adjust result = {} # (leftCp, rightCp) -> 4.4 fixed-point adjust
for (lg, rg), du in raw_kern.items(): for (lg, rg), du in raw_kern.items():
lcp = glyph_to_cp[lg] lcp = glyph_to_cp[lg]
rcp = glyph_to_cp[rg] rcp = glyph_to_cp[rg]
adjust = int(math.floor(du * scale)) adjust = fp4_from_design_units(du, scale)
if adjust != 0: if adjust != 0:
adjust = max(-128, min(127, adjust))
result[(lcp, rcp)] = adjust result[(lcp, rcp)] = adjust
return result return result
@@ -661,7 +689,38 @@ print(f"ligatures: {len(ligature_pairs)} pairs extracted", file=sys.stderr)
compress = args.compress compress = args.compress
def to_byte_aligned(packed, width, height):
"""Convert packed 2-bit bitmap to byte-aligned format (rows padded to byte boundary).
In packed format, pixels flow continuously across row boundaries (4 pixels/byte).
In byte-aligned format, each row starts at a byte boundary, padding the last byte
of each row with zero bits if width % 4 != 0. This improves DEFLATE compression
because identical pixel rows produce identical byte patterns regardless of position.
"""
if width == 0 or height == 0:
return b''
row_stride = (width + 3) // 4 # bytes per byte-aligned row
aligned = bytearray(row_stride * height)
for y in range(height):
for x in range(width):
# Read pixel from packed format (continuous bit stream)
packed_pos = y * width + x
packed_byte_idx = packed_pos // 4
packed_shift = (3 - (packed_pos % 4)) * 2
pixel = (packed[packed_byte_idx] >> packed_shift) & 0x3
# Write pixel to byte-aligned format (row-aligned)
aligned_byte_idx = y * row_stride + x // 4
aligned_shift = (3 - (x % 4)) * 2
aligned[aligned_byte_idx] |= (pixel << aligned_shift)
return bytes(aligned)
# Build groups for compression # Build groups for compression
if compress and not is2Bit:
print("Error: --compress requires --2bit (byte-aligned compression only supports 2-bit format)", file=sys.stderr)
sys.exit(1)
if compress: if compress:
# Script-based grouping: glyphs that co-occur in typical text rendering # Script-based grouping: glyphs that co-occur in typical text rendering
# are grouped together for efficient LRU caching on the embedded target. # are grouped together for efficient LRU caching on the embedded target.
@@ -719,11 +778,12 @@ if compress:
for first_idx, count in groups: for first_idx, count in groups:
# Concatenate bitmap data for this group # Concatenate bitmap data for this group
group_data = b'' packed_len = 0
group_aligned = bytearray()
for gi in range(first_idx, first_idx + count): for gi in range(first_idx, first_idx + count):
props, packed = all_glyphs[gi] props, packed = all_glyphs[gi]
# Update glyph's dataOffset to be within-group offset # Update glyph's dataOffset to be within-group offset (packed offset)
within_group_offset = len(group_data) within_group_offset = packed_len
old_props = modified_glyph_props[gi] old_props = modified_glyph_props[gi]
modified_glyph_props[gi] = GlyphProps( modified_glyph_props[gi] = GlyphProps(
width=old_props.width, width=old_props.width,
@@ -735,13 +795,14 @@ if compress:
data_offset=within_group_offset, data_offset=within_group_offset,
code_point=old_props.code_point, code_point=old_props.code_point,
) )
group_data += packed packed_len += len(packed)
group_aligned.extend(to_byte_aligned(packed, old_props.width, old_props.height))
# Compress with raw DEFLATE (no zlib/gzip header) # Compress byte-aligned data with raw DEFLATE (no zlib/gzip header)
compressor = zlib.compressobj(level=9, wbits=-15) compressor = zlib.compressobj(level=9, wbits=-15)
compressed = compressor.compress(group_data) + compressor.flush() compressed = compressor.compress(bytes(group_aligned)) + compressor.flush()
compressed_groups.append((compressed, len(group_data), count, first_idx)) compressed_groups.append((compressed, len(group_aligned), count, first_idx))
compressed_bitmap_data.extend(compressed) compressed_bitmap_data.extend(compressed)
compressed_offset += len(compressed) compressed_offset += len(compressed)
@@ -834,8 +895,10 @@ if compress:
print(f" {font_name}Groups,") print(f" {font_name}Groups,")
print(f" {len(compressed_groups)},") print(f" {len(compressed_groups)},")
else: else:
print(f" nullptr,") print(" nullptr,")
print(f" 0,") print(" 0,")
# glyphToGroup (not used for script-grouped fonts)
print(" nullptr,")
if kern_map: if kern_map:
print(f" {font_name}KernLeftClasses,") print(f" {font_name}KernLeftClasses,")
print(f" {font_name}KernRightClasses,") print(f" {font_name}KernRightClasses,")
+118 -14
View File
@@ -3,9 +3,13 @@
Round-trip verification for compressed font headers. Round-trip verification for compressed font headers.
Parses each generated .h file in the given directory, identifies compressed fonts Parses each generated .h file in the given directory, identifies compressed fonts
(those with a Groups array), decompresses each group, and verifies that (those with a Groups array), decompresses each group (byte-aligned bitmap format),
decompression succeeds and all glyph offsets/lengths fall within bounds. compacts to packed format, and verifies the data matches expected glyph sizes.
Supports both contiguous-group fonts (Latin) and frequency-grouped fonts (CJK)
with glyphToGroup mapping arrays.
""" """
import math
import os import os
import re import re
import sys import sys
@@ -18,6 +22,11 @@ def parse_hex_array(text):
return bytes(int(h, 16) for h in hex_vals) return bytes(int(h, 16) for h in hex_vals)
def parse_uint8_array(text):
"""Extract uint8/uint16 values from a C array string like '{ 0, 1, 0xFF, ... }'"""
return [int(v, 0) for v in re.findall(r'\b0x[0-9A-Fa-f]+\b|\b\d+\b', text)]
def parse_groups(text): def parse_groups(text):
"""Parse EpdFontGroup array entries: { compressedOffset, compressedSize, uncompressedSize, glyphCount, firstGlyphIndex }""" """Parse EpdFontGroup array entries: { compressedOffset, compressedSize, uncompressedSize, glyphCount, firstGlyphIndex }"""
groups = [] groups = []
@@ -48,6 +57,45 @@ def parse_glyphs(text):
return glyphs return glyphs
def get_group_glyph_indices(group, group_index, glyphs, glyph_to_group):
"""Get the ordered list of glyph indices belonging to a group."""
if glyph_to_group is not None:
# Frequency-grouped: scan all glyphs
return [i for i in range(len(glyphs)) if glyph_to_group[i] == group_index]
else:
# Contiguous: sequential from firstGlyphIndex
first = group['firstGlyphIndex']
return list(range(first, first + group['glyphCount']))
def compact_aligned_to_packed(aligned_data, width, height):
"""Convert byte-aligned 2-bit bitmap to packed format (reverse of to_byte_aligned).
In byte-aligned format, each row starts at a byte boundary.
In packed format, pixels flow continuously across row boundaries (4 pixels/byte).
"""
if width == 0 or height == 0:
return b''
packed_size = math.ceil(width * height / 4)
packed = bytearray(packed_size)
row_stride = (width + 3) // 4 # bytes per byte-aligned row
for y in range(height):
for x in range(width):
# Read pixel from byte-aligned format (row-aligned)
aligned_byte_idx = y * row_stride + x // 4
aligned_shift = (3 - (x % 4)) * 2
pixel = (aligned_data[aligned_byte_idx] >> aligned_shift) & 0x3
# Write pixel to packed format (continuous bit stream)
packed_pos = y * width + x
packed_byte_idx = packed_pos // 4
packed_shift = (3 - (packed_pos % 4)) * 2
packed[packed_byte_idx] |= (pixel << packed_shift)
return bytes(packed)
def verify_font_file(filepath): def verify_font_file(filepath):
"""Verify a single font header file. Returns (font_name, success, message).""" """Verify a single font header file. Returns (font_name, success, message)."""
with open(filepath, 'r') as f: with open(filepath, 'r') as f:
@@ -92,6 +140,20 @@ def verify_font_file(filepath):
glyphs = parse_glyphs(glyphs_match.group(1)) glyphs = parse_glyphs(glyphs_match.group(1))
# Check for glyphToGroup array (frequency-grouped fonts)
glyph_to_group = None
g2g_match = re.search(
r'static const uint16_t ' + re.escape(font_name) + r'GlyphToGroup\[\]\s*=\s*\{(.+?)\};',
content, re.DOTALL
)
if g2g_match:
glyph_to_group = parse_uint8_array(g2g_match.group(1))
if len(glyph_to_group) != len(glyphs):
return (font_name, False, f"glyphToGroup length ({len(glyph_to_group)}) != glyph count ({len(glyphs)})")
max_group_id = max(glyph_to_group)
if max_group_id >= len(groups):
return (font_name, False, f"glyphToGroup contains group ID {max_group_id} but only {len(groups)} groups exist")
# Verify each group # Verify each group
for gi, group in enumerate(groups): for gi, group in enumerate(groups):
# Extract compressed chunk # Extract compressed chunk
@@ -99,7 +161,7 @@ def verify_font_file(filepath):
if len(chunk) != group['compressedSize']: if len(chunk) != group['compressedSize']:
return (font_name, False, f"group {gi}: compressed data truncated (expected {group['compressedSize']}, got {len(chunk)})") return (font_name, False, f"group {gi}: compressed data truncated (expected {group['compressedSize']}, got {len(chunk)})")
# Decompress with raw DEFLATE # Decompress with raw DEFLATE — result is byte-aligned data
try: try:
decompressed = zlib.decompress(chunk, -15) decompressed = zlib.decompress(chunk, -15)
except zlib.error as e: except zlib.error as e:
@@ -108,22 +170,64 @@ def verify_font_file(filepath):
if len(decompressed) != group['uncompressedSize']: if len(decompressed) != group['uncompressedSize']:
return (font_name, False, f"group {gi}: size mismatch (expected {group['uncompressedSize']}, got {len(decompressed)})") return (font_name, False, f"group {gi}: size mismatch (expected {group['uncompressedSize']}, got {len(decompressed)})")
# Verify each glyph's data within the group # Get glyph indices for this group
first = group['firstGlyphIndex'] group_glyph_indices = get_group_glyph_indices(group, gi, glyphs, glyph_to_group)
for j in range(group['glyphCount']): if glyph_to_group is not None and len(group_glyph_indices) != group['glyphCount']:
glyph_idx = first + j return (font_name, False,
f"group {gi}: glyphCount {group['glyphCount']} != mapping count {len(group_glyph_indices)}")
# Walk through byte-aligned data, compact each glyph, and verify against packed format
byte_aligned_offset = 0
packed_offset = 0
for glyph_idx in group_glyph_indices:
if glyph_idx >= len(glyphs): if glyph_idx >= len(glyphs):
return (font_name, False, f"group {gi}: glyph index {glyph_idx} out of range") return (font_name, False, f"group {gi}: glyph index {glyph_idx} out of range")
glyph = glyphs[glyph_idx] glyph = glyphs[glyph_idx]
offset = glyph['dataOffset'] width = glyph['width']
length = glyph['dataLength'] height = glyph['height']
if offset + length > len(decompressed): if width == 0 or height == 0:
return (font_name, False, f"group {gi}, glyph {glyph_idx}: data extends beyond decompressed buffer " # Zero-size glyphs should have dataOffset == current packed_offset and dataLength == 0
f"(offset={offset}, length={length}, decompressed_size={len(decompressed)})") if glyph['dataOffset'] != packed_offset:
return (font_name, False, f"group {gi}, glyph {glyph_idx}: zero-size glyph dataOffset {glyph['dataOffset']} != expected packed offset {packed_offset}")
if glyph['dataLength'] != 0:
return (font_name, False, f"group {gi}, glyph {glyph_idx}: zero-size glyph dataLength {glyph['dataLength']} != expected 0")
continue
return (font_name, True, f"{len(groups)} groups, {len(glyphs)} glyphs OK") aligned_size = ((width + 3) // 4) * height
packed_size = math.ceil(width * height / 4)
# Verify packed offset and size match glyph metadata
if glyph['dataOffset'] != packed_offset:
return (font_name, False, f"group {gi}, glyph {glyph_idx}: dataOffset {glyph['dataOffset']} != expected packed offset {packed_offset}")
if glyph['dataLength'] != packed_size:
return (font_name, False, f"group {gi}, glyph {glyph_idx}: dataLength {glyph['dataLength']} != expected packed length {packed_size} "
f"(width={width}, height={height})")
# Extract byte-aligned data for this glyph
if byte_aligned_offset + aligned_size > len(decompressed):
return (font_name, False, f"group {gi}, glyph {glyph_idx}: byte-aligned data extends beyond decompressed buffer "
f"(offset={byte_aligned_offset}, size={aligned_size}, buf_size={len(decompressed)})")
aligned_glyph = decompressed[byte_aligned_offset:byte_aligned_offset + aligned_size]
# Compact to packed and verify pixel values are valid (0-3 for 2-bit)
packed_glyph = compact_aligned_to_packed(aligned_glyph, width, height)
if len(packed_glyph) != packed_size:
return (font_name, False, f"group {gi}, glyph {glyph_idx}: compacted size {len(packed_glyph)} != expected {packed_size}")
byte_aligned_offset += aligned_size
packed_offset += packed_size
# Verify total byte-aligned size matches uncompressedSize
if byte_aligned_offset != group['uncompressedSize']:
return (font_name, False, f"group {gi}: total byte-aligned size {byte_aligned_offset} != uncompressedSize {group['uncompressedSize']}")
extra_info = ""
if glyph_to_group is not None:
extra_info = " (frequency-grouped)"
return (font_name, True, f"{len(groups)} groups, {len(glyphs)} glyphs OK{extra_info}")
def main(): def main():
+8 -13
View File
@@ -103,14 +103,11 @@ bool Epub::parseContentOpf(BookMetadataCache::BookMetadata& bookMetadata) {
pos += strlen(pattern); pos += strlen(pattern);
const auto endPos = coverPageHtml.find('"', pos); const auto endPos = coverPageHtml.find('"', pos);
if (endPos != std::string::npos) { if (endPos != std::string::npos) {
const auto ref = coverPageHtml.substr(pos, endPos - pos); const auto ref = std::string_view{coverPageHtml}.substr(pos, endPos - pos);
// Check if it's an image file // Check if it's an image file
if (ref.length() >= 4) { if (FsHelpers::hasPngExtension(ref) || FsHelpers::hasJpgExtension(ref) || FsHelpers::hasGifExtension(ref)) {
const auto ext = ref.substr(ref.length() - 4); imageRef = ref;
if (ext == ".png" || ext == ".jpg" || ext == "jpeg" || ext == ".gif") { break;
imageRef = ref;
break;
}
} }
} }
pos = coverPageHtml.find(pattern, pos); pos = coverPageHtml.find(pattern, pos);
@@ -541,8 +538,7 @@ bool Epub::generateCoverBmp(bool cropped) const {
return false; return false;
} }
if (coverImageHref.substr(coverImageHref.length() - 4) == ".jpg" || if (FsHelpers::hasJpgExtension(coverImageHref)) {
coverImageHref.substr(coverImageHref.length() - 5) == ".jpeg") {
LOG_DBG("EBP", "Generating BMP from JPG cover image (%s mode)", cropped ? "cropped" : "fit"); LOG_DBG("EBP", "Generating BMP from JPG cover image (%s mode)", cropped ? "cropped" : "fit");
const auto coverJpgTempPath = getCachePath() + "/.cover.jpg"; const auto coverJpgTempPath = getCachePath() + "/.cover.jpg";
@@ -575,7 +571,7 @@ bool Epub::generateCoverBmp(bool cropped) const {
return success; return success;
} }
if (coverImageHref.substr(coverImageHref.length() - 4) == ".png") { if (FsHelpers::hasPngExtension(coverImageHref)) {
LOG_DBG("EBP", "Generating BMP from PNG cover image (%s mode)", cropped ? "cropped" : "fit"); LOG_DBG("EBP", "Generating BMP from PNG cover image (%s mode)", cropped ? "cropped" : "fit");
const auto coverPngTempPath = getCachePath() + "/.cover.png"; const auto coverPngTempPath = getCachePath() + "/.cover.png";
@@ -629,8 +625,7 @@ bool Epub::generateThumbBmp(int height) const {
const auto coverImageHref = bookMetadataCache->coreMetadata.coverItemHref; const auto coverImageHref = bookMetadataCache->coreMetadata.coverItemHref;
if (coverImageHref.empty()) { if (coverImageHref.empty()) {
LOG_DBG("EBP", "No known cover image for thumbnail"); LOG_DBG("EBP", "No known cover image for thumbnail");
} else if (coverImageHref.substr(coverImageHref.length() - 4) == ".jpg" || } else if (FsHelpers::hasJpgExtension(coverImageHref)) {
coverImageHref.substr(coverImageHref.length() - 5) == ".jpeg") {
LOG_DBG("EBP", "Generating thumb BMP from JPG cover image"); LOG_DBG("EBP", "Generating thumb BMP from JPG cover image");
const auto coverJpgTempPath = getCachePath() + "/.cover.jpg"; const auto coverJpgTempPath = getCachePath() + "/.cover.jpg";
@@ -666,7 +661,7 @@ bool Epub::generateThumbBmp(int height) const {
} }
LOG_DBG("EBP", "Generated thumb BMP from JPG cover image, success: %s", success ? "yes" : "no"); LOG_DBG("EBP", "Generated thumb BMP from JPG cover image, success: %s", success ? "yes" : "no");
return success; return success;
} else if (coverImageHref.substr(coverImageHref.length() - 4) == ".png") { } else if (FsHelpers::hasPngExtension(coverImageHref)) {
LOG_DBG("EBP", "Generating thumb BMP from PNG cover image"); LOG_DBG("EBP", "Generating thumb BMP from PNG cover image");
const auto coverPngTempPath = getCachePath() + "/.cover.png"; const auto coverPngTempPath = getCachePath() + "/.cover.png";
+6 -4
View File
@@ -274,11 +274,13 @@ bool BookMetadataCache::buildBookBin(const std::string& epubPath, const BookMeta
} }
bool BookMetadataCache::cleanupTmpFiles() const { bool BookMetadataCache::cleanupTmpFiles() const {
if (Storage.exists((cachePath + tmpSpineBinFile).c_str())) { const auto spineBinFile = cachePath + tmpSpineBinFile;
Storage.remove((cachePath + tmpSpineBinFile).c_str()); if (Storage.exists(spineBinFile.c_str())) {
Storage.remove(spineBinFile.c_str());
} }
if (Storage.exists((cachePath + tmpTocBinFile).c_str())) { const auto tocBinFile = cachePath + tmpTocBinFile;
Storage.remove((cachePath + tmpTocBinFile).c_str()); if (Storage.exists(tocBinFile.c_str())) {
Storage.remove(tocBinFile.c_str());
} }
return true; return true;
} }
+1
View File
@@ -2,6 +2,7 @@
#include <HalStorage.h> #include <HalStorage.h>
#include <algorithm> #include <algorithm>
#include <string>
#include <utility> #include <utility>
#include <vector> #include <vector>
+37 -35
View File
@@ -101,20 +101,19 @@ void ParsedText::layoutAndExtractLines(const GfxRenderer& renderer, const int fo
applyParagraphIndent(); applyParagraphIndent();
const int pageWidth = viewportWidth; const int pageWidth = viewportWidth;
const int spaceWidth = renderer.getSpaceWidth(fontId, EpdFontFamily::REGULAR);
auto wordWidths = calculateWordWidths(renderer, fontId); auto wordWidths = calculateWordWidths(renderer, fontId);
std::vector<size_t> lineBreakIndices; std::vector<size_t> lineBreakIndices;
if (hyphenationEnabled) { if (hyphenationEnabled) {
// Use greedy layout that can split words mid-loop when a hyphenated prefix fits. // Use greedy layout that can split words mid-loop when a hyphenated prefix fits.
lineBreakIndices = computeHyphenatedLineBreaks(renderer, fontId, pageWidth, spaceWidth, wordWidths, wordContinues); lineBreakIndices = computeHyphenatedLineBreaks(renderer, fontId, pageWidth, wordWidths, wordContinues);
} else { } else {
lineBreakIndices = computeLineBreaks(renderer, fontId, pageWidth, spaceWidth, wordWidths, wordContinues); lineBreakIndices = computeLineBreaks(renderer, fontId, pageWidth, wordWidths, wordContinues);
} }
const size_t lineCount = includeLastLine ? lineBreakIndices.size() : lineBreakIndices.size() - 1; const size_t lineCount = includeLastLine ? lineBreakIndices.size() : lineBreakIndices.size() - 1;
for (size_t i = 0; i < lineCount; ++i) { for (size_t i = 0; i < lineCount; ++i) {
extractLine(i, pageWidth, spaceWidth, wordWidths, wordContinues, lineBreakIndices, processLine, renderer, fontId); extractLine(i, pageWidth, wordWidths, wordContinues, lineBreakIndices, processLine, renderer, fontId);
} }
// Remove consumed words so size() reflects only remaining words // Remove consumed words so size() reflects only remaining words
@@ -138,15 +137,17 @@ std::vector<uint16_t> ParsedText::calculateWordWidths(const GfxRenderer& rendere
} }
std::vector<size_t> ParsedText::computeLineBreaks(const GfxRenderer& renderer, const int fontId, const int pageWidth, std::vector<size_t> ParsedText::computeLineBreaks(const GfxRenderer& renderer, const int fontId, const int pageWidth,
const int spaceWidth, std::vector<uint16_t>& wordWidths, std::vector<uint16_t>& wordWidths, std::vector<bool>& continuesVec) {
std::vector<bool>& continuesVec) {
if (words.empty()) { if (words.empty()) {
return {}; return {};
} }
// Calculate first line indent (only for left/justified text without extra paragraph spacing) // Calculate first line indent (only for left/justified text).
// Positive text-indent (paragraph indent) is suppressed when extraParagraphSpacing is on.
// Negative text-indent (hanging indent, e.g. margin-left:3em; text-indent:-1em) always applies —
// it is structural (positions the bullet/marker), not decorative.
const int firstLineIndent = const int firstLineIndent =
blockStyle.textIndent > 0 && !extraParagraphSpacing && blockStyle.textIndentDefined && (blockStyle.textIndent < 0 || !extraParagraphSpacing) &&
(blockStyle.alignment == CssTextAlign::Justify || blockStyle.alignment == CssTextAlign::Left) (blockStyle.alignment == CssTextAlign::Justify || blockStyle.alignment == CssTextAlign::Left)
? blockStyle.textIndent ? blockStyle.textIndent
: 0; : 0;
@@ -184,9 +185,8 @@ std::vector<size_t> ParsedText::computeLineBreaks(const GfxRenderer& renderer, c
// Add space before word j, unless it's the first word on the line or a continuation // Add space before word j, unless it's the first word on the line or a continuation
int gap = 0; int gap = 0;
if (j > static_cast<size_t>(i) && !continuesVec[j]) { if (j > static_cast<size_t>(i) && !continuesVec[j]) {
gap = spaceWidth; gap =
gap += renderer.getSpaceKernAdjust(fontId, lastCodepoint(words[j - 1]), firstCodepoint(words[j]), renderer.getSpaceAdvance(fontId, lastCodepoint(words[j - 1]), firstCodepoint(words[j]), wordStyles[j - 1]);
wordStyles[j - 1]);
} else if (j > static_cast<size_t>(i) && continuesVec[j]) { } else if (j > static_cast<size_t>(i) && continuesVec[j]) {
// Cross-boundary kerning for continuation words (e.g. nonbreaking spaces, attached punctuation) // Cross-boundary kerning for continuation words (e.g. nonbreaking spaces, attached punctuation)
gap = renderer.getKerning(fontId, lastCodepoint(words[j - 1]), firstCodepoint(words[j]), wordStyles[j - 1]); gap = renderer.getKerning(fontId, lastCodepoint(words[j - 1]), firstCodepoint(words[j]), wordStyles[j - 1]);
@@ -272,12 +272,14 @@ void ParsedText::applyParagraphIndent() {
// Builds break indices while opportunistically splitting the word that would overflow the current line. // Builds break indices while opportunistically splitting the word that would overflow the current line.
std::vector<size_t> ParsedText::computeHyphenatedLineBreaks(const GfxRenderer& renderer, const int fontId, std::vector<size_t> ParsedText::computeHyphenatedLineBreaks(const GfxRenderer& renderer, const int fontId,
const int pageWidth, const int spaceWidth, const int pageWidth, std::vector<uint16_t>& wordWidths,
std::vector<uint16_t>& wordWidths,
std::vector<bool>& continuesVec) { std::vector<bool>& continuesVec) {
// Calculate first line indent (only for left/justified text without extra paragraph spacing) // Calculate first line indent (only for left/justified text).
// Positive text-indent (paragraph indent) is suppressed when extraParagraphSpacing is on.
// Negative text-indent (hanging indent, e.g. margin-left:3em; text-indent:-1em) always applies —
// it is structural (positions the bullet/marker), not decorative.
const int firstLineIndent = const int firstLineIndent =
blockStyle.textIndent > 0 && !extraParagraphSpacing && blockStyle.textIndentDefined && (blockStyle.textIndent < 0 || !extraParagraphSpacing) &&
(blockStyle.alignment == CssTextAlign::Justify || blockStyle.alignment == CssTextAlign::Left) (blockStyle.alignment == CssTextAlign::Justify || blockStyle.alignment == CssTextAlign::Left)
? blockStyle.textIndent ? blockStyle.textIndent
: 0; : 0;
@@ -298,9 +300,8 @@ std::vector<size_t> ParsedText::computeHyphenatedLineBreaks(const GfxRenderer& r
const bool isFirstWord = currentIndex == lineStart; const bool isFirstWord = currentIndex == lineStart;
int spacing = 0; int spacing = 0;
if (!isFirstWord && !continuesVec[currentIndex]) { if (!isFirstWord && !continuesVec[currentIndex]) {
spacing = spaceWidth; spacing = renderer.getSpaceAdvance(fontId, lastCodepoint(words[currentIndex - 1]),
spacing += renderer.getSpaceKernAdjust(fontId, lastCodepoint(words[currentIndex - 1]), firstCodepoint(words[currentIndex]), wordStyles[currentIndex - 1]);
firstCodepoint(words[currentIndex]), wordStyles[currentIndex - 1]);
} else if (!isFirstWord && continuesVec[currentIndex]) { } else if (!isFirstWord && continuesVec[currentIndex]) {
// Cross-boundary kerning for continuation words (e.g. nonbreaking spaces, attached punctuation) // Cross-boundary kerning for continuation words (e.g. nonbreaking spaces, attached punctuation)
spacing = renderer.getKerning(fontId, lastCodepoint(words[currentIndex - 1]), spacing = renderer.getKerning(fontId, lastCodepoint(words[currentIndex - 1]),
@@ -434,19 +435,21 @@ bool ParsedText::hyphenateWordAtIndex(const size_t wordIndex, const int availabl
return true; return true;
} }
void ParsedText::extractLine(const size_t breakIndex, const int pageWidth, const int spaceWidth, void ParsedText::extractLine(const size_t breakIndex, const int pageWidth, const std::vector<uint16_t>& wordWidths,
const std::vector<uint16_t>& wordWidths, const std::vector<bool>& continuesVec, const std::vector<bool>& continuesVec, const std::vector<size_t>& lineBreakIndices,
const std::vector<size_t>& lineBreakIndices,
const std::function<void(std::shared_ptr<TextBlock>)>& processLine, const std::function<void(std::shared_ptr<TextBlock>)>& processLine,
const GfxRenderer& renderer, const int fontId) { const GfxRenderer& renderer, const int fontId) {
const size_t lineBreak = lineBreakIndices[breakIndex]; const size_t lineBreak = lineBreakIndices[breakIndex];
const size_t lastBreakAt = breakIndex > 0 ? lineBreakIndices[breakIndex - 1] : 0; const size_t lastBreakAt = breakIndex > 0 ? lineBreakIndices[breakIndex - 1] : 0;
const size_t lineWordCount = lineBreak - lastBreakAt; const size_t lineWordCount = lineBreak - lastBreakAt;
// Calculate first line indent (only for left/justified text without extra paragraph spacing) // Calculate first line indent (only for left/justified text).
// Positive text-indent (paragraph indent) is suppressed when extraParagraphSpacing is on.
// Negative text-indent (hanging indent, e.g. margin-left:3em; text-indent:-1em) always applies —
// it is structural (positions the bullet/marker), not decorative.
const bool isFirstLine = breakIndex == 0; const bool isFirstLine = breakIndex == 0;
const int firstLineIndent = const int firstLineIndent =
isFirstLine && blockStyle.textIndent > 0 && !extraParagraphSpacing && isFirstLine && blockStyle.textIndentDefined && (blockStyle.textIndent < 0 || !extraParagraphSpacing) &&
(blockStyle.alignment == CssTextAlign::Justify || blockStyle.alignment == CssTextAlign::Left) (blockStyle.alignment == CssTextAlign::Justify || blockStyle.alignment == CssTextAlign::Left)
? blockStyle.textIndent ? blockStyle.textIndent
: 0; : 0;
@@ -462,11 +465,9 @@ void ParsedText::extractLine(const size_t breakIndex, const int pageWidth, const
// Count gaps: each word after the first creates a gap, unless it's a continuation // Count gaps: each word after the first creates a gap, unless it's a continuation
if (wordIdx > 0 && !continuesVec[lastBreakAt + wordIdx]) { if (wordIdx > 0 && !continuesVec[lastBreakAt + wordIdx]) {
actualGapCount++; actualGapCount++;
int naturalGap = spaceWidth; totalNaturalGaps +=
naturalGap += renderer.getSpaceKernAdjust(fontId, lastCodepoint(words[lastBreakAt + wordIdx - 1]), renderer.getSpaceAdvance(fontId, lastCodepoint(words[lastBreakAt + wordIdx - 1]),
firstCodepoint(words[lastBreakAt + wordIdx]), firstCodepoint(words[lastBreakAt + wordIdx]), wordStyles[lastBreakAt + wordIdx - 1]);
wordStyles[lastBreakAt + wordIdx - 1]);
totalNaturalGaps += naturalGap;
} else if (wordIdx > 0 && continuesVec[lastBreakAt + wordIdx]) { } else if (wordIdx > 0 && continuesVec[lastBreakAt + wordIdx]) {
// Cross-boundary kerning for continuation words (e.g. nonbreaking spaces, attached punctuation) // Cross-boundary kerning for continuation words (e.g. nonbreaking spaces, attached punctuation)
totalNaturalGaps += totalNaturalGaps +=
@@ -485,8 +486,9 @@ void ParsedText::extractLine(const size_t breakIndex, const int pageWidth, const
? spareSpace / static_cast<int>(actualGapCount) ? spareSpace / static_cast<int>(actualGapCount)
: 0; : 0;
// Calculate initial x position (first line starts at indent for left/justified text) // Calculate initial x position (first line starts at indent for left/justified text;
auto xpos = static_cast<uint16_t>(firstLineIndent); // may be negative for hanging indents, e.g. margin-left:3em; text-indent:-1em).
auto xpos = static_cast<int16_t>(firstLineIndent);
if (blockStyle.alignment == CssTextAlign::Right) { if (blockStyle.alignment == CssTextAlign::Right) {
xpos = effectivePageWidth - lineWordWidthSum - totalNaturalGaps; xpos = effectivePageWidth - lineWordWidthSum - totalNaturalGaps;
} else if (blockStyle.alignment == CssTextAlign::Center) { } else if (blockStyle.alignment == CssTextAlign::Center) {
@@ -495,7 +497,7 @@ void ParsedText::extractLine(const size_t breakIndex, const int pageWidth, const
// Pre-calculate X positions for words // Pre-calculate X positions for words
// Continuation words attach to the previous word with no space before them // Continuation words attach to the previous word with no space before them
std::vector<uint16_t> lineXPos; std::vector<int16_t> lineXPos;
lineXPos.reserve(lineWordCount); lineXPos.reserve(lineWordCount);
for (size_t wordIdx = 0; wordIdx < lineWordCount; wordIdx++) { for (size_t wordIdx = 0; wordIdx < lineWordCount; wordIdx++) {
@@ -510,11 +512,11 @@ void ParsedText::extractLine(const size_t breakIndex, const int pageWidth, const
firstCodepoint(words[lastBreakAt + wordIdx + 1]), wordStyles[lastBreakAt + wordIdx]); firstCodepoint(words[lastBreakAt + wordIdx + 1]), wordStyles[lastBreakAt + wordIdx]);
xpos += advance; xpos += advance;
} else { } else {
int gap = spaceWidth; int gap = 0;
if (wordIdx + 1 < lineWordCount) { if (wordIdx + 1 < lineWordCount) {
gap += renderer.getSpaceKernAdjust(fontId, lastCodepoint(words[lastBreakAt + wordIdx]), gap = renderer.getSpaceAdvance(fontId, lastCodepoint(words[lastBreakAt + wordIdx]),
firstCodepoint(words[lastBreakAt + wordIdx + 1]), firstCodepoint(words[lastBreakAt + wordIdx + 1]),
wordStyles[lastBreakAt + wordIdx]); wordStyles[lastBreakAt + wordIdx]);
} }
if (blockStyle.alignment == CssTextAlign::Justify && !isLastLine) { if (blockStyle.alignment == CssTextAlign::Justify && !isLastLine) {
gap += justifyExtra; gap += justifyExtra;
+3 -4
View File
@@ -21,14 +21,13 @@ class ParsedText {
bool hyphenationEnabled; bool hyphenationEnabled;
void applyParagraphIndent(); void applyParagraphIndent();
std::vector<size_t> computeLineBreaks(const GfxRenderer& renderer, int fontId, int pageWidth, int spaceWidth, std::vector<size_t> computeLineBreaks(const GfxRenderer& renderer, int fontId, int pageWidth,
std::vector<uint16_t>& wordWidths, std::vector<bool>& continuesVec); std::vector<uint16_t>& wordWidths, std::vector<bool>& continuesVec);
std::vector<size_t> computeHyphenatedLineBreaks(const GfxRenderer& renderer, int fontId, int pageWidth, std::vector<size_t> computeHyphenatedLineBreaks(const GfxRenderer& renderer, int fontId, int pageWidth,
int spaceWidth, std::vector<uint16_t>& wordWidths, std::vector<uint16_t>& wordWidths, std::vector<bool>& continuesVec);
std::vector<bool>& continuesVec);
bool hyphenateWordAtIndex(size_t wordIndex, int availableWidth, const GfxRenderer& renderer, int fontId, bool hyphenateWordAtIndex(size_t wordIndex, int availableWidth, const GfxRenderer& renderer, int fontId,
std::vector<uint16_t>& wordWidths, bool allowFallbackBreaks); std::vector<uint16_t>& wordWidths, bool allowFallbackBreaks);
void extractLine(size_t breakIndex, int pageWidth, int spaceWidth, const std::vector<uint16_t>& wordWidths, void extractLine(size_t breakIndex, int pageWidth, const std::vector<uint16_t>& wordWidths,
const std::vector<bool>& continuesVec, const std::vector<size_t>& lineBreakIndices, const std::vector<bool>& continuesVec, const std::vector<size_t>& lineBreakIndices,
const std::function<void(std::shared_ptr<TextBlock>)>& processLine, const GfxRenderer& renderer, const std::function<void(std::shared_ptr<TextBlock>)>& processLine, const GfxRenderer& renderer,
int fontId); int fontId);
+62 -13
View File
@@ -10,10 +10,10 @@
#include "parsers/ChapterHtmlSlimParser.h" #include "parsers/ChapterHtmlSlimParser.h"
namespace { namespace {
constexpr uint8_t SECTION_FILE_VERSION = 14; constexpr uint8_t SECTION_FILE_VERSION = 18;
constexpr uint32_t HEADER_SIZE = sizeof(uint8_t) + sizeof(int) + sizeof(float) + sizeof(bool) + sizeof(uint8_t) + constexpr uint32_t HEADER_SIZE = sizeof(uint8_t) + sizeof(int) + sizeof(float) + sizeof(bool) + sizeof(uint8_t) +
sizeof(uint16_t) + sizeof(uint16_t) + sizeof(uint16_t) + sizeof(bool) + sizeof(bool) + sizeof(uint16_t) + sizeof(uint16_t) + sizeof(uint16_t) + sizeof(bool) + sizeof(bool) +
sizeof(uint32_t); sizeof(uint8_t) + sizeof(uint32_t) + sizeof(uint32_t);
} // namespace } // namespace
uint32_t Section::onPageComplete(std::unique_ptr<Page> page) { uint32_t Section::onPageComplete(std::unique_ptr<Page> page) {
@@ -36,7 +36,7 @@ uint32_t Section::onPageComplete(std::unique_ptr<Page> page) {
void Section::writeSectionFileHeader(const int fontId, const float lineCompression, const bool extraParagraphSpacing, void Section::writeSectionFileHeader(const int fontId, const float lineCompression, const bool extraParagraphSpacing,
const uint8_t paragraphAlignment, const uint16_t viewportWidth, const uint8_t paragraphAlignment, const uint16_t viewportWidth,
const uint16_t viewportHeight, const bool hyphenationEnabled, const uint16_t viewportHeight, const bool hyphenationEnabled,
const bool embeddedStyle) { const bool embeddedStyle, const uint8_t imageRendering) {
if (!file) { if (!file) {
LOG_DBG("SCT", "File not open for writing header"); LOG_DBG("SCT", "File not open for writing header");
return; return;
@@ -44,7 +44,7 @@ void Section::writeSectionFileHeader(const int fontId, const float lineCompressi
static_assert(HEADER_SIZE == sizeof(SECTION_FILE_VERSION) + sizeof(fontId) + sizeof(lineCompression) + static_assert(HEADER_SIZE == sizeof(SECTION_FILE_VERSION) + sizeof(fontId) + sizeof(lineCompression) +
sizeof(extraParagraphSpacing) + sizeof(paragraphAlignment) + sizeof(viewportWidth) + sizeof(extraParagraphSpacing) + sizeof(paragraphAlignment) + sizeof(viewportWidth) +
sizeof(viewportHeight) + sizeof(pageCount) + sizeof(hyphenationEnabled) + sizeof(viewportHeight) + sizeof(pageCount) + sizeof(hyphenationEnabled) +
sizeof(embeddedStyle) + sizeof(uint32_t), sizeof(embeddedStyle) + sizeof(imageRendering) + sizeof(uint32_t) + sizeof(uint32_t),
"Header size mismatch"); "Header size mismatch");
serialization::writePod(file, SECTION_FILE_VERSION); serialization::writePod(file, SECTION_FILE_VERSION);
serialization::writePod(file, fontId); serialization::writePod(file, fontId);
@@ -55,13 +55,16 @@ void Section::writeSectionFileHeader(const int fontId, const float lineCompressi
serialization::writePod(file, viewportHeight); serialization::writePod(file, viewportHeight);
serialization::writePod(file, hyphenationEnabled); serialization::writePod(file, hyphenationEnabled);
serialization::writePod(file, embeddedStyle); serialization::writePod(file, embeddedStyle);
serialization::writePod(file, pageCount); // Placeholder for page count (will be initially 0 when written) serialization::writePod(file, imageRendering);
serialization::writePod(file, static_cast<uint32_t>(0)); // Placeholder for LUT offset serialization::writePod(file, pageCount); // Placeholder for page count (will be initially 0, patched later)
serialization::writePod(file, static_cast<uint32_t>(0)); // Placeholder for LUT offset (patched later)
serialization::writePod(file, static_cast<uint32_t>(0)); // Placeholder for anchor map offset (patched later)
} }
bool Section::loadSectionFile(const int fontId, const float lineCompression, const bool extraParagraphSpacing, bool Section::loadSectionFile(const int fontId, const float lineCompression, const bool extraParagraphSpacing,
const uint8_t paragraphAlignment, const uint16_t viewportWidth, const uint8_t paragraphAlignment, const uint16_t viewportWidth,
const uint16_t viewportHeight, const bool hyphenationEnabled, const bool embeddedStyle) { const uint16_t viewportHeight, const bool hyphenationEnabled, const bool embeddedStyle,
const uint8_t imageRendering) {
if (!Storage.openFileForRead("SCT", filePath, file)) { if (!Storage.openFileForRead("SCT", filePath, file)) {
return false; return false;
} }
@@ -83,6 +86,7 @@ bool Section::loadSectionFile(const int fontId, const float lineCompression, con
uint8_t fileParagraphAlignment; uint8_t fileParagraphAlignment;
bool fileHyphenationEnabled; bool fileHyphenationEnabled;
bool fileEmbeddedStyle; bool fileEmbeddedStyle;
uint8_t fileImageRendering;
serialization::readPod(file, fileFontId); serialization::readPod(file, fileFontId);
serialization::readPod(file, fileLineCompression); serialization::readPod(file, fileLineCompression);
serialization::readPod(file, fileExtraParagraphSpacing); serialization::readPod(file, fileExtraParagraphSpacing);
@@ -91,11 +95,13 @@ bool Section::loadSectionFile(const int fontId, const float lineCompression, con
serialization::readPod(file, fileViewportHeight); serialization::readPod(file, fileViewportHeight);
serialization::readPod(file, fileHyphenationEnabled); serialization::readPod(file, fileHyphenationEnabled);
serialization::readPod(file, fileEmbeddedStyle); serialization::readPod(file, fileEmbeddedStyle);
serialization::readPod(file, fileImageRendering);
if (fontId != fileFontId || lineCompression != fileLineCompression || if (fontId != fileFontId || lineCompression != fileLineCompression ||
extraParagraphSpacing != fileExtraParagraphSpacing || paragraphAlignment != fileParagraphAlignment || extraParagraphSpacing != fileExtraParagraphSpacing || paragraphAlignment != fileParagraphAlignment ||
viewportWidth != fileViewportWidth || viewportHeight != fileViewportHeight || viewportWidth != fileViewportWidth || viewportHeight != fileViewportHeight ||
hyphenationEnabled != fileHyphenationEnabled || embeddedStyle != fileEmbeddedStyle) { hyphenationEnabled != fileHyphenationEnabled || embeddedStyle != fileEmbeddedStyle ||
imageRendering != fileImageRendering) {
LOG_ERR("SCT", "Deserialization failed: Parameters do not match"); LOG_ERR("SCT", "Deserialization failed: Parameters do not match");
clearCache(); // closes file before removal clearCache(); // closes file before removal
return false; return false;
@@ -157,7 +163,7 @@ bool Section::clearCache() {
bool Section::createSectionFile(const int fontId, const float lineCompression, const bool extraParagraphSpacing, bool Section::createSectionFile(const int fontId, const float lineCompression, const bool extraParagraphSpacing,
const uint8_t paragraphAlignment, const uint16_t viewportWidth, const uint8_t paragraphAlignment, const uint16_t viewportWidth,
const uint16_t viewportHeight, const bool hyphenationEnabled, const bool embeddedStyle, const uint16_t viewportHeight, const bool hyphenationEnabled, const bool embeddedStyle,
const std::function<void()>& popupFn) { const uint8_t imageRendering, const std::function<void()>& popupFn) {
const auto localPath = epub->getSpineItem(spineIndex).href; const auto localPath = epub->getSpineItem(spineIndex).href;
const auto tmpHtmlPath = epub->getCachePath() + "/.tmp_" + std::to_string(spineIndex) + ".html"; const auto tmpHtmlPath = epub->getCachePath() + "/.tmp_" + std::to_string(spineIndex) + ".html";
@@ -207,7 +213,7 @@ bool Section::createSectionFile(const int fontId, const float lineCompression, c
return false; return false;
} }
writeSectionFileHeader(fontId, lineCompression, extraParagraphSpacing, paragraphAlignment, viewportWidth, writeSectionFileHeader(fontId, lineCompression, extraParagraphSpacing, paragraphAlignment, viewportWidth,
viewportHeight, hyphenationEnabled, embeddedStyle); viewportHeight, hyphenationEnabled, embeddedStyle, imageRendering);
std::vector<uint32_t> lut = {}; std::vector<uint32_t> lut = {};
// Derive the content base directory and image cache path prefix for the parser // Derive the content base directory and image cache path prefix for the parser
@@ -229,7 +235,7 @@ bool Section::createSectionFile(const int fontId, const float lineCompression, c
epub, tmpHtmlPath, renderer, fontId, lineCompression, extraParagraphSpacing, paragraphAlignment, viewportWidth, epub, tmpHtmlPath, renderer, fontId, lineCompression, extraParagraphSpacing, paragraphAlignment, viewportWidth,
viewportHeight, hyphenationEnabled, viewportHeight, hyphenationEnabled,
[this, &lut](std::unique_ptr<Page> page) { lut.emplace_back(this->onPageComplete(std::move(page))); }, [this, &lut](std::unique_ptr<Page> page) { lut.emplace_back(this->onPageComplete(std::move(page))); },
embeddedStyle, contentBase, imageBasePath, popupFn, cssParser); embeddedStyle, contentBase, imageBasePath, imageRendering, popupFn, cssParser);
Hyphenator::setPreferredLanguage(epub->getLanguage()); Hyphenator::setPreferredLanguage(epub->getLanguage());
success = visitor.parseAndBuildPages(); success = visitor.parseAndBuildPages();
@@ -262,10 +268,20 @@ bool Section::createSectionFile(const int fontId, const float lineCompression, c
return false; return false;
} }
// Go back and write LUT offset // Write anchor-to-page map for fragment navigation (e.g. footnote targets)
file.seek(HEADER_SIZE - sizeof(uint32_t) - sizeof(pageCount)); const uint32_t anchorMapOffset = file.position();
const auto& anchors = visitor.getAnchors();
serialization::writePod(file, static_cast<uint16_t>(anchors.size()));
for (const auto& [anchor, page] : anchors) {
serialization::writeString(file, anchor);
serialization::writePod(file, page);
}
// Patch header with final pageCount, lutOffset, and anchorMapOffset
file.seek(HEADER_SIZE - sizeof(uint32_t) * 2 - sizeof(pageCount));
serialization::writePod(file, pageCount); serialization::writePod(file, pageCount);
serialization::writePod(file, lutOffset); serialization::writePod(file, lutOffset);
serialization::writePod(file, anchorMapOffset);
file.close(); file.close();
if (cssParser) { if (cssParser) {
cssParser->clear(); cssParser->clear();
@@ -303,3 +319,36 @@ std::unique_ptr<Page> Section::loadPageFromSectionFile() {
return Page::deserialize(file); return Page::deserialize(file);
// File is intentionally NOT closed; stays open for the next page load // File is intentionally NOT closed; stays open for the next page load
} }
std::optional<uint16_t> Section::getPageForAnchor(const std::string& anchor) const {
FsFile f;
if (!Storage.openFileForRead("SCT", filePath, f)) {
return std::nullopt;
}
const uint32_t fileSize = f.size();
f.seek(HEADER_SIZE - sizeof(uint32_t));
uint32_t anchorMapOffset;
serialization::readPod(f, anchorMapOffset);
if (anchorMapOffset == 0 || anchorMapOffset >= fileSize) {
f.close();
return std::nullopt;
}
f.seek(anchorMapOffset);
uint16_t count;
serialization::readPod(f, count);
for (uint16_t i = 0; i < count; i++) {
std::string key;
uint16_t page;
serialization::readString(f, key);
serialization::readPod(f, page);
if (key == anchor) {
f.close();
return page;
}
}
f.close();
return std::nullopt;
}
+9 -3
View File
@@ -1,6 +1,8 @@
#pragma once #pragma once
#include <functional> #include <functional>
#include <memory> #include <memory>
#include <optional>
#include <string>
#include <vector> #include <vector>
#include "Epub.h" #include "Epub.h"
@@ -18,7 +20,7 @@ class Section {
void writeSectionFileHeader(int fontId, float lineCompression, bool extraParagraphSpacing, uint8_t paragraphAlignment, void writeSectionFileHeader(int fontId, float lineCompression, bool extraParagraphSpacing, uint8_t paragraphAlignment,
uint16_t viewportWidth, uint16_t viewportHeight, bool hyphenationEnabled, uint16_t viewportWidth, uint16_t viewportHeight, bool hyphenationEnabled,
bool embeddedStyle); bool embeddedStyle, uint8_t imageRendering);
uint32_t onPageComplete(std::unique_ptr<Page> page); uint32_t onPageComplete(std::unique_ptr<Page> page);
public: public:
@@ -32,10 +34,14 @@ class Section {
filePath(epub->getCachePath() + "/sections/" + std::to_string(spineIndex) + ".bin") {} filePath(epub->getCachePath() + "/sections/" + std::to_string(spineIndex) + ".bin") {}
~Section() = default; ~Section() = default;
bool loadSectionFile(int fontId, float lineCompression, bool extraParagraphSpacing, uint8_t paragraphAlignment, bool loadSectionFile(int fontId, float lineCompression, bool extraParagraphSpacing, uint8_t paragraphAlignment,
uint16_t viewportWidth, uint16_t viewportHeight, bool hyphenationEnabled, bool embeddedStyle); uint16_t viewportWidth, uint16_t viewportHeight, bool hyphenationEnabled, bool embeddedStyle,
uint8_t imageRendering);
bool clearCache(); bool clearCache();
bool createSectionFile(int fontId, float lineCompression, bool extraParagraphSpacing, uint8_t paragraphAlignment, bool createSectionFile(int fontId, float lineCompression, bool extraParagraphSpacing, uint8_t paragraphAlignment,
uint16_t viewportWidth, uint16_t viewportHeight, bool hyphenationEnabled, bool embeddedStyle, uint16_t viewportWidth, uint16_t viewportHeight, bool hyphenationEnabled, bool embeddedStyle,
const std::function<void()>& popupFn = nullptr); uint8_t imageRendering, const std::function<void()>& popupFn = nullptr);
std::unique_ptr<Page> loadPageFromSectionFile(); std::unique_ptr<Page> loadPageFromSectionFile();
// Look up the page number for an anchor id from the section cache file.
std::optional<uint16_t> getPageForAnchor(const std::string& anchor) const;
}; };
+1 -1
View File
@@ -74,7 +74,7 @@ bool TextBlock::serialize(FsFile& file) const {
std::unique_ptr<TextBlock> TextBlock::deserialize(FsFile& file) { std::unique_ptr<TextBlock> TextBlock::deserialize(FsFile& file) {
uint16_t wc; uint16_t wc;
std::vector<std::string> words; std::vector<std::string> words;
std::vector<uint16_t> wordXpos; std::vector<int16_t> wordXpos;
std::vector<EpdFontFamily::Style> wordStyles; std::vector<EpdFontFamily::Style> wordStyles;
BlockStyle blockStyle; BlockStyle blockStyle;
+2 -2
View File
@@ -13,12 +13,12 @@
class TextBlock final : public Block { class TextBlock final : public Block {
private: private:
std::vector<std::string> words; std::vector<std::string> words;
std::vector<uint16_t> wordXpos; std::vector<int16_t> wordXpos;
std::vector<EpdFontFamily::Style> wordStyles; std::vector<EpdFontFamily::Style> wordStyles;
BlockStyle blockStyle; BlockStyle blockStyle;
public: public:
explicit TextBlock(std::vector<std::string> words, std::vector<uint16_t> word_xpos, explicit TextBlock(std::vector<std::string> words, std::vector<int16_t> word_xpos,
std::vector<EpdFontFamily::Style> word_styles, const BlockStyle& blockStyle = BlockStyle()) std::vector<EpdFontFamily::Style> word_styles, const BlockStyle& blockStyle = BlockStyle())
: words(std::move(words)), : words(std::move(words)),
wordXpos(std::move(word_xpos)), wordXpos(std::move(word_xpos)),
@@ -1,45 +1,360 @@
#include "JpegToFramebufferConverter.h" #include "JpegToFramebufferConverter.h"
#include <FsHelpers.h>
#include <GfxRenderer.h> #include <GfxRenderer.h>
#include <HalStorage.h> #include <HalStorage.h>
#include <JPEGDEC.h>
#include <Logging.h> #include <Logging.h>
#include <picojpeg.h>
#include <cstdio> #include <cstdlib>
#include <cstring> #include <new>
#include "DitherUtils.h" #include "DitherUtils.h"
#include "PixelCache.h" #include "PixelCache.h"
namespace {
// Context struct passed through JPEGDEC callbacks to avoid global mutable state.
// The draw callback receives this via pDraw->pUser (set by setUserPointer()).
// The file I/O callbacks receive the FsFile* via pFile->fHandle (set by jpegOpen()).
struct JpegContext { struct JpegContext {
FsFile& file; GfxRenderer* renderer;
uint8_t buffer[512]; const RenderConfig* config;
size_t bufferPos; int screenWidth;
size_t bufferFilled; int screenHeight;
JpegContext(FsFile& f) : file(f), bufferPos(0), bufferFilled(0) {}
// Source dimensions after JPEGDEC's built-in scaling
int scaledSrcWidth;
int scaledSrcHeight;
// Final output dimensions
int dstWidth;
int dstHeight;
// Fine scale in 16.16 fixed-point (ESP32-C3 has no FPU)
int32_t fineScaleFP; // src -> dst mapping
int32_t invScaleFP; // dst -> src mapping
PixelCache cache;
bool caching;
JpegContext()
: renderer(nullptr),
config(nullptr),
screenWidth(0),
screenHeight(0),
scaledSrcWidth(0),
scaledSrcHeight(0),
dstWidth(0),
dstHeight(0),
fineScaleFP(1 << 16),
invScaleFP(1 << 16),
caching(false) {}
}; };
// File I/O callbacks use pFile->fHandle to access the FsFile*,
// avoiding the need for global file state.
void* jpegOpen(const char* filename, int32_t* size) {
FsFile* f = new FsFile();
if (!Storage.openFileForRead("JPG", std::string(filename), *f)) {
delete f;
return nullptr;
}
*size = f->size();
return f;
}
void jpegClose(void* handle) {
FsFile* f = reinterpret_cast<FsFile*>(handle);
if (f) {
f->close();
delete f;
}
}
// JPEGDEC tracks file position via pFile->iPos internally (e.g. JPEGGetMoreData
// checks iPos < iSize to decide whether more data is available). The callbacks
// MUST maintain iPos to match the actual file position, otherwise progressive
// JPEGs with large headers fail during parsing.
int32_t jpegRead(JPEGFILE* pFile, uint8_t* pBuf, int32_t len) {
FsFile* f = reinterpret_cast<FsFile*>(pFile->fHandle);
if (!f) return 0;
int32_t bytesRead = f->read(pBuf, len);
if (bytesRead < 0) return 0;
pFile->iPos += bytesRead;
return bytesRead;
}
int32_t jpegSeek(JPEGFILE* pFile, int32_t pos) {
FsFile* f = reinterpret_cast<FsFile*>(pFile->fHandle);
if (!f) return -1;
if (!f->seek(pos)) return -1;
pFile->iPos = pos;
return pos;
}
// JPEGDEC object is ~17 KB due to internal decode buffers.
// Heap-allocate on demand so memory is only used during active decode.
constexpr size_t JPEG_DECODER_APPROX_SIZE = 20 * 1024;
constexpr size_t MIN_FREE_HEAP_FOR_JPEG = JPEG_DECODER_APPROX_SIZE + 16 * 1024;
// Choose JPEGDEC's built-in scale factor for coarse downscaling.
// Returns the scale denominator (1, 2, 4, or 8) and sets jpegScaleOption.
int chooseJpegScale(float targetScale, int& jpegScaleOption) {
if (targetScale <= 0.125f) {
jpegScaleOption = JPEG_SCALE_EIGHTH;
return 8;
}
if (targetScale <= 0.25f) {
jpegScaleOption = JPEG_SCALE_QUARTER;
return 4;
}
if (targetScale <= 0.5f) {
jpegScaleOption = JPEG_SCALE_HALF;
return 2;
}
jpegScaleOption = 0;
return 1;
}
// Fixed-point 16.16 arithmetic avoids software float emulation on ESP32-C3 (no FPU).
constexpr int FP_SHIFT = 16;
constexpr int32_t FP_ONE = 1 << FP_SHIFT;
constexpr int32_t FP_MASK = FP_ONE - 1;
int jpegDrawCallback(JPEGDRAW* pDraw) {
JpegContext* ctx = reinterpret_cast<JpegContext*>(pDraw->pUser);
if (!ctx || !ctx->config || !ctx->renderer) return 0;
// In EIGHT_BIT_GRAYSCALE mode, pPixels contains 8-bit grayscale values
// Buffer is densely packed: stride = pDraw->iWidth, valid columns = pDraw->iWidthUsed
uint8_t* pixels = reinterpret_cast<uint8_t*>(pDraw->pPixels);
const int stride = pDraw->iWidth;
const int validW = pDraw->iWidthUsed;
const int blockH = pDraw->iHeight;
if (stride <= 0 || blockH <= 0 || validW <= 0) return 1;
const bool useDithering = ctx->config->useDithering;
const bool caching = ctx->caching;
const int32_t fineScaleFP = ctx->fineScaleFP;
const int32_t invScaleFP = ctx->invScaleFP;
GfxRenderer& renderer = *ctx->renderer;
const int cfgX = ctx->config->x;
const int cfgY = ctx->config->y;
const int blockX = pDraw->x;
const int blockY = pDraw->y;
// Determine destination pixel range covered by this source block
const int srcYEnd = blockY + blockH;
const int srcXEnd = blockX + validW;
int dstYStart = (int)((int64_t)blockY * fineScaleFP >> FP_SHIFT);
int dstYEnd = (srcYEnd >= ctx->scaledSrcHeight) ? ctx->dstHeight : (int)((int64_t)srcYEnd * fineScaleFP >> FP_SHIFT);
int dstXStart = (int)((int64_t)blockX * fineScaleFP >> FP_SHIFT);
int dstXEnd = (srcXEnd >= ctx->scaledSrcWidth) ? ctx->dstWidth : (int)((int64_t)srcXEnd * fineScaleFP >> FP_SHIFT);
// Pre-clamp destination ranges to screen bounds (eliminates per-pixel screen checks)
int clampYMax = ctx->dstHeight;
if (ctx->screenHeight - cfgY < clampYMax) clampYMax = ctx->screenHeight - cfgY;
if (dstYStart < -cfgY) dstYStart = -cfgY;
if (dstYEnd > clampYMax) dstYEnd = clampYMax;
int clampXMax = ctx->dstWidth;
if (ctx->screenWidth - cfgX < clampXMax) clampXMax = ctx->screenWidth - cfgX;
if (dstXStart < -cfgX) dstXStart = -cfgX;
if (dstXEnd > clampXMax) dstXEnd = clampXMax;
if (dstYStart >= dstYEnd || dstXStart >= dstXEnd) return 1;
// === 1:1 fast path: no scaling math ===
if (fineScaleFP == FP_ONE) {
for (int dstY = dstYStart; dstY < dstYEnd; dstY++) {
const int outY = cfgY + dstY;
const uint8_t* row = &pixels[(dstY - blockY) * stride];
for (int dstX = dstXStart; dstX < dstXEnd; dstX++) {
const int outX = cfgX + dstX;
uint8_t gray = row[dstX - blockX];
uint8_t dithered;
if (useDithering) {
dithered = applyBayerDither4Level(gray, outX, outY);
} else {
dithered = gray / 85;
if (dithered > 3) dithered = 3;
}
drawPixelWithRenderMode(renderer, outX, outY, dithered);
if (caching) ctx->cache.setPixel(outX, outY, dithered);
}
}
return 1;
}
// === Bilinear interpolation (upscale: fineScale > 1.0) ===
// Smooths block boundaries that would otherwise create visible banding
// on progressive JPEG DC-only decode (1/8 resolution upscaled to target).
if (fineScaleFP > FP_ONE) {
// Pre-compute safe X range where lx0 and lx0+1 are both in [0, validW-1].
// Only the left/right edge pixels (typically 0-2 and 1-8 respectively) need clamping.
int safeXStart = (int)(((int64_t)blockX * fineScaleFP + FP_MASK) >> FP_SHIFT);
int safeXEnd = (int)((int64_t)(blockX + validW - 1) * fineScaleFP >> FP_SHIFT);
if (safeXStart < dstXStart) safeXStart = dstXStart;
if (safeXEnd > dstXEnd) safeXEnd = dstXEnd;
if (safeXStart > safeXEnd) safeXEnd = safeXStart;
for (int dstY = dstYStart; dstY < dstYEnd; dstY++) {
const int outY = cfgY + dstY;
const int32_t srcFyFP = dstY * invScaleFP;
const int32_t fy = srcFyFP & FP_MASK;
const int32_t fyInv = FP_ONE - fy;
int ly0 = (srcFyFP >> FP_SHIFT) - blockY;
int ly1 = ly0 + 1;
if (ly0 < 0) ly0 = 0;
if (ly0 >= blockH) ly0 = blockH - 1;
if (ly1 >= blockH) ly1 = blockH - 1;
const uint8_t* row0 = &pixels[ly0 * stride];
const uint8_t* row1 = &pixels[ly1 * stride];
// Left edge (with X boundary clamping)
for (int dstX = dstXStart; dstX < safeXStart; dstX++) {
const int outX = cfgX + dstX;
const int32_t srcFxFP = dstX * invScaleFP;
const int32_t fx = srcFxFP & FP_MASK;
const int32_t fxInv = FP_ONE - fx;
int lx0 = (srcFxFP >> FP_SHIFT) - blockX;
int lx1 = lx0 + 1;
if (lx0 < 0) lx0 = 0;
if (lx1 < 0) lx1 = 0;
if (lx0 >= validW) lx0 = validW - 1;
if (lx1 >= validW) lx1 = validW - 1;
int top = ((int)row0[lx0] * fxInv + (int)row0[lx1] * fx) >> FP_SHIFT;
int bot = ((int)row1[lx0] * fxInv + (int)row1[lx1] * fx) >> FP_SHIFT;
uint8_t gray = (uint8_t)((top * fyInv + bot * fy) >> FP_SHIFT);
uint8_t dithered;
if (useDithering) {
dithered = applyBayerDither4Level(gray, outX, outY);
} else {
dithered = gray / 85;
if (dithered > 3) dithered = 3;
}
drawPixelWithRenderMode(renderer, outX, outY, dithered);
if (caching) ctx->cache.setPixel(outX, outY, dithered);
}
// Interior (no X boundary checks — lx0 and lx0+1 guaranteed in bounds)
for (int dstX = safeXStart; dstX < safeXEnd; dstX++) {
const int outX = cfgX + dstX;
const int32_t srcFxFP = dstX * invScaleFP;
const int32_t fx = srcFxFP & FP_MASK;
const int32_t fxInv = FP_ONE - fx;
const int lx0 = (srcFxFP >> FP_SHIFT) - blockX;
int top = ((int)row0[lx0] * fxInv + (int)row0[lx0 + 1] * fx) >> FP_SHIFT;
int bot = ((int)row1[lx0] * fxInv + (int)row1[lx0 + 1] * fx) >> FP_SHIFT;
uint8_t gray = (uint8_t)((top * fyInv + bot * fy) >> FP_SHIFT);
uint8_t dithered;
if (useDithering) {
dithered = applyBayerDither4Level(gray, outX, outY);
} else {
dithered = gray / 85;
if (dithered > 3) dithered = 3;
}
drawPixelWithRenderMode(renderer, outX, outY, dithered);
if (caching) ctx->cache.setPixel(outX, outY, dithered);
}
// Right edge (with X boundary clamping)
for (int dstX = safeXEnd; dstX < dstXEnd; dstX++) {
const int outX = cfgX + dstX;
const int32_t srcFxFP = dstX * invScaleFP;
const int32_t fx = srcFxFP & FP_MASK;
const int32_t fxInv = FP_ONE - fx;
int lx0 = (srcFxFP >> FP_SHIFT) - blockX;
int lx1 = lx0 + 1;
if (lx0 >= validW) lx0 = validW - 1;
if (lx1 >= validW) lx1 = validW - 1;
int top = ((int)row0[lx0] * fxInv + (int)row0[lx1] * fx) >> FP_SHIFT;
int bot = ((int)row1[lx0] * fxInv + (int)row1[lx1] * fx) >> FP_SHIFT;
uint8_t gray = (uint8_t)((top * fyInv + bot * fy) >> FP_SHIFT);
uint8_t dithered;
if (useDithering) {
dithered = applyBayerDither4Level(gray, outX, outY);
} else {
dithered = gray / 85;
if (dithered > 3) dithered = 3;
}
drawPixelWithRenderMode(renderer, outX, outY, dithered);
if (caching) ctx->cache.setPixel(outX, outY, dithered);
}
}
return 1;
}
// === Nearest-neighbor (downscale: fineScale < 1.0) ===
for (int dstY = dstYStart; dstY < dstYEnd; dstY++) {
const int outY = cfgY + dstY;
const int32_t srcFyFP = dstY * invScaleFP;
int ly = (srcFyFP >> FP_SHIFT) - blockY;
if (ly < 0) ly = 0;
if (ly >= blockH) ly = blockH - 1;
const uint8_t* row = &pixels[ly * stride];
for (int dstX = dstXStart; dstX < dstXEnd; dstX++) {
const int outX = cfgX + dstX;
const int32_t srcFxFP = dstX * invScaleFP;
int lx = (srcFxFP >> FP_SHIFT) - blockX;
if (lx < 0) lx = 0;
if (lx >= validW) lx = validW - 1;
uint8_t gray = row[lx];
uint8_t dithered;
if (useDithering) {
dithered = applyBayerDither4Level(gray, outX, outY);
} else {
dithered = gray / 85;
if (dithered > 3) dithered = 3;
}
drawPixelWithRenderMode(renderer, outX, outY, dithered);
if (caching) ctx->cache.setPixel(outX, outY, dithered);
}
}
return 1;
}
} // namespace
bool JpegToFramebufferConverter::getDimensionsStatic(const std::string& imagePath, ImageDimensions& out) { bool JpegToFramebufferConverter::getDimensionsStatic(const std::string& imagePath, ImageDimensions& out) {
FsFile file; size_t freeHeap = ESP.getFreeHeap();
if (!Storage.openFileForRead("JPG", imagePath, file)) { if (freeHeap < MIN_FREE_HEAP_FOR_JPEG) {
LOG_ERR("JPG", "Failed to open file for dimensions: %s", imagePath.c_str()); LOG_ERR("JPG", "Not enough heap for JPEG decoder (%u free, need %u)", freeHeap, MIN_FREE_HEAP_FOR_JPEG);
return false; return false;
} }
JpegContext context(file); JPEGDEC* jpeg = new (std::nothrow) JPEGDEC();
pjpeg_image_info_t imageInfo; if (!jpeg) {
LOG_ERR("JPG", "Failed to allocate JPEG decoder for dimensions");
int status = pjpeg_decode_init(&imageInfo, jpegReadCallback, &context, 0);
file.close();
if (status != 0) {
LOG_ERR("JPG", "Failed to init JPEG for dimensions: %d", status);
return false; return false;
} }
out.width = imageInfo.m_width; int rc = jpeg->open(imagePath.c_str(), jpegOpen, jpegClose, jpegRead, jpegSeek, nullptr);
out.height = imageInfo.m_height; if (rc != 1) {
LOG_ERR("JPG", "Failed to open JPEG for dimensions (err=%d): %s", jpeg->getLastError(), imagePath.c_str());
delete jpeg;
return false;
}
out.width = jpeg->getWidth();
out.height = jpeg->getHeight();
LOG_DBG("JPG", "Image dimensions: %dx%d", out.width, out.height); LOG_DBG("JPG", "Image dimensions: %dx%d", out.width, out.height);
jpeg->close();
delete jpeg;
return true; return true;
} }
@@ -47,250 +362,130 @@ bool JpegToFramebufferConverter::decodeToFramebuffer(const std::string& imagePat
const RenderConfig& config) { const RenderConfig& config) {
LOG_DBG("JPG", "Decoding JPEG: %s", imagePath.c_str()); LOG_DBG("JPG", "Decoding JPEG: %s", imagePath.c_str());
FsFile file; size_t freeHeap = ESP.getFreeHeap();
if (!Storage.openFileForRead("JPG", imagePath, file)) { if (freeHeap < MIN_FREE_HEAP_FOR_JPEG) {
LOG_ERR("JPG", "Failed to open file: %s", imagePath.c_str()); LOG_ERR("JPG", "Not enough heap for JPEG decoder (%u free, need %u)", freeHeap, MIN_FREE_HEAP_FOR_JPEG);
return false; return false;
} }
JpegContext context(file); JPEGDEC* jpeg = new (std::nothrow) JPEGDEC();
pjpeg_image_info_t imageInfo; if (!jpeg) {
LOG_ERR("JPG", "Failed to allocate JPEG decoder");
int status = pjpeg_decode_init(&imageInfo, jpegReadCallback, &context, 0);
if (status != 0) {
LOG_ERR("JPG", "picojpeg init failed: %d", status);
file.close();
return false; return false;
} }
if (!validateImageDimensions(imageInfo.m_width, imageInfo.m_height, "JPEG")) { JpegContext ctx;
file.close(); ctx.renderer = &renderer;
ctx.config = &config;
ctx.screenWidth = renderer.getScreenWidth();
ctx.screenHeight = renderer.getScreenHeight();
int rc = jpeg->open(imagePath.c_str(), jpegOpen, jpegClose, jpegRead, jpegSeek, jpegDrawCallback);
if (rc != 1) {
LOG_ERR("JPG", "Failed to open JPEG (err=%d): %s", jpeg->getLastError(), imagePath.c_str());
delete jpeg;
return false; return false;
} }
// Calculate output dimensions int srcWidth = jpeg->getWidth();
int srcHeight = jpeg->getHeight();
if (srcWidth <= 0 || srcHeight <= 0) {
LOG_ERR("JPG", "Invalid JPEG dimensions: %dx%d", srcWidth, srcHeight);
jpeg->close();
delete jpeg;
return false;
}
if (!validateImageDimensions(srcWidth, srcHeight, "JPEG")) {
jpeg->close();
delete jpeg;
return false;
}
bool isProgressive = jpeg->getJPEGType() == JPEG_MODE_PROGRESSIVE;
if (isProgressive) {
LOG_INF("JPG", "Progressive JPEG detected - decoding DC coefficients only (lower quality)");
}
// Calculate overall target scale
float targetScale;
int destWidth, destHeight; int destWidth, destHeight;
float scale;
if (config.useExactDimensions && config.maxWidth > 0 && config.maxHeight > 0) { if (config.useExactDimensions && config.maxWidth > 0 && config.maxHeight > 0) {
// Use exact dimensions as specified (avoids rounding mismatches with pre-calculated sizes)
destWidth = config.maxWidth; destWidth = config.maxWidth;
destHeight = config.maxHeight; destHeight = config.maxHeight;
scale = (float)destWidth / imageInfo.m_width; targetScale = (float)destWidth / srcWidth;
} else { } else {
// Calculate scale factor to fit within maxWidth/maxHeight float scaleX = (config.maxWidth > 0 && srcWidth > config.maxWidth) ? (float)config.maxWidth / srcWidth : 1.0f;
float scaleX = (config.maxWidth > 0 && imageInfo.m_width > config.maxWidth) float scaleY = (config.maxHeight > 0 && srcHeight > config.maxHeight) ? (float)config.maxHeight / srcHeight : 1.0f;
? (float)config.maxWidth / imageInfo.m_width targetScale = (scaleX < scaleY) ? scaleX : scaleY;
: 1.0f; if (targetScale > 1.0f) targetScale = 1.0f;
float scaleY = (config.maxHeight > 0 && imageInfo.m_height > config.maxHeight)
? (float)config.maxHeight / imageInfo.m_height
: 1.0f;
scale = (scaleX < scaleY) ? scaleX : scaleY;
if (scale > 1.0f) scale = 1.0f;
destWidth = (int)(imageInfo.m_width * scale); destWidth = (int)(srcWidth * targetScale);
destHeight = (int)(imageInfo.m_height * scale); destHeight = (int)(srcHeight * targetScale);
} }
LOG_DBG("JPG", "JPEG %dx%d -> %dx%d (scale %.2f), scan type: %d, MCU: %dx%d", imageInfo.m_width, imageInfo.m_height, // Choose JPEGDEC built-in scaling for coarse downscaling.
destWidth, destHeight, scale, imageInfo.m_scanType, imageInfo.m_MCUWidth, imageInfo.m_MCUHeight); // Progressive JPEGs: JPEGDEC forces JPEG_SCALE_EIGHTH internally (DC-only
// decode produces 1/8 resolution). We must match this to avoid the if/else
// priority chain in DecodeJPEG selecting a different scale.
int jpegScaleOption;
int jpegScaleDenom;
if (isProgressive) {
jpegScaleOption = JPEG_SCALE_EIGHTH;
jpegScaleDenom = 8;
} else {
jpegScaleDenom = chooseJpegScale(targetScale, jpegScaleOption);
}
if (!imageInfo.m_pMCUBufR || !imageInfo.m_pMCUBufG || !imageInfo.m_pMCUBufB) { ctx.scaledSrcWidth = (srcWidth + jpegScaleDenom - 1) / jpegScaleDenom;
LOG_ERR("JPG", "Null buffer pointers in imageInfo"); ctx.scaledSrcHeight = (srcHeight + jpegScaleDenom - 1) / jpegScaleDenom;
file.close(); ctx.dstWidth = destWidth;
ctx.dstHeight = destHeight;
ctx.fineScaleFP = (int32_t)((int64_t)destWidth * FP_ONE / ctx.scaledSrcWidth);
ctx.invScaleFP = (int32_t)((int64_t)ctx.scaledSrcWidth * FP_ONE / destWidth);
LOG_DBG("JPG", "JPEG %dx%d -> %dx%d (scale %.2f, jpegScale 1/%d, fineScale %.2f)%s", srcWidth, srcHeight, destWidth,
destHeight, targetScale, jpegScaleDenom, (float)destWidth / ctx.scaledSrcWidth,
isProgressive ? " [progressive]" : "");
// Set pixel type to 8-bit grayscale (must be after open())
jpeg->setPixelType(EIGHT_BIT_GRAYSCALE);
jpeg->setUserPointer(&ctx);
// Allocate cache buffer using final output dimensions
ctx.caching = !config.cachePath.empty();
if (ctx.caching) {
if (!ctx.cache.allocate(destWidth, destHeight, config.x, config.y)) {
LOG_ERR("JPG", "Failed to allocate cache buffer, continuing without caching");
ctx.caching = false;
}
}
unsigned long decodeStart = millis();
rc = jpeg->decode(0, 0, jpegScaleOption);
unsigned long decodeTime = millis() - decodeStart;
if (rc != 1) {
LOG_ERR("JPG", "Decode failed (rc=%d, lastError=%d)", rc, jpeg->getLastError());
jpeg->close();
delete jpeg;
return false; return false;
} }
const int screenWidth = renderer.getScreenWidth(); jpeg->close();
const int screenHeight = renderer.getScreenHeight(); delete jpeg;
LOG_DBG("JPG", "JPEG decoding complete - render time: %lu ms", decodeTime);
// Allocate pixel cache if cachePath is provided
PixelCache cache;
bool caching = !config.cachePath.empty();
if (caching) {
if (!cache.allocate(destWidth, destHeight, config.x, config.y)) {
LOG_ERR("JPG", "Failed to allocate cache buffer, continuing without caching");
caching = false;
}
}
int mcuX = 0;
int mcuY = 0;
while (mcuY < imageInfo.m_MCUSPerCol) {
status = pjpeg_decode_mcu();
if (status == PJPG_NO_MORE_BLOCKS) {
break;
}
if (status != 0) {
LOG_ERR("JPG", "MCU decode failed: %d", status);
file.close();
return false;
}
// Source position in image coordinates
int srcStartX = mcuX * imageInfo.m_MCUWidth;
int srcStartY = mcuY * imageInfo.m_MCUHeight;
switch (imageInfo.m_scanType) {
case PJPG_GRAYSCALE:
for (int row = 0; row < 8; row++) {
int srcY = srcStartY + row;
int destY = config.y + (int)(srcY * scale);
if (destY >= screenHeight || destY >= config.y + destHeight) continue;
for (int col = 0; col < 8; col++) {
int srcX = srcStartX + col;
int destX = config.x + (int)(srcX * scale);
if (destX >= screenWidth || destX >= config.x + destWidth) continue;
uint8_t gray = imageInfo.m_pMCUBufR[row * 8 + col];
uint8_t dithered = config.useDithering ? applyBayerDither4Level(gray, destX, destY) : gray / 85;
if (dithered > 3) dithered = 3;
drawPixelWithRenderMode(renderer, destX, destY, dithered);
if (caching) cache.setPixel(destX, destY, dithered);
}
}
break;
case PJPG_YH1V1:
for (int row = 0; row < 8; row++) {
int srcY = srcStartY + row;
int destY = config.y + (int)(srcY * scale);
if (destY >= screenHeight || destY >= config.y + destHeight) continue;
for (int col = 0; col < 8; col++) {
int srcX = srcStartX + col;
int destX = config.x + (int)(srcX * scale);
if (destX >= screenWidth || destX >= config.x + destWidth) continue;
uint8_t r = imageInfo.m_pMCUBufR[row * 8 + col];
uint8_t g = imageInfo.m_pMCUBufG[row * 8 + col];
uint8_t b = imageInfo.m_pMCUBufB[row * 8 + col];
uint8_t gray = (uint8_t)((r * 77 + g * 150 + b * 29) >> 8);
uint8_t dithered = config.useDithering ? applyBayerDither4Level(gray, destX, destY) : gray / 85;
if (dithered > 3) dithered = 3;
drawPixelWithRenderMode(renderer, destX, destY, dithered);
if (caching) cache.setPixel(destX, destY, dithered);
}
}
break;
case PJPG_YH2V1:
for (int row = 0; row < 8; row++) {
int srcY = srcStartY + row;
int destY = config.y + (int)(srcY * scale);
if (destY >= screenHeight || destY >= config.y + destHeight) continue;
for (int col = 0; col < 16; col++) {
int srcX = srcStartX + col;
int destX = config.x + (int)(srcX * scale);
if (destX >= screenWidth || destX >= config.x + destWidth) continue;
int blockIndex = (col < 8) ? 0 : 1;
int pixelIndex = row * 8 + (col % 8);
uint8_t r = imageInfo.m_pMCUBufR[blockIndex * 64 + pixelIndex];
uint8_t g = imageInfo.m_pMCUBufG[blockIndex * 64 + pixelIndex];
uint8_t b = imageInfo.m_pMCUBufB[blockIndex * 64 + pixelIndex];
uint8_t gray = (uint8_t)((r * 77 + g * 150 + b * 29) >> 8);
uint8_t dithered = config.useDithering ? applyBayerDither4Level(gray, destX, destY) : gray / 85;
if (dithered > 3) dithered = 3;
drawPixelWithRenderMode(renderer, destX, destY, dithered);
if (caching) cache.setPixel(destX, destY, dithered);
}
}
break;
case PJPG_YH1V2:
for (int row = 0; row < 16; row++) {
int srcY = srcStartY + row;
int destY = config.y + (int)(srcY * scale);
if (destY >= screenHeight || destY >= config.y + destHeight) continue;
for (int col = 0; col < 8; col++) {
int srcX = srcStartX + col;
int destX = config.x + (int)(srcX * scale);
if (destX >= screenWidth || destX >= config.x + destWidth) continue;
int blockIndex = (row < 8) ? 0 : 1;
int pixelIndex = (row % 8) * 8 + col;
uint8_t r = imageInfo.m_pMCUBufR[blockIndex * 128 + pixelIndex];
uint8_t g = imageInfo.m_pMCUBufG[blockIndex * 128 + pixelIndex];
uint8_t b = imageInfo.m_pMCUBufB[blockIndex * 128 + pixelIndex];
uint8_t gray = (uint8_t)((r * 77 + g * 150 + b * 29) >> 8);
uint8_t dithered = config.useDithering ? applyBayerDither4Level(gray, destX, destY) : gray / 85;
if (dithered > 3) dithered = 3;
drawPixelWithRenderMode(renderer, destX, destY, dithered);
if (caching) cache.setPixel(destX, destY, dithered);
}
}
break;
case PJPG_YH2V2:
for (int row = 0; row < 16; row++) {
int srcY = srcStartY + row;
int destY = config.y + (int)(srcY * scale);
if (destY >= screenHeight || destY >= config.y + destHeight) continue;
for (int col = 0; col < 16; col++) {
int srcX = srcStartX + col;
int destX = config.x + (int)(srcX * scale);
if (destX >= screenWidth || destX >= config.x + destWidth) continue;
int blockX = (col < 8) ? 0 : 1;
int blockY = (row < 8) ? 0 : 1;
int blockIndex = blockY * 2 + blockX;
int pixelIndex = (row % 8) * 8 + (col % 8);
int blockOffset = blockIndex * 64;
uint8_t r = imageInfo.m_pMCUBufR[blockOffset + pixelIndex];
uint8_t g = imageInfo.m_pMCUBufG[blockOffset + pixelIndex];
uint8_t b = imageInfo.m_pMCUBufB[blockOffset + pixelIndex];
uint8_t gray = (uint8_t)((r * 77 + g * 150 + b * 29) >> 8);
uint8_t dithered = config.useDithering ? applyBayerDither4Level(gray, destX, destY) : gray / 85;
if (dithered > 3) dithered = 3;
drawPixelWithRenderMode(renderer, destX, destY, dithered);
if (caching) cache.setPixel(destX, destY, dithered);
}
}
break;
}
mcuX++;
if (mcuX >= imageInfo.m_MCUSPerRow) {
mcuX = 0;
mcuY++;
}
}
LOG_DBG("JPG", "Decoding complete");
file.close();
// Write cache file if caching was enabled // Write cache file if caching was enabled
if (caching) { if (ctx.caching) {
cache.writeToFile(config.cachePath); ctx.cache.writeToFile(config.cachePath);
} }
return true; return true;
} }
unsigned char JpegToFramebufferConverter::jpegReadCallback(unsigned char* pBuf, unsigned char buf_size,
unsigned char* pBytes_actually_read, void* pCallback_data) {
JpegContext* context = reinterpret_cast<JpegContext*>(pCallback_data);
if (context->bufferPos >= context->bufferFilled) {
int readCount = context->file.read(context->buffer, sizeof(context->buffer));
if (readCount <= 0) {
*pBytes_actually_read = 0;
return 0;
}
context->bufferFilled = readCount;
context->bufferPos = 0;
}
unsigned int bytesAvailable = context->bufferFilled - context->bufferPos;
unsigned int bytesToCopy = (bytesAvailable < buf_size) ? bytesAvailable : buf_size;
memcpy(pBuf, &context->buffer[context->bufferPos], bytesToCopy);
context->bufferPos += bytesToCopy;
*pBytes_actually_read = bytesToCopy;
return 0;
}
bool JpegToFramebufferConverter::supportsFormat(const std::string& extension) { bool JpegToFramebufferConverter::supportsFormat(const std::string& extension) {
std::string ext = extension; return FsHelpers::hasJpgExtension(extension);
for (auto& c : ext) {
c = tolower(c);
}
return (ext == ".jpg" || ext == ".jpeg");
} }
@@ -1,4 +1,5 @@
#pragma once #pragma once
#include <stdint.h> #include <stdint.h>
#include <string> #include <string>
@@ -17,8 +18,4 @@ class JpegToFramebufferConverter final : public ImageToFramebufferDecoder {
static bool supportsFormat(const std::string& extension); static bool supportsFormat(const std::string& extension);
const char* getFormatName() const override { return "JPEG"; } const char* getFormatName() const override { return "JPEG"; }
private:
static unsigned char jpegReadCallback(unsigned char* pBuf, unsigned char buf_size,
unsigned char* pBytes_actually_read, void* pCallback_data);
}; };
@@ -1,5 +1,6 @@
#include "PngToFramebufferConverter.h" #include "PngToFramebufferConverter.h"
#include <FsHelpers.h>
#include <GfxRenderer.h> #include <GfxRenderer.h>
#include <HalStorage.h> #include <HalStorage.h>
#include <Logging.h> #include <Logging.h>
@@ -391,9 +392,5 @@ bool PngToFramebufferConverter::decodeToFramebuffer(const std::string& imagePath
} }
bool PngToFramebufferConverter::supportsFormat(const std::string& extension) { bool PngToFramebufferConverter::supportsFormat(const std::string& extension) {
std::string ext = extension; return FsHelpers::hasPngExtension(extension);
for (auto& c : ext) {
c = tolower(c);
}
return (ext == ".png");
} }
+74
View File
@@ -52,6 +52,29 @@ constexpr size_t MAX_SELECTOR_LENGTH = 256;
// Check if character is CSS whitespace // Check if character is CSS whitespace
bool isCssWhitespace(const char c) { return c == ' ' || c == '\t' || c == '\n' || c == '\r' || c == '\f'; } bool isCssWhitespace(const char c) { return c == ' ' || c == '\t' || c == '\n' || c == '\r' || c == '\f'; }
std::string_view stripTrailingImportant(std::string_view value) {
constexpr std::string_view IMPORTANT = "!important";
while (!value.empty() && isCssWhitespace(value.back())) {
value.remove_suffix(1);
}
if (value.size() < IMPORTANT.size()) {
return value;
}
const size_t suffixPos = value.size() - IMPORTANT.size();
if (value.substr(suffixPos) != IMPORTANT) {
return value;
}
value.remove_suffix(IMPORTANT.size());
while (!value.empty() && isCssWhitespace(value.back())) {
value.remove_suffix(1);
}
return value;
}
} // anonymous namespace } // anonymous namespace
// String utilities implementation // String utilities implementation
@@ -317,6 +340,10 @@ void CssParser::parseDeclarationIntoStyle(const std::string& decl, CssStyle& sty
style.imageWidth = len; style.imageWidth = len;
style.defined.imageWidth = 1; style.defined.imageWidth = 1;
} }
} else if (propNameBuf == "display") {
const std::string_view displayValue = stripTrailingImportant(propValueBuf);
style.display = (displayValue == "none") ? CssDisplay::None : CssDisplay::Block;
style.defined.display = 1;
} }
} }
@@ -692,6 +719,7 @@ bool CssParser::saveToCache() const {
writeLength(style.paddingRight); writeLength(style.paddingRight);
writeLength(style.imageHeight); writeLength(style.imageHeight);
writeLength(style.imageWidth); writeLength(style.imageWidth);
file.write(static_cast<uint8_t>(style.display));
// Write defined flags as uint16_t // Write defined flags as uint16_t
uint16_t definedBits = 0; uint16_t definedBits = 0;
@@ -710,6 +738,7 @@ bool CssParser::saveToCache() const {
if (style.defined.paddingRight) definedBits |= 1 << 12; if (style.defined.paddingRight) definedBits |= 1 << 12;
if (style.defined.imageHeight) definedBits |= 1 << 13; if (style.defined.imageHeight) definedBits |= 1 << 13;
if (style.defined.imageWidth) definedBits |= 1 << 14; if (style.defined.imageWidth) definedBits |= 1 << 14;
if (style.defined.display) definedBits |= 1 << 15;
file.write(reinterpret_cast<const uint8_t*>(&definedBits), sizeof(definedBits)); file.write(reinterpret_cast<const uint8_t*>(&definedBits), sizeof(definedBits));
} }
@@ -748,16 +777,44 @@ bool CssParser::loadFromCache() {
return false; return false;
} }
if (ruleCount > MAX_RULES) {
LOG_DBG("CSS", "Invalid cache rule count (%u > %zu)", ruleCount, MAX_RULES);
rulesBySelector_.clear();
file.close();
return false;
}
auto hasRemainingBytes = [&file](const size_t neededBytes) -> bool {
return static_cast<size_t>(file.available()) >= neededBytes;
};
constexpr size_t CSS_LENGTH_FIELD_COUNT = 11;
constexpr size_t CSS_LENGTH_BYTES = sizeof(float) + sizeof(uint8_t);
constexpr size_t CSS_FIXED_STYLE_BYTES =
4 * sizeof(uint8_t) + (CSS_LENGTH_FIELD_COUNT * CSS_LENGTH_BYTES) + sizeof(uint8_t) + sizeof(uint16_t);
// Read each rule // Read each rule
for (uint16_t i = 0; i < ruleCount; ++i) { for (uint16_t i = 0; i < ruleCount; ++i) {
// Read selector string // Read selector string
uint16_t selectorLen = 0; uint16_t selectorLen = 0;
if (!hasRemainingBytes(sizeof(selectorLen))) {
rulesBySelector_.clear();
file.close();
return false;
}
if (file.read(&selectorLen, sizeof(selectorLen)) != sizeof(selectorLen)) { if (file.read(&selectorLen, sizeof(selectorLen)) != sizeof(selectorLen)) {
rulesBySelector_.clear(); rulesBySelector_.clear();
file.close(); file.close();
return false; return false;
} }
if (selectorLen == 0 || selectorLen > MAX_SELECTOR_LENGTH || !hasRemainingBytes(selectorLen)) {
LOG_DBG("CSS", "Invalid selector length in cache: %u", selectorLen);
rulesBySelector_.clear();
file.close();
return false;
}
std::string selector; std::string selector;
selector.resize(selectorLen); selector.resize(selectorLen);
if (file.read(&selector[0], selectorLen) != selectorLen) { if (file.read(&selector[0], selectorLen) != selectorLen) {
@@ -766,6 +823,13 @@ bool CssParser::loadFromCache() {
return false; return false;
} }
if (!hasRemainingBytes(CSS_FIXED_STYLE_BYTES)) {
LOG_DBG("CSS", "Truncated CSS cache while reading style payload");
rulesBySelector_.clear();
file.close();
return false;
}
// Read CssStyle fields // Read CssStyle fields
CssStyle style; CssStyle style;
uint8_t enumVal; uint8_t enumVal;
@@ -820,6 +884,15 @@ bool CssParser::loadFromCache() {
return false; return false;
} }
// Read display value
uint8_t displayVal;
if (file.read(&displayVal, 1) != 1) {
rulesBySelector_.clear();
file.close();
return false;
}
style.display = static_cast<CssDisplay>(displayVal);
// Read defined flags // Read defined flags
uint16_t definedBits = 0; uint16_t definedBits = 0;
if (file.read(&definedBits, sizeof(definedBits)) != sizeof(definedBits)) { if (file.read(&definedBits, sizeof(definedBits)) != sizeof(definedBits)) {
@@ -842,6 +915,7 @@ bool CssParser::loadFromCache() {
style.defined.paddingRight = (definedBits & 1 << 12) != 0; style.defined.paddingRight = (definedBits & 1 << 12) != 0;
style.defined.imageHeight = (definedBits & 1 << 13) != 0; style.defined.imageHeight = (definedBits & 1 << 13) != 0;
style.defined.imageWidth = (definedBits & 1 << 14) != 0; style.defined.imageWidth = (definedBits & 1 << 14) != 0;
style.defined.display = (definedBits & 1 << 15) != 0;
rulesBySelector_[selector] = style; rulesBySelector_[selector] = style;
} }
+1 -1
View File
@@ -31,7 +31,7 @@
class CssParser { class CssParser {
public: public:
// Bump when CSS cache format or rules change; section caches are invalidated when this changes // Bump when CSS cache format or rules change; section caches are invalidated when this changes
static constexpr uint8_t CSS_CACHE_VERSION = 3; static constexpr uint8_t CSS_CACHE_VERSION = 4;
explicit CssParser(std::string cachePath) : cachePath(std::move(cachePath)) {} explicit CssParser(std::string cachePath) : cachePath(std::move(cachePath)) {}
~CssParser() = default; ~CssParser() = default;
+15 -3
View File
@@ -54,6 +54,9 @@ enum class CssFontWeight : uint8_t { Normal = 0, Bold = 1 };
// Text decoration options // Text decoration options
enum class CssTextDecoration : uint8_t { None = 0, Underline = 1 }; enum class CssTextDecoration : uint8_t { None = 0, Underline = 1 };
// Display options - only None and Block are relevant for e-ink rendering
enum class CssDisplay : uint8_t { Block = 0, None = 1 };
// Bitmask for tracking which properties have been explicitly set // Bitmask for tracking which properties have been explicitly set
struct CssPropertyFlags { struct CssPropertyFlags {
uint16_t textAlign : 1; uint16_t textAlign : 1;
@@ -71,6 +74,7 @@ struct CssPropertyFlags {
uint16_t paddingRight : 1; uint16_t paddingRight : 1;
uint16_t imageHeight : 1; uint16_t imageHeight : 1;
uint16_t imageWidth : 1; uint16_t imageWidth : 1;
uint16_t display : 1;
CssPropertyFlags() CssPropertyFlags()
: textAlign(0), : textAlign(0),
@@ -87,19 +91,20 @@ struct CssPropertyFlags {
paddingLeft(0), paddingLeft(0),
paddingRight(0), paddingRight(0),
imageHeight(0), imageHeight(0),
imageWidth(0) {} imageWidth(0),
display(0) {}
[[nodiscard]] bool anySet() const { [[nodiscard]] bool anySet() const {
return textAlign || fontStyle || fontWeight || textDecoration || textIndent || marginTop || marginBottom || return textAlign || fontStyle || fontWeight || textDecoration || textIndent || marginTop || marginBottom ||
marginLeft || marginRight || paddingTop || paddingBottom || paddingLeft || paddingRight || imageHeight || marginLeft || marginRight || paddingTop || paddingBottom || paddingLeft || paddingRight || imageHeight ||
imageWidth; imageWidth || display;
} }
void clearAll() { void clearAll() {
textAlign = fontStyle = fontWeight = textDecoration = textIndent = 0; textAlign = fontStyle = fontWeight = textDecoration = textIndent = 0;
marginTop = marginBottom = marginLeft = marginRight = 0; marginTop = marginBottom = marginLeft = marginRight = 0;
paddingTop = paddingBottom = paddingLeft = paddingRight = 0; paddingTop = paddingBottom = paddingLeft = paddingRight = 0;
imageHeight = imageWidth = 0; imageHeight = imageWidth = display = 0;
} }
}; };
@@ -123,6 +128,7 @@ struct CssStyle {
CssLength paddingRight; // Padding right CssLength paddingRight; // Padding right
CssLength imageHeight; // Height for img (e.g. 2em) width derived from aspect ratio when only height set CssLength imageHeight; // Height for img (e.g. 2em) width derived from aspect ratio when only height set
CssLength imageWidth; // Width for img when both or only width set CssLength imageWidth; // Width for img when both or only width set
CssDisplay display = CssDisplay::Block; // display property (Block or None)
CssPropertyFlags defined; // Tracks which properties were explicitly set CssPropertyFlags defined; // Tracks which properties were explicitly set
@@ -189,6 +195,10 @@ struct CssStyle {
imageWidth = base.imageWidth; imageWidth = base.imageWidth;
defined.imageWidth = 1; defined.imageWidth = 1;
} }
if (base.hasDisplay()) {
display = base.display;
defined.display = 1;
}
} }
[[nodiscard]] bool hasTextAlign() const { return defined.textAlign; } [[nodiscard]] bool hasTextAlign() const { return defined.textAlign; }
@@ -206,6 +216,7 @@ struct CssStyle {
[[nodiscard]] bool hasPaddingRight() const { return defined.paddingRight; } [[nodiscard]] bool hasPaddingRight() const { return defined.paddingRight; }
[[nodiscard]] bool hasImageHeight() const { return defined.imageHeight; } [[nodiscard]] bool hasImageHeight() const { return defined.imageHeight; }
[[nodiscard]] bool hasImageWidth() const { return defined.imageWidth; } [[nodiscard]] bool hasImageWidth() const { return defined.imageWidth; }
[[nodiscard]] bool hasDisplay() const { return defined.display; }
void reset() { void reset() {
textAlign = CssTextAlign::Left; textAlign = CssTextAlign::Left;
@@ -216,6 +227,7 @@ struct CssStyle {
marginTop = marginBottom = marginLeft = marginRight = CssLength{}; marginTop = marginBottom = marginLeft = marginRight = CssLength{};
paddingTop = paddingBottom = paddingLeft = paddingRight = CssLength{}; paddingTop = paddingBottom = paddingLeft = paddingRight = CssLength{};
imageHeight = imageWidth = CssLength{}; imageHeight = imageWidth = CssLength{};
display = CssDisplay::Block;
defined.clearAll(); defined.clearAll();
} }
}; };
@@ -107,6 +107,17 @@ bool isPunctuation(const uint32_t cp) {
bool isAsciiDigit(const uint32_t cp) { return cp >= '0' && cp <= '9'; } bool isAsciiDigit(const uint32_t cp) { return cp >= '0' && cp <= '9'; }
bool isApostrophe(const uint32_t cp) {
switch (cp) {
case '\'':
case 0x2018: // left single quotation mark
case 0x2019: // right single quotation mark
return true;
default:
return false;
}
}
bool isExplicitHyphen(const uint32_t cp) { bool isExplicitHyphen(const uint32_t cp) {
switch (cp) { switch (cp) {
case '-': case '-':
@@ -19,6 +19,7 @@ bool isCyrillicLetter(uint32_t cp);
bool isAlphabetic(uint32_t cp); bool isAlphabetic(uint32_t cp);
bool isPunctuation(uint32_t cp); bool isPunctuation(uint32_t cp);
bool isAsciiDigit(uint32_t cp); bool isAsciiDigit(uint32_t cp);
bool isApostrophe(uint32_t cp);
bool isExplicitHyphen(uint32_t cp); bool isExplicitHyphen(uint32_t cp);
bool isSoftHyphen(uint32_t cp); bool isSoftHyphen(uint32_t cp);
void trimSurroundingPunctuationAndFootnote(std::vector<CodepointInfo>& cps); void trimSurroundingPunctuationAndFootnote(std::vector<CodepointInfo>& cps);
+120 -21
View File
@@ -1,6 +1,7 @@
#include "Hyphenator.h" #include "Hyphenator.h"
#include <algorithm> #include <algorithm>
#include <cassert>
#include <vector> #include <vector>
#include "HyphenationCommon.h" #include "HyphenationCommon.h"
@@ -59,6 +60,94 @@ std::vector<Hyphenator::BreakInfo> buildExplicitBreakInfos(const std::vector<Cod
return breaks; return breaks;
} }
bool isSegmentSeparator(const uint32_t cp) { return isExplicitHyphen(cp) || isApostrophe(cp); }
void appendSegmentPatternBreaks(const std::vector<CodepointInfo>& cps, const LanguageHyphenator& hyphenator,
const bool includeFallback, std::vector<Hyphenator::BreakInfo>& outBreaks) {
size_t segStart = 0;
for (size_t i = 0; i <= cps.size(); ++i) {
const bool atEnd = i == cps.size();
const bool atSeparator = !atEnd && isSegmentSeparator(cps[i].value);
if (!atEnd && !atSeparator) {
continue;
}
if (i > segStart) {
std::vector<CodepointInfo> segment(cps.begin() + segStart, cps.begin() + i);
auto segIndexes = hyphenator.breakIndexes(segment);
if (includeFallback && segIndexes.empty()) {
const size_t minPrefix = hyphenator.minPrefix();
const size_t minSuffix = hyphenator.minSuffix();
for (size_t idx = minPrefix; idx + minSuffix <= segment.size(); ++idx) {
segIndexes.push_back(idx);
}
}
for (const size_t idx : segIndexes) {
assert(idx > 0 && idx < segment.size());
if (idx == 0 || idx >= segment.size()) continue;
const size_t cpIdx = segStart + idx;
if (cpIdx < cps.size()) {
outBreaks.push_back({cps[cpIdx].byteOffset, true});
}
}
}
segStart = i + 1;
}
}
void appendApostropheContractionBreaks(const std::vector<CodepointInfo>& cps,
std::vector<Hyphenator::BreakInfo>& outBreaks) {
constexpr size_t kMinLeftSegmentLen = 3;
constexpr size_t kMinRightSegmentLen = 3;
size_t segmentStart = 0;
for (size_t i = 0; i < cps.size(); ++i) {
if (isSegmentSeparator(cps[i].value)) {
if (isApostrophe(cps[i].value) && i > 0 && i + 1 < cps.size() && isAlphabetic(cps[i - 1].value) &&
isAlphabetic(cps[i + 1].value)) {
size_t leftPrefixLen = 0;
for (size_t j = segmentStart; j < i; ++j) {
if (isAlphabetic(cps[j].value)) {
++leftPrefixLen;
}
}
size_t rightSuffixLen = 0;
for (size_t j = i + 1; j < cps.size() && !isSegmentSeparator(cps[j].value); ++j) {
if (isAlphabetic(cps[j].value)) {
++rightSuffixLen;
}
}
// Avoid stranding short clitics like "l'"/"d'" or contraction tails like "'ve"/"'re"/"'ll".
if (leftPrefixLen >= kMinLeftSegmentLen && rightSuffixLen >= kMinRightSegmentLen) {
outBreaks.push_back({cps[i + 1].byteOffset, false});
}
}
segmentStart = i + 1;
}
}
}
void sortAndDedupeBreakInfos(std::vector<Hyphenator::BreakInfo>& infos) {
std::sort(infos.begin(), infos.end(), [](const Hyphenator::BreakInfo& a, const Hyphenator::BreakInfo& b) {
if (a.byteOffset != b.byteOffset) {
return a.byteOffset < b.byteOffset;
}
return a.requiresInsertedHyphen < b.requiresInsertedHyphen;
});
infos.erase(std::unique(infos.begin(), infos.end(),
[](const Hyphenator::BreakInfo& a, const Hyphenator::BreakInfo& b) {
return a.byteOffset == b.byteOffset;
}),
infos.end());
}
} // namespace } // namespace
std::vector<Hyphenator::BreakInfo> Hyphenator::breakOffsets(const std::string& word, const bool includeFallback) { std::vector<Hyphenator::BreakInfo> Hyphenator::breakOffsets(const std::string& word, const bool includeFallback) {
@@ -71,6 +160,15 @@ std::vector<Hyphenator::BreakInfo> Hyphenator::breakOffsets(const std::string& w
trimSurroundingPunctuationAndFootnote(cps); trimSurroundingPunctuationAndFootnote(cps);
const auto* hyphenator = cachedHyphenator_; const auto* hyphenator = cachedHyphenator_;
// Detect apostrophe-like separators early; used by both branches below.
bool hasApostropheLikeSeparator = false;
for (const auto& cp : cps) {
if (isApostrophe(cp.value)) {
hasApostropheLikeSeparator = true;
break;
}
}
// Explicit hyphen markers (soft or hard) take precedence over language breaks. // Explicit hyphen markers (soft or hard) take precedence over language breaks.
auto explicitBreakInfos = buildExplicitBreakInfos(cps); auto explicitBreakInfos = buildExplicitBreakInfos(cps);
if (!explicitBreakInfos.empty()) { if (!explicitBreakInfos.empty()) {
@@ -89,31 +187,32 @@ std::vector<Hyphenator::BreakInfo> Hyphenator::breakOffsets(const std::string& w
// @16 Satellitensys|tems (+hyphen) // @16 Satellitensys|tems (+hyphen)
// Result: 6 sorted break points; the line-breaker picks the widest prefix that fits. // Result: 6 sorted break points; the line-breaker picks the widest prefix that fits.
if (hyphenator) { if (hyphenator) {
size_t segStart = 0; appendSegmentPatternBreaks(cps, *hyphenator, /*includeFallback=*/false, explicitBreakInfos);
for (size_t i = 0; i <= cps.size(); ++i) {
const bool atEnd = (i == cps.size());
const bool atHyphen = !atEnd && isExplicitHyphen(cps[i].value);
if (atEnd || atHyphen) {
if (i > segStart) {
std::vector<CodepointInfo> segment(cps.begin() + segStart, cps.begin() + i);
auto segIndexes = hyphenator->breakIndexes(segment);
for (const size_t idx : segIndexes) {
const size_t cpIdx = segStart + idx;
if (cpIdx < cps.size()) {
explicitBreakInfos.push_back({cps[cpIdx].byteOffset, true});
}
}
}
segStart = i + 1;
}
}
// Merge explicit and pattern breaks into ascending byte-offset order.
std::sort(explicitBreakInfos.begin(), explicitBreakInfos.end(),
[](const BreakInfo& a, const BreakInfo& b) { return a.byteOffset < b.byteOffset; });
} }
// Also add apostrophe contraction breaks when present (e.g. "l'état-major"
// has both an explicit hyphen and an apostrophe that can independently break).
if (hasApostropheLikeSeparator) {
appendApostropheContractionBreaks(cps, explicitBreakInfos);
}
// Merge all break points into ascending byte-offset order.
sortAndDedupeBreakInfos(explicitBreakInfos);
return explicitBreakInfos; return explicitBreakInfos;
} }
// Apostrophe-like separators split compounds into alphabetic segments; run Liang on each segment.
// This allows words like "all'improvviso" to hyphenate within "improvviso" instead of becoming
// completely unsplittable due to the apostrophe punctuation. Apostrophe contraction breaks are
// applied regardless of whether a language hyphenator is available.
if (hasApostropheLikeSeparator) {
std::vector<BreakInfo> segmentedBreaks;
if (hyphenator) {
appendSegmentPatternBreaks(cps, *hyphenator, includeFallback, segmentedBreaks);
}
appendApostropheContractionBreaks(cps, segmentedBreaks);
sortAndDedupeBreakInfos(segmentedBreaks);
return segmentedBreaks;
}
// Ask language hyphenator for legal break points. // Ask language hyphenator for legal break points.
std::vector<size_t> indexes; std::vector<size_t> indexes;
if (hyphenator) { if (hyphenator) {
+10 -4
View File
@@ -11,7 +11,8 @@ class Hyphenator {
struct BreakInfo { struct BreakInfo {
size_t byteOffset; // Byte position inside the UTF-8 word where a break may occur. size_t byteOffset; // Byte position inside the UTF-8 word where a break may occur.
bool requiresInsertedHyphen; // true = a visible '-' must be rendered at the break (pattern/fallback breaks). bool requiresInsertedHyphen; // true = a visible '-' must be rendered at the break (pattern/fallback breaks).
// false = the word already contains a hyphen at this position (explicit '-'). // false = break occurs at an existing visible separator boundary
// (explicit '-' or eligible apostrophe contraction boundary).
}; };
// Returns byte offsets where the word may be hyphenated. // Returns byte offsets where the word may be hyphenated.
@@ -19,12 +20,17 @@ class Hyphenator {
// Break sources (in priority order): // Break sources (in priority order):
// 1. Explicit hyphens already present in the word (e.g. '-' or soft-hyphen U+00AD). // 1. Explicit hyphens already present in the word (e.g. '-' or soft-hyphen U+00AD).
// When found, language patterns are additionally run on each alphabetic segment // When found, language patterns are additionally run on each alphabetic segment
// between hyphens so compound words can break within their parts. // between separators so compound words can break within their parts.
// Example: "US-Satellitensystems" yields breaks after "US-" (no inserted hyphen) // Example: "US-Satellitensystems" yields breaks after "US-" (no inserted hyphen)
// plus pattern breaks inside "Satellitensystems" (Sa|tel|li|ten|sys|tems). // plus pattern breaks inside "Satellitensystems" (Sa|tel|li|ten|sys|tems).
// 2. Language-specific Liang patterns (e.g. German de_patterns). // 2. Apostrophe contractions between letters (e.g. all'improvviso).
// Liang patterns are run per alphabetic segment around apostrophes.
// A direct break at the apostrophe boundary is allowed only when the left
// segment has at least 3 letters and the right segment has at least 3 letters,
// avoiding short clitics (e.g. l', d') and contraction tails (e.g. 've, 're, 'll).
// 3. Language-specific Liang patterns (e.g. German de_patterns).
// Example: "Quadratkilometer" -> Qua|drat|ki|lo|me|ter. // Example: "Quadratkilometer" -> Qua|drat|ki|lo|me|ter.
// 3. Fallback every-N-chars splitting (only when includeFallback is true AND no // 4. Fallback every-N-chars splitting (only when includeFallback is true AND no
// pattern breaks were found). Used as a last resort to prevent a single oversized // pattern breaks were found). Used as a last resort to prevent a single oversized
// word from overflowing the page width. // word from overflowing the page width.
static std::vector<BreakInfo> breakOffsets(const std::string& word, bool includeFallback); static std::vector<BreakInfo> breakOffsets(const std::string& word, bool includeFallback);
+105 -20
View File
@@ -4,6 +4,7 @@
#include <GfxRenderer.h> #include <GfxRenderer.h>
#include <HalStorage.h> #include <HalStorage.h>
#include <Logging.h> #include <Logging.h>
#include <Utf8.h>
#include <expat.h> #include <expat.h>
#include "../../Epub.h" #include "../../Epub.h"
@@ -133,11 +134,21 @@ void ChapterHtmlSlimParser::startNewTextBlock(const BlockStyle& blockStyle) {
// This handles cases like <div style="margin-bottom:2em"><h1>text</h1></div> where the // This handles cases like <div style="margin-bottom:2em"><h1>text</h1></div> where the
// div's margin should be preserved, even though it has no direct text content. // div's margin should be preserved, even though it has no direct text content.
currentTextBlock->setBlockStyle(currentTextBlock->getBlockStyle().getCombinedBlockStyle(blockStyle)); currentTextBlock->setBlockStyle(currentTextBlock->getBlockStyle().getCombinedBlockStyle(blockStyle));
if (!pendingAnchorId.empty()) {
anchorData.push_back({std::move(pendingAnchorId), static_cast<uint16_t>(completedPageCount)});
pendingAnchorId.clear();
}
return; return;
} }
makePages(); makePages();
} }
// Record deferred anchor after previous block is flushed
if (!pendingAnchorId.empty()) {
anchorData.push_back({std::move(pendingAnchorId), static_cast<uint16_t>(completedPageCount)});
pendingAnchorId.clear();
}
currentTextBlock.reset(new ParsedText(extraParagraphSpacing, hyphenationEnabled, blockStyle)); currentTextBlock.reset(new ParsedText(extraParagraphSpacing, hyphenationEnabled, blockStyle));
wordsExtractedInBlock = 0; wordsExtractedInBlock = 0;
} }
@@ -151,7 +162,7 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char*
return; return;
} }
// Extract class and style attributes for CSS processing // Extract class, style, and id attributes
std::string classAttr; std::string classAttr;
std::string styleAttr; std::string styleAttr;
if (atts != nullptr) { if (atts != nullptr) {
@@ -160,6 +171,9 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char*
classAttr = atts[i + 1]; classAttr = atts[i + 1];
} else if (strcmp(atts[i], "style") == 0) { } else if (strcmp(atts[i], "style") == 0) {
styleAttr = atts[i + 1]; styleAttr = atts[i + 1];
} else if (strcmp(atts[i], "id") == 0) {
// Defer recording until startNewTextBlock, after previous block is flushed to pages
self->pendingAnchorId = atts[i + 1];
} }
} }
} }
@@ -168,6 +182,24 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char*
centeredBlockStyle.textAlignDefined = true; centeredBlockStyle.textAlignDefined = true;
centeredBlockStyle.alignment = CssTextAlign::Center; centeredBlockStyle.alignment = CssTextAlign::Center;
// Compute CSS style for this element early so display:none can short-circuit
// before tag-specific branches emit any content or metadata.
CssStyle cssStyle;
if (self->cssParser) {
cssStyle = self->cssParser->resolveStyle(name, classAttr);
if (!styleAttr.empty()) {
CssStyle inlineStyle = CssParser::parseInlineStyle(styleAttr);
cssStyle.applyOver(inlineStyle);
}
}
// Skip elements with display:none before all fast paths (tables, links, etc.).
if (cssStyle.hasDisplay() && cssStyle.display == CssDisplay::None) {
self->skipUntilDepth = self->depth;
self->depth += 1;
return;
}
// Special handling for tables/cells: flatten into per-cell paragraphs with a prefixed header. // Special handling for tables/cells: flatten into per-cell paragraphs with a prefixed header.
if (strcmp(name, "table") == 0) { if (strcmp(name, "table") == 0) {
// skip nested tables // skip nested tables
@@ -243,7 +275,27 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char*
} }
} }
if (!src.empty()) { // imageRendering: 0=display, 1=placeholder (alt text only), 2=suppress entirely
if (self->imageRendering == 2) {
self->skipUntilDepth = self->depth;
self->depth += 1;
return;
}
// Skip image if CSS display:none
if (self->cssParser) {
CssStyle imgDisplayStyle = self->cssParser->resolveStyle("img", classAttr);
if (!styleAttr.empty()) {
imgDisplayStyle.applyOver(CssParser::parseInlineStyle(styleAttr));
}
if (imgDisplayStyle.hasDisplay() && imgDisplayStyle.display == CssDisplay::None) {
self->skipUntilDepth = self->depth;
self->depth += 1;
return;
}
}
if (!src.empty() && self->imageRendering != 1) {
LOG_DBG("EHP", "Found image: src=%s", src.c_str()); LOG_DBG("EHP", "Found image: src=%s", src.c_str());
{ {
@@ -278,8 +330,7 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char*
int displayWidth = 0; int displayWidth = 0;
int displayHeight = 0; int displayHeight = 0;
const float emSize = const float emSize = static_cast<float>(self->renderer.getFontAscenderSize(self->fontId));
static_cast<float>(self->renderer.getLineHeight(self->fontId)) * self->lineCompression;
CssStyle imgStyle = self->cssParser ? self->cssParser->resolveStyle("img", classAttr) : CssStyle{}; CssStyle imgStyle = self->cssParser ? self->cssParser->resolveStyle("img", classAttr) : CssStyle{};
// Merge inline style (e.g. style="height: 2em") so it overrides stylesheet rules // Merge inline style (e.g. style="height: 2em") so it overrides stylesheet rules
if (!styleAttr.empty()) { if (!styleAttr.empty()) {
@@ -364,10 +415,20 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char*
LOG_DBG("EHP", "Display size: %dx%d (scale %.2f)", displayWidth, displayHeight, scale); LOG_DBG("EHP", "Display size: %dx%d (scale %.2f)", displayWidth, displayHeight, scale);
} }
// Flush any pending text block so it appears before the image
if (self->partWordBufferIndex > 0) {
self->flushPartWordBuffer();
}
if (self->currentTextBlock && !self->currentTextBlock->isEmpty()) {
const BlockStyle parentBlockStyle = self->currentTextBlock->getBlockStyle();
self->startNewTextBlock(parentBlockStyle);
}
// Create page for image - only break if image won't fit remaining space // Create page for image - only break if image won't fit remaining space
if (self->currentPage && !self->currentPage->elements.empty() && if (self->currentPage && !self->currentPage->elements.empty() &&
(self->currentPageNextY + displayHeight > self->viewportHeight)) { (self->currentPageNextY + displayHeight > self->viewportHeight)) {
self->completePageFn(std::move(self->currentPage)); self->completePageFn(std::move(self->currentPage));
self->completedPageCount++;
self->currentPage.reset(new Page()); self->currentPage.reset(new Page());
if (!self->currentPage) { if (!self->currentPage) {
LOG_ERR("EHP", "Failed to create new page"); LOG_ERR("EHP", "Failed to create new page");
@@ -493,19 +554,7 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char*
} }
} }
// Compute CSS style for this element const float emSize = static_cast<float>(self->renderer.getFontAscenderSize(self->fontId));
CssStyle cssStyle;
if (self->cssParser) {
// Get combined tag + class styles
cssStyle = self->cssParser->resolveStyle(name, classAttr);
// Merge inline style (highest priority)
if (!styleAttr.empty()) {
CssStyle inlineStyle = CssParser::parseInlineStyle(styleAttr);
cssStyle.applyOver(inlineStyle);
}
}
const float emSize = static_cast<float>(self->renderer.getLineHeight(self->fontId)) * self->lineCompression;
const auto userAlignmentBlockStyle = BlockStyle::fromCssStyle( const auto userAlignmentBlockStyle = BlockStyle::fromCssStyle(
cssStyle, emSize, static_cast<CssTextAlign>(self->paragraphAlignment), self->viewportWidth); cssStyle, emSize, static_cast<CssTextAlign>(self->paragraphAlignment), self->viewportWidth);
@@ -738,9 +787,30 @@ void XMLCALL ChapterHtmlSlimParser::characterData(void* userData, const XML_Char
} }
} }
// If we're about to run out of space, then cut the word off and start a new one // If we're about to run out of space, then cut the word off and start a new one.
// For CJK text (no spaces), this is the primary word-breaking mechanism.
// We must avoid splitting multi-byte UTF-8 sequences across word boundaries,
// otherwise the trailing bytes become orphaned continuation bytes that the
// decoder can't interpret.
if (self->partWordBufferIndex >= MAX_WORD_SIZE) { if (self->partWordBufferIndex >= MAX_WORD_SIZE) {
self->flushPartWordBuffer(); int safeLen = utf8SafeTruncateBuffer(self->partWordBuffer, self->partWordBufferIndex);
if (safeLen < self->partWordBufferIndex && safeLen > 0) {
// Incomplete UTF-8 sequence at the end — save it before flushing
int overflow = self->partWordBufferIndex - safeLen;
char saved[4];
for (int j = 0; j < overflow; j++) {
saved[j] = self->partWordBuffer[safeLen + j];
}
self->partWordBufferIndex = safeLen;
self->flushPartWordBuffer();
for (int j = 0; j < overflow; j++) {
self->partWordBuffer[j] = saved[j];
}
self->partWordBufferIndex = overflow;
} else {
self->flushPartWordBuffer();
}
} }
self->partWordBuffer[self->partWordBufferIndex++] = s[i]; self->partWordBuffer[self->partWordBufferIndex++] = s[i];
@@ -752,8 +822,12 @@ void XMLCALL ChapterHtmlSlimParser::characterData(void* userData, const XML_Char
// Spotted when reading Intermezzo, there are some really long text blocks in there. // Spotted when reading Intermezzo, there are some really long text blocks in there.
if (self->currentTextBlock->size() > 750) { if (self->currentTextBlock->size() > 750) {
LOG_DBG("EHP", "Text block too long, splitting into multiple pages"); LOG_DBG("EHP", "Text block too long, splitting into multiple pages");
const int horizontalInset = self->currentTextBlock->getBlockStyle().totalHorizontalInset();
const uint16_t effectiveWidth = (horizontalInset < self->viewportWidth)
? static_cast<uint16_t>(self->viewportWidth - horizontalInset)
: self->viewportWidth;
self->currentTextBlock->layoutAndExtractLines( self->currentTextBlock->layoutAndExtractLines(
self->renderer, self->fontId, self->viewportWidth, self->renderer, self->fontId, effectiveWidth,
[self](const std::shared_ptr<TextBlock>& textBlock) { self->addLineToPage(textBlock); }, false); [self](const std::shared_ptr<TextBlock>& textBlock) { self->addLineToPage(textBlock); }, false);
} }
} }
@@ -984,7 +1058,12 @@ bool ChapterHtmlSlimParser::parseAndBuildPages() {
// Process last page if there is still text // Process last page if there is still text
if (currentTextBlock) { if (currentTextBlock) {
makePages(); makePages();
if (!pendingAnchorId.empty()) {
anchorData.push_back({std::move(pendingAnchorId), static_cast<uint16_t>(completedPageCount)});
pendingAnchorId.clear();
}
completePageFn(std::move(currentPage)); completePageFn(std::move(currentPage));
completedPageCount++;
currentPage.reset(); currentPage.reset();
currentTextBlock.reset(); currentTextBlock.reset();
} }
@@ -995,8 +1074,14 @@ bool ChapterHtmlSlimParser::parseAndBuildPages() {
void ChapterHtmlSlimParser::addLineToPage(std::shared_ptr<TextBlock> line) { void ChapterHtmlSlimParser::addLineToPage(std::shared_ptr<TextBlock> line) {
const int lineHeight = renderer.getLineHeight(fontId) * lineCompression; const int lineHeight = renderer.getLineHeight(fontId) * lineCompression;
if (!currentPage) {
currentPage.reset(new Page());
currentPageNextY = 0;
}
if (currentPageNextY + lineHeight > viewportHeight) { if (currentPageNextY + lineHeight > viewportHeight) {
completePageFn(std::move(currentPage)); completePageFn(std::move(currentPage));
completedPageCount++;
currentPage.reset(new Page()); currentPage.reset(new Page());
currentPageNextY = 0; currentPageNextY = 0;
} }
+11 -2
View File
@@ -5,6 +5,7 @@
#include <climits> #include <climits>
#include <functional> #include <functional>
#include <memory> #include <memory>
#include <string>
#include <vector> #include <vector>
#include "../FootnoteEntry.h" #include "../FootnoteEntry.h"
@@ -48,6 +49,7 @@ class ChapterHtmlSlimParser {
bool hyphenationEnabled; bool hyphenationEnabled;
const CssParser* cssParser; const CssParser* cssParser;
bool embeddedStyle; bool embeddedStyle;
uint8_t imageRendering;
std::string contentBase; std::string contentBase;
std::string imageBasePath; std::string imageBasePath;
int imageCounter = 0; int imageCounter = 0;
@@ -68,6 +70,11 @@ class ChapterHtmlSlimParser {
int tableRowIndex = 0; int tableRowIndex = 0;
int tableColIndex = 0; int tableColIndex = 0;
// Anchor-to-page mapping: tracks which page each HTML id attribute lands on
int completedPageCount = 0;
std::vector<std::pair<std::string, uint16_t>> anchorData;
std::string pendingAnchorId; // deferred until after previous text block is flushed
// Footnote link tracking // Footnote link tracking
bool insideFootnoteLink = false; bool insideFootnoteLink = false;
int footnoteLinkDepth = -1; int footnoteLinkDepth = -1;
@@ -94,8 +101,8 @@ class ChapterHtmlSlimParser {
const uint16_t viewportHeight, const bool hyphenationEnabled, const uint16_t viewportHeight, const bool hyphenationEnabled,
const std::function<void(std::unique_ptr<Page>)>& completePageFn, const std::function<void(std::unique_ptr<Page>)>& completePageFn,
const bool embeddedStyle, const std::string& contentBase, const bool embeddedStyle, const std::string& contentBase,
const std::string& imageBasePath, const std::function<void()>& popupFn = nullptr, const std::string& imageBasePath, const uint8_t imageRendering = 0,
const CssParser* cssParser = nullptr) const std::function<void()>& popupFn = nullptr, const CssParser* cssParser = nullptr)
: epub(epub), : epub(epub),
filepath(filepath), filepath(filepath),
@@ -111,10 +118,12 @@ class ChapterHtmlSlimParser {
popupFn(popupFn), popupFn(popupFn),
cssParser(cssParser), cssParser(cssParser),
embeddedStyle(embeddedStyle), embeddedStyle(embeddedStyle),
imageRendering(imageRendering),
contentBase(contentBase), contentBase(contentBase),
imageBasePath(imageBasePath) {} imageBasePath(imageBasePath) {}
~ChapterHtmlSlimParser() = default; ~ChapterHtmlSlimParser() = default;
bool parseAndBuildPages(); bool parseAndBuildPages();
void addLineToPage(std::shared_ptr<TextBlock> line); void addLineToPage(std::shared_ptr<TextBlock> line);
const std::vector<std::pair<std::string, uint16_t>>& getAnchors() const { return anchorData; }
}; };
+3 -5
View File
@@ -36,12 +36,10 @@ ContentOpfParser::~ContentOpfParser() {
if (tempItemStore) { if (tempItemStore) {
tempItemStore.close(); tempItemStore.close();
} }
if (Storage.exists((cachePath + itemCacheFile).c_str())) { const auto itemCachePath = cachePath + itemCacheFile;
Storage.remove((cachePath + itemCacheFile).c_str()); if (Storage.exists(itemCachePath.c_str())) {
Storage.remove(itemCachePath.c_str());
} }
itemIndex.clear();
itemIndex.shrink_to_fit();
useItemIndex = false;
} }
size_t ContentOpfParser::write(const uint8_t data) { return write(&data, 1); } size_t ContentOpfParser::write(const uint8_t data) { return write(&data, 1); }
+43 -1
View File
@@ -1,8 +1,12 @@
#include "FsHelpers.h" #include "FsHelpers.h"
#include <cctype>
#include <cstring>
#include <vector> #include <vector>
std::string FsHelpers::normalisePath(const std::string& path) { namespace FsHelpers {
std::string normalisePath(const std::string& path) {
std::vector<std::string> components; std::vector<std::string> components;
std::string component; std::string component;
@@ -37,3 +41,41 @@ std::string FsHelpers::normalisePath(const std::string& path) {
return result; return result;
} }
bool checkFileExtension(std::string_view fileName, const char* extension) {
const size_t extLen = strlen(extension);
if (fileName.length() < extLen) {
return false;
}
const size_t offset = fileName.length() - extLen;
for (size_t i = 0; i < extLen; i++) {
if (tolower(static_cast<unsigned char>(fileName[offset + i])) !=
tolower(static_cast<unsigned char>(extension[i]))) {
return false;
}
}
return true;
}
bool hasJpgExtension(std::string_view fileName) {
return checkFileExtension(fileName, ".jpg") || checkFileExtension(fileName, ".jpeg");
}
bool hasPngExtension(std::string_view fileName) { return checkFileExtension(fileName, ".png"); }
bool hasBmpExtension(std::string_view fileName) { return checkFileExtension(fileName, ".bmp"); }
bool hasGifExtension(std::string_view fileName) { return checkFileExtension(fileName, ".gif"); }
bool hasEpubExtension(std::string_view fileName) { return checkFileExtension(fileName, ".epub"); }
bool hasXtcExtension(std::string_view fileName) {
return checkFileExtension(fileName, ".xtc") || checkFileExtension(fileName, ".xtch");
}
bool hasTxtExtension(std::string_view fileName) { return checkFileExtension(fileName, ".txt"); }
bool hasMarkdownExtension(std::string_view fileName) { return checkFileExtension(fileName, ".md"); }
} // namespace FsHelpers
+56 -5
View File
@@ -1,7 +1,58 @@
#pragma once #pragma once
#include <string> #include <WString.h>
class FsHelpers { #include <string>
public: #include <string_view>
static std::string normalisePath(const std::string& path);
}; namespace FsHelpers {
std::string normalisePath(const std::string& path);
/**
* Check if the given filename ends with the specified extension (case-insensitive).
*/
bool checkFileExtension(std::string_view fileName, const char* extension);
inline bool checkFileExtension(const String& fileName, const char* extension) {
return checkFileExtension(std::string_view{fileName.c_str(), fileName.length()}, extension);
}
// Check for either .jpg or .jpeg extension (case-insensitive)
bool hasJpgExtension(std::string_view fileName);
inline bool hasJpgExtension(const String& fileName) {
return hasJpgExtension(std::string_view{fileName.c_str(), fileName.length()});
}
// Check for .png extension (case-insensitive)
bool hasPngExtension(std::string_view fileName);
inline bool hasPngExtension(const String& fileName) {
return hasPngExtension(std::string_view{fileName.c_str(), fileName.length()});
}
// Check for .bmp extension (case-insensitive)
bool hasBmpExtension(std::string_view fileName);
// Check for .gif extension (case-insensitive)
bool hasGifExtension(std::string_view fileName);
inline bool hasGifExtension(const String& fileName) {
return hasGifExtension(std::string_view{fileName.c_str(), fileName.length()});
}
// Check for .epub extension (case-insensitive)
bool hasEpubExtension(std::string_view fileName);
inline bool hasEpubExtension(const String& fileName) {
return hasEpubExtension(std::string_view{fileName.c_str(), fileName.length()});
}
// Check for either .xtc or .xtch extension (case-insensitive)
bool hasXtcExtension(std::string_view fileName);
// Check for .txt extension (case-insensitive)
bool hasTxtExtension(std::string_view fileName);
inline bool hasTxtExtension(const String& fileName) {
return hasTxtExtension(std::string_view{fileName.c_str(), fileName.length()});
}
// Check for .md extension (case-insensitive)
bool hasMarkdownExtension(std::string_view fileName);
} // namespace FsHelpers
+96
View File
@@ -0,0 +1,96 @@
#include "FontCacheManager.h"
#include <FontDecompressor.h>
#include <Logging.h>
#include <cstring>
FontCacheManager::FontCacheManager(const std::map<int, EpdFontFamily>& fontMap) : fontMap_(fontMap) {}
void FontCacheManager::setFontDecompressor(FontDecompressor* d) { fontDecompressor_ = d; }
void FontCacheManager::clearCache() {
if (fontDecompressor_) fontDecompressor_->clearCache();
}
void FontCacheManager::prewarmCache(int fontId, const char* utf8Text, uint8_t styleMask) {
if (!fontDecompressor_ || fontMap_.count(fontId) == 0) return;
for (uint8_t i = 0; i < 4; i++) {
if (!(styleMask & (1 << i))) continue;
auto style = static_cast<EpdFontFamily::Style>(i);
const EpdFontData* data = fontMap_.at(fontId).getData(style);
if (!data || !data->groups) continue;
int missed = fontDecompressor_->prewarmCache(data, utf8Text);
if (missed > 0) {
LOG_DBG("FCM", "prewarmCache: %d glyph(s) not cached for style %d", missed, i);
}
}
}
void FontCacheManager::logStats(const char* label) {
if (fontDecompressor_) fontDecompressor_->logStats(label);
}
void FontCacheManager::resetStats() {
if (fontDecompressor_) fontDecompressor_->resetStats();
}
bool FontCacheManager::isScanning() const { return scanMode_ == ScanMode::Scanning; }
void FontCacheManager::recordText(const char* text, int fontId, EpdFontFamily::Style style) {
scanText_ += text;
if (scanFontId_ < 0) scanFontId_ = fontId;
const uint8_t baseStyle = static_cast<uint8_t>(style) & 0x03;
const unsigned char* p = reinterpret_cast<const unsigned char*>(text);
uint32_t cpCount = 0;
while (*p) {
if ((*p & 0xC0) != 0x80) cpCount++;
p++;
}
scanStyleCounts_[baseStyle] += cpCount;
}
// --- PrewarmScope implementation ---
FontCacheManager::PrewarmScope::PrewarmScope(FontCacheManager& manager) : manager_(&manager) {
manager_->scanMode_ = ScanMode::Scanning;
manager_->clearCache();
manager_->resetStats();
manager_->scanText_.clear();
manager_->scanText_.reserve(2048); // Pre-allocate to avoid heap fragmentation from repeated concat
memset(manager_->scanStyleCounts_, 0, sizeof(manager_->scanStyleCounts_));
manager_->scanFontId_ = -1;
}
void FontCacheManager::PrewarmScope::endScanAndPrewarm() {
manager_->scanMode_ = ScanMode::None;
if (manager_->scanText_.empty()) return;
// Build style bitmask from all styles that appeared during the scan
uint8_t styleMask = 0;
for (uint8_t i = 0; i < 4; i++) {
if (manager_->scanStyleCounts_[i] > 0) styleMask |= (1 << i);
}
if (styleMask == 0) styleMask = 1; // default to regular
manager_->prewarmCache(manager_->scanFontId_, manager_->scanText_.c_str(), styleMask);
// Free scan string memory
manager_->scanText_.clear();
manager_->scanText_.shrink_to_fit();
}
FontCacheManager::PrewarmScope::~PrewarmScope() {
if (active_) {
endScanAndPrewarm(); // no-op if already called (scanText_ is empty)
manager_->clearCache();
}
}
FontCacheManager::PrewarmScope::PrewarmScope(PrewarmScope&& other) noexcept
: manager_(other.manager_), active_(other.active_) {
other.active_ = false;
}
FontCacheManager::PrewarmScope FontCacheManager::createPrewarmScope() { return PrewarmScope(*this); }
+55
View File
@@ -0,0 +1,55 @@
#pragma once
#include <EpdFontFamily.h>
#include <cstdint>
#include <map>
#include <string>
class FontDecompressor;
class FontCacheManager {
public:
explicit FontCacheManager(const std::map<int, EpdFontFamily>& fontMap);
void setFontDecompressor(FontDecompressor* d);
void clearCache();
void prewarmCache(int fontId, const char* utf8Text, uint8_t styleMask = 0x0F);
void logStats(const char* label = "render");
void resetStats();
// Scan-mode API: called by GfxRenderer::drawText() during scan pass
bool isScanning() const;
void recordText(const char* text, int fontId, EpdFontFamily::Style style);
// The FontDecompressor pointer, needed by GfxRenderer::getGlyphBitmap()
FontDecompressor* getDecompressor() const { return fontDecompressor_; }
// RAII scope for two-pass prewarm pattern
class PrewarmScope {
public:
explicit PrewarmScope(FontCacheManager& manager);
~PrewarmScope();
void endScanAndPrewarm();
PrewarmScope(PrewarmScope&& other) noexcept;
PrewarmScope& operator=(PrewarmScope&&) = delete;
PrewarmScope(const PrewarmScope&) = delete;
PrewarmScope& operator=(const PrewarmScope&) = delete;
private:
FontCacheManager* manager_;
bool active_ = true;
};
PrewarmScope createPrewarmScope();
private:
const std::map<int, EpdFontFamily>& fontMap_;
FontDecompressor* fontDecompressor_ = nullptr;
enum class ScanMode : uint8_t { None, Scanning };
ScanMode scanMode_ = ScanMode::None;
std::string scanText_;
uint32_t scanStyleCounts_[4] = {};
int scanFontId_ = -1;
};
+64 -54
View File
@@ -1,16 +1,23 @@
#include "GfxRenderer.h" #include "GfxRenderer.h"
#include <FontDecompressor.h>
#include <Logging.h> #include <Logging.h>
#include <Utf8.h> #include <Utf8.h>
#include "FontCacheManager.h"
const uint8_t* GfxRenderer::getGlyphBitmap(const EpdFontData* fontData, const EpdGlyph* glyph) const { const uint8_t* GfxRenderer::getGlyphBitmap(const EpdFontData* fontData, const EpdGlyph* glyph) const {
if (fontData->groups != nullptr) { if (fontData->groups != nullptr) {
if (!fontDecompressor) { auto* fd = fontCacheManager_ ? fontCacheManager_->getDecompressor() : nullptr;
if (!fd) {
LOG_ERR("GFX", "Compressed font but no FontDecompressor set"); LOG_ERR("GFX", "Compressed font but no FontDecompressor set");
return nullptr; return nullptr;
} }
uint16_t glyphIndex = static_cast<uint16_t>(glyph - fontData->glyph); uint32_t glyphIndex = static_cast<uint32_t>(glyph - fontData->glyph);
return fontDecompressor->getBitmap(fontData, glyph, glyphIndex); // For page-buffer hits the pointer is stable for the page lifetime.
// For hot-group hits it is valid only until the next getBitmap() call — callers
// must consume it (draw the glyph) before requesting another bitmap.
return fd->getBitmap(fontData, glyph, glyphIndex);
} }
return &fontData->bitmap[glyph->dataOffset]; return &fontData->bitmap[glyph->dataOffset];
} }
@@ -65,7 +72,7 @@ enum class TextRotation { None, Rotated90CW };
// Coordinate mapping and cursor advance direction are selected at compile time via the template parameter. // Coordinate mapping and cursor advance direction are selected at compile time via the template parameter.
template <TextRotation rotation> template <TextRotation rotation>
static void renderCharImpl(const GfxRenderer& renderer, GfxRenderer::RenderMode renderMode, static void renderCharImpl(const GfxRenderer& renderer, GfxRenderer::RenderMode renderMode,
const EpdFontFamily& fontFamily, const uint32_t cp, int* cursorX, int* cursorY, const EpdFontFamily& fontFamily, const uint32_t cp, int cursorX, int cursorY,
const bool pixelState, const EpdFontFamily::Style style) { const bool pixelState, const EpdFontFamily::Style style) {
const EpdGlyph* glyph = fontFamily.getGlyph(cp, style); const EpdGlyph* glyph = fontFamily.getGlyph(cp, style);
if (!glyph) { if (!glyph) {
@@ -87,11 +94,11 @@ static void renderCharImpl(const GfxRenderer& renderer, GfxRenderer::RenderMode
// For Rotated: outer loop advances screenX, inner loop advances screenY (in reverse) // For Rotated: outer loop advances screenX, inner loop advances screenY (in reverse)
int outerBase, innerBase; int outerBase, innerBase;
if constexpr (rotation == TextRotation::Rotated90CW) { if constexpr (rotation == TextRotation::Rotated90CW) {
outerBase = *cursorX + fontData->ascender - top; // screenX = outerBase + glyphY outerBase = cursorX + fontData->ascender - top; // screenX = outerBase + glyphY
innerBase = *cursorY - left; // screenY = innerBase - glyphX innerBase = cursorY - left; // screenY = innerBase - glyphX
} else { } else {
outerBase = *cursorY - top; // screenY = outerBase + glyphY outerBase = cursorY - top; // screenY = outerBase + glyphY
innerBase = *cursorX + left; // screenX = innerBase + glyphX innerBase = cursorX + left; // screenX = innerBase + glyphX
} }
if (is2Bit) { if (is2Bit) {
@@ -152,12 +159,6 @@ static void renderCharImpl(const GfxRenderer& renderer, GfxRenderer::RenderMode
} }
} }
} }
if constexpr (rotation == TextRotation::Rotated90CW) {
*cursorY -= glyph->advanceX;
} else {
*cursorX += glyph->advanceX;
}
} }
// IMPORTANT: This function is in critical rendering path and is called for every pixel. Please keep it as simple and // IMPORTANT: This function is in critical rendering path and is called for every pixel. Please keep it as simple and
@@ -206,11 +207,10 @@ void GfxRenderer::drawCenteredText(const int fontId, const int y, const char* te
void GfxRenderer::drawText(const int fontId, const int x, const int y, const char* text, const bool black, void GfxRenderer::drawText(const int fontId, const int x, const int y, const char* text, const bool black,
const EpdFontFamily::Style style) const { const EpdFontFamily::Style style) const {
int yPos = y + getFontAscenderSize(fontId); const int yPos = y + getFontAscenderSize(fontId);
int xPos = x; int32_t xPosFP = fp4::fromPixel(x); // 12.4 fixed-point accumulator
int lastBaseX = x; int lastBaseX = x;
int lastBaseY = yPos; int lastBaseAdvanceFP = 0; // 12.4 fixed-point
int lastBaseAdvance = 0;
int lastBaseTop = 0; int lastBaseTop = 0;
// cannot draw a NULL / empty string // cannot draw a NULL / empty string
@@ -218,6 +218,11 @@ void GfxRenderer::drawText(const int fontId, const int x, const int y, const cha
return; return;
} }
if (fontCacheManager_ && fontCacheManager_->isScanning()) {
fontCacheManager_->recordText(text, fontId, style);
return;
}
const auto fontIt = fontMap.find(fontId); const auto fontIt = fontMap.find(fontId);
if (fontIt == fontMap.end()) { if (fontIt == fontMap.end()) {
LOG_ERR("GFX", "Font %d not found", fontId); LOG_ERR("GFX", "Font %d not found", fontId);
@@ -239,30 +244,32 @@ void GfxRenderer::drawText(const int fontId, const int x, const int y, const cha
} }
} }
int combiningX = lastBaseX + lastBaseAdvance / 2; const int combiningX = lastBaseX + fp4::toPixel(lastBaseAdvanceFP / 2);
int combiningY = lastBaseY - raiseBy; const int combiningY = yPos - raiseBy;
renderChar(font, cp, &combiningX, &combiningY, black, style); renderCharImpl<TextRotation::None>(*this, renderMode, font, cp, combiningX, combiningY, black, style);
continue; continue;
} }
cp = font.applyLigatures(cp, text, style); cp = font.applyLigatures(cp, text, style);
if (prevCp != 0) { const int kernFP = (prevCp != 0) ? font.getKerning(prevCp, cp, style) : 0; // 4.4 fixed-point kern
xPos += font.getKerning(prevCp, cp, style); xPosFP += kernFP;
}
lastBaseX = fp4::toPixel(xPosFP); // snap 12.4 fixed-point to nearest pixel
const EpdGlyph* glyph = font.getGlyph(cp, style); const EpdGlyph* glyph = font.getGlyph(cp, style);
lastBaseX = xPos; lastBaseAdvanceFP = glyph ? glyph->advanceX : 0;
lastBaseY = yPos;
lastBaseAdvance = glyph ? glyph->advanceX : 0;
lastBaseTop = glyph ? glyph->top : 0; lastBaseTop = glyph ? glyph->top : 0;
renderChar(font, cp, &xPos, &yPos, black, style); renderCharImpl<TextRotation::None>(*this, renderMode, font, cp, lastBaseX, yPos, black, style);
if (glyph) {
xPosFP += glyph->advanceX; // 12.4 fixed-point advance
}
prevCp = cp; prevCp = cp;
} }
} }
void GfxRenderer::drawLine(int x1, int y1, int x2, int y2, const bool state) const { void GfxRenderer::drawLine(int x1, int y1, int x2, int y2, const bool state) const {
if (fontCacheManager_ && fontCacheManager_->isScanning()) return;
if (x1 == x2) { if (x1 == x2) {
if (y2 < y1) { if (y2 < y1) {
std::swap(y1, y2); std::swap(y1, y2);
@@ -575,6 +582,7 @@ void GfxRenderer::drawIcon(const uint8_t bitmap[], const int x, const int y, con
void GfxRenderer::drawBitmap(const Bitmap& bitmap, const int x, const int y, const int maxWidth, const int maxHeight, void GfxRenderer::drawBitmap(const Bitmap& bitmap, const int x, const int y, const int maxWidth, const int maxHeight,
const float cropX, const float cropY) const { const float cropX, const float cropY) const {
if (fontCacheManager_ && fontCacheManager_->isScanning()) return;
// For 1-bit bitmaps, use optimized 1-bit rendering path (no crop support for 1-bit) // For 1-bit bitmaps, use optimized 1-bit rendering path (no crop support for 1-bit)
if (bitmap.is1Bit() && cropX == 0.0f && cropY == 0.0f) { if (bitmap.is1Bit() && cropX == 0.0f && cropY == 0.0f) {
drawBitmap1Bit(bitmap, x, y, maxWidth, maxHeight); drawBitmap1Bit(bitmap, x, y, maxWidth, maxHeight);
@@ -946,22 +954,29 @@ int GfxRenderer::getSpaceWidth(const int fontId, const EpdFontFamily::Style styl
} }
const EpdGlyph* spaceGlyph = fontIt->second.getGlyph(' ', style); const EpdGlyph* spaceGlyph = fontIt->second.getGlyph(' ', style);
return spaceGlyph ? spaceGlyph->advanceX : 0; return spaceGlyph ? fp4::toPixel(spaceGlyph->advanceX) : 0; // snap 12.4 fixed-point to nearest pixel
} }
int GfxRenderer::getSpaceKernAdjust(const int fontId, const uint32_t leftCp, const uint32_t rightCp, int GfxRenderer::getSpaceAdvance(const int fontId, const uint32_t leftCp, const uint32_t rightCp,
const EpdFontFamily::Style style) const { const EpdFontFamily::Style style) const {
const auto fontIt = fontMap.find(fontId); const auto fontIt = fontMap.find(fontId);
if (fontIt == fontMap.end()) return 0; if (fontIt == fontMap.end()) return 0;
const auto& font = fontIt->second; const auto& font = fontIt->second;
return font.getKerning(leftCp, ' ', style) + font.getKerning(' ', rightCp, style); const EpdGlyph* spaceGlyph = font.getGlyph(' ', style);
const int32_t spaceAdvanceFP = spaceGlyph ? static_cast<int32_t>(spaceGlyph->advanceX) : 0;
// Combine space advance + flanking kern into one fixed-point sum before snapping.
// Snapping the combined value avoids the +/-1 px error from snapping each component separately.
const int32_t kernFP = static_cast<int32_t>(font.getKerning(leftCp, ' ', style)) +
static_cast<int32_t>(font.getKerning(' ', rightCp, style));
return fp4::toPixel(spaceAdvanceFP + kernFP);
} }
int GfxRenderer::getKerning(const int fontId, const uint32_t leftCp, const uint32_t rightCp, int GfxRenderer::getKerning(const int fontId, const uint32_t leftCp, const uint32_t rightCp,
const EpdFontFamily::Style style) const { const EpdFontFamily::Style style) const {
const auto fontIt = fontMap.find(fontId); const auto fontIt = fontMap.find(fontId);
if (fontIt == fontMap.end()) return 0; if (fontIt == fontMap.end()) return 0;
return fontIt->second.getKerning(leftCp, rightCp, style); const int kernFP = fontIt->second.getKerning(leftCp, rightCp, style); // 4.4 fixed-point
return fp4::toPixel(kernFP); // snap 4.4 fixed-point to nearest pixel
} }
int GfxRenderer::getTextAdvanceX(const int fontId, const char* text, EpdFontFamily::Style style) const { int GfxRenderer::getTextAdvanceX(const int fontId, const char* text, EpdFontFamily::Style style) const {
@@ -973,7 +988,7 @@ int GfxRenderer::getTextAdvanceX(const int fontId, const char* text, EpdFontFami
uint32_t cp; uint32_t cp;
uint32_t prevCp = 0; uint32_t prevCp = 0;
int width = 0; int32_t widthFP = 0; // 12.4 fixed-point accumulator
const auto& font = fontIt->second; const auto& font = fontIt->second;
while ((cp = utf8NextCodepoint(reinterpret_cast<const uint8_t**>(&text)))) { while ((cp = utf8NextCodepoint(reinterpret_cast<const uint8_t**>(&text)))) {
if (utf8IsCombiningMark(cp)) { if (utf8IsCombiningMark(cp)) {
@@ -981,13 +996,13 @@ int GfxRenderer::getTextAdvanceX(const int fontId, const char* text, EpdFontFami
} }
cp = font.applyLigatures(cp, text, style); cp = font.applyLigatures(cp, text, style);
if (prevCp != 0) { if (prevCp != 0) {
width += font.getKerning(prevCp, cp, style); widthFP += font.getKerning(prevCp, cp, style); // 4.4 fixed-point kern
} }
const EpdGlyph* glyph = font.getGlyph(cp, style); const EpdGlyph* glyph = font.getGlyph(cp, style);
if (glyph) width += glyph->advanceX; if (glyph) widthFP += glyph->advanceX; // 12.4 fixed-point advance
prevCp = cp; prevCp = cp;
} }
return width; return fp4::toPixel(widthFP); // snap 12.4 fixed-point to nearest pixel
} }
int GfxRenderer::getFontAscenderSize(const int fontId) const { int GfxRenderer::getFontAscenderSize(const int fontId) const {
@@ -1034,11 +1049,9 @@ void GfxRenderer::drawTextRotated90CW(const int fontId, const int x, const int y
const auto& font = fontIt->second; const auto& font = fontIt->second;
int xPos = x; int32_t yPosFP = fp4::fromPixel(y); // 12.4 fixed-point accumulator
int yPos = y;
int lastBaseX = x;
int lastBaseY = y; int lastBaseY = y;
int lastBaseAdvance = 0; int lastBaseAdvanceFP = 0; // 12.4 fixed-point
int lastBaseTop = 0; int lastBaseTop = 0;
constexpr int MIN_COMBINING_GAP_PX = 1; constexpr int MIN_COMBINING_GAP_PX = 1;
@@ -1055,25 +1068,27 @@ void GfxRenderer::drawTextRotated90CW(const int fontId, const int x, const int y
} }
} }
int combiningX = lastBaseX - raiseBy; const int combiningX = x - raiseBy;
int combiningY = lastBaseY - lastBaseAdvance / 2; const int combiningY = lastBaseY - fp4::toPixel(lastBaseAdvanceFP / 2);
renderCharImpl<TextRotation::Rotated90CW>(*this, renderMode, font, cp, &combiningX, &combiningY, black, style); renderCharImpl<TextRotation::Rotated90CW>(*this, renderMode, font, cp, combiningX, combiningY, black, style);
continue; continue;
} }
cp = font.applyLigatures(cp, text, style); cp = font.applyLigatures(cp, text, style);
if (prevCp != 0) { if (prevCp != 0) {
yPos -= font.getKerning(prevCp, cp, style); yPosFP -= font.getKerning(prevCp, cp, style); // 4.4 fixed-point kern (subtract for rotated)
} }
lastBaseY = fp4::toPixel(yPosFP); // snap 12.4 fixed-point to nearest pixel
const EpdGlyph* glyph = font.getGlyph(cp, style); const EpdGlyph* glyph = font.getGlyph(cp, style);
lastBaseX = xPos; lastBaseAdvanceFP = glyph ? glyph->advanceX : 0; // 12.4 fixed-point
lastBaseY = yPos;
lastBaseAdvance = glyph ? glyph->advanceX : 0;
lastBaseTop = glyph ? glyph->top : 0; lastBaseTop = glyph ? glyph->top : 0;
renderCharImpl<TextRotation::Rotated90CW>(*this, renderMode, font, cp, &xPos, &yPos, black, style); renderCharImpl<TextRotation::Rotated90CW>(*this, renderMode, font, cp, x, lastBaseY, black, style);
if (glyph) {
yPosFP -= glyph->advanceX; // 12.4 fixed-point advance (subtract for rotated)
}
prevCp = cp; prevCp = cp;
} }
} }
@@ -1174,11 +1189,6 @@ void GfxRenderer::cleanupGrayscaleWithFrameBuffer() const {
} }
} }
void GfxRenderer::renderChar(const EpdFontFamily& fontFamily, uint32_t cp, int* x, int* y, bool pixelState,
EpdFontFamily::Style style) const {
renderCharImpl<TextRotation::None>(*this, renderMode, fontFamily, cp, x, y, pixelState, style);
}
void GfxRenderer::getOrientedViewableTRBL(int* outTop, int* outRight, int* outBottom, int* outLeft) const { void GfxRenderer::getOrientedViewableTRBL(int* outTop, int* outRight, int* outBottom, int* outLeft) const {
switch (orientation) { switch (orientation) {
case Portrait: case Portrait:
+16 -9
View File
@@ -1,9 +1,11 @@
#pragma once #pragma once
#include <EpdFontFamily.h> #include <EpdFontFamily.h>
#include <FontDecompressor.h>
#include <HalDisplay.h> #include <HalDisplay.h>
class FontCacheManager;
#include <cstring>
#include <map> #include <map>
#include <string> #include <string>
#include <vector> #include <vector>
@@ -39,7 +41,12 @@ class GfxRenderer {
uint8_t* frameBuffer = nullptr; uint8_t* frameBuffer = nullptr;
uint8_t* bwBufferChunks[BW_BUFFER_NUM_CHUNKS] = {nullptr}; uint8_t* bwBufferChunks[BW_BUFFER_NUM_CHUNKS] = {nullptr};
std::map<int, EpdFontFamily> fontMap; std::map<int, EpdFontFamily> fontMap;
FontDecompressor* fontDecompressor = nullptr;
// Mutable because drawText() is const but needs to delegate scan-mode
// recording to the (non-const) FontCacheManager. Same pragmatic compromise
// as before, concentrated in a single pointer instead of four fields.
mutable FontCacheManager* fontCacheManager_ = nullptr;
void renderChar(const EpdFontFamily& fontFamily, uint32_t cp, int* x, int* y, bool pixelState, void renderChar(const EpdFontFamily& fontFamily, uint32_t cp, int* x, int* y, bool pixelState,
EpdFontFamily::Style style) const; EpdFontFamily::Style style) const;
void freeBwBufferChunks(); void freeBwBufferChunks();
@@ -61,10 +68,9 @@ class GfxRenderer {
// Setup // Setup
void begin(); // must be called right after display.begin() void begin(); // must be called right after display.begin()
void insertFont(int fontId, EpdFontFamily font); void insertFont(int fontId, EpdFontFamily font);
void setFontDecompressor(FontDecompressor* d) { fontDecompressor = d; } void setFontCacheManager(FontCacheManager* m) { fontCacheManager_ = m; }
void clearFontCache() { FontCacheManager* getFontCacheManager() const { return fontCacheManager_; }
if (fontDecompressor) fontDecompressor->clearCache(); const std::map<int, EpdFontFamily>& getFontMap() const { return fontMap; }
}
// Orientation control (affects logical width/height and coordinate transforms) // Orientation control (affects logical width/height and coordinate transforms)
void setOrientation(const Orientation o) { orientation = o; } void setOrientation(const Orientation o) { orientation = o; }
@@ -112,9 +118,10 @@ class GfxRenderer {
void drawText(int fontId, int x, int y, const char* text, bool black = true, void drawText(int fontId, int x, int y, const char* text, bool black = true,
EpdFontFamily::Style style = EpdFontFamily::REGULAR) const; EpdFontFamily::Style style = EpdFontFamily::REGULAR) const;
int getSpaceWidth(int fontId, EpdFontFamily::Style style = EpdFontFamily::REGULAR) const; int getSpaceWidth(int fontId, EpdFontFamily::Style style = EpdFontFamily::REGULAR) const;
/// Returns the kerning adjustment for a space between two codepoints: /// Returns the total inter-word advance: fp4::toPixel(spaceAdvance + kern(leftCp,' ') + kern(' ',rightCp)).
/// kern(leftCp, ' ') + kern(' ', rightCp). Returns 0 if kerning is unavailable. /// Using a single snap avoids the +/-1 px rounding error that arises when space advance and kern are
int getSpaceKernAdjust(int fontId, uint32_t leftCp, uint32_t rightCp, EpdFontFamily::Style style) const; /// snapped separately and then added as integers.
int getSpaceAdvance(int fontId, uint32_t leftCp, uint32_t rightCp, EpdFontFamily::Style style) const;
/// Returns the kerning adjustment between two adjacent codepoints. /// Returns the kerning adjustment between two adjacent codepoints.
int getKerning(int fontId, uint32_t leftCp, uint32_t rightCp, EpdFontFamily::Style style) const; int getKerning(int fontId, uint32_t leftCp, uint32_t rightCp, EpdFontFamily::Style style) const;
int getTextAdvanceX(int fontId, const char* text, EpdFontFamily::Style style) const; int getTextAdvanceX(int fontId, const char* text, EpdFontFamily::Style style) const;
+1 -57
View File
@@ -9,12 +9,10 @@ STR_ENTERING_SLEEP: "Пераход у сон"
STR_BROWSE_FILES: "Прагляд файлаў" STR_BROWSE_FILES: "Прагляд файлаў"
STR_FILE_TRANSFER: "Перадача файлаў" STR_FILE_TRANSFER: "Перадача файлаў"
STR_SETTINGS_TITLE: "Налады" STR_SETTINGS_TITLE: "Налады"
STR_CALIBRE_LIBRARY: "Бібліятэка Calibre"
STR_CONTINUE_READING: "Працягнуць чытанне" STR_CONTINUE_READING: "Працягнуць чытанне"
STR_NO_OPEN_BOOK: "Няма адкрытай кнігі" STR_NO_OPEN_BOOK: "Няма адкрытай кнігі"
STR_START_READING: "Пачніце чытанне ніжэй" STR_START_READING: "Пачніце чытанне ніжэй"
STR_BOOKS: "Кнігі" STR_NO_FILES_FOUND: "Файлы не знойдзены"
STR_NO_BOOKS_FOUND: "Кнігі не знойдзены"
STR_SELECT_CHAPTER: "Абярыце раздзел" STR_SELECT_CHAPTER: "Абярыце раздзел"
STR_NO_CHAPTERS: "Раздзелаў няма" STR_NO_CHAPTERS: "Раздзелаў няма"
STR_END_OF_BOOK: "Канец кнігі" STR_END_OF_BOOK: "Канец кнігі"
@@ -26,10 +24,6 @@ STR_EMPTY_FILE: "Пусты файл"
STR_OUT_OF_BOUNDS: "Выхад за межы" STR_OUT_OF_BOUNDS: "Выхад за межы"
STR_LOADING: "Загрузка..." STR_LOADING: "Загрузка..."
STR_LOADING_POPUP: "Загрузка" STR_LOADING_POPUP: "Загрузка"
STR_LOAD_XTC_FAILED: "Не ўдалося загрузіць XTC"
STR_LOAD_TXT_FAILED: "Не ўдалося загрузіць TXT"
STR_LOAD_EPUB_FAILED: "Не ўдалося загрузіць EPUB"
STR_SD_CARD_ERROR: "Памылка SD-карты"
STR_WIFI_NETWORKS: "Сеткі Wi-Fi" STR_WIFI_NETWORKS: "Сеткі Wi-Fi"
STR_NO_NETWORKS: "Сеткі не знойдзены" STR_NO_NETWORKS: "Сеткі не знойдзены"
STR_NETWORKS_FOUND: "Знойдзена сетак: %zu" STR_NETWORKS_FOUND: "Знойдзена сетак: %zu"
@@ -37,14 +31,9 @@ STR_SCANNING: "Сканаванне..."
STR_CONNECTING: "Падключэнне..." STR_CONNECTING: "Падключэнне..."
STR_CONNECTED: "Падключана!" STR_CONNECTED: "Падключана!"
STR_CONNECTION_FAILED: "Памылка падключэння" STR_CONNECTION_FAILED: "Памылка падключэння"
STR_CONNECTION_TIMEOUT: "Тайм-аўт падключэння"
STR_FORGET_NETWORK: "Забыць сетку?" STR_FORGET_NETWORK: "Забыць сетку?"
STR_SAVE_PASSWORD: "Захаваць пароль?" STR_SAVE_PASSWORD: "Захаваць пароль?"
STR_REMOVE_PASSWORD: "Выдаліць захаваны пароль?"
STR_PRESS_OK_SCAN: "Націсніце OK для паўторнага пошуку" STR_PRESS_OK_SCAN: "Націсніце OK для паўторнага пошуку"
STR_PRESS_ANY_CONTINUE: "Націсніце любую кнопку"
STR_SELECT_HINT: "УЛЕВА/УПРАВА: выбар | OK: пацвердзіць"
STR_HOW_CONNECT: "Як вы хочаце падключыцца?"
STR_JOIN_NETWORK: "Падключыцца да сеткі" STR_JOIN_NETWORK: "Падключыцца да сеткі"
STR_CREATE_HOTSPOT: "Стварыць кропку доступу" STR_CREATE_HOTSPOT: "Стварыць кропку доступу"
STR_JOIN_DESC: "Падключэнне да існуючай сеткі Wi-Fi" STR_JOIN_DESC: "Падключэнне да існуючай сеткі Wi-Fi"
@@ -57,27 +46,13 @@ STR_OR_HTTP_PREFIX: "або http://"
STR_SCAN_QR_HINT: "або адсканіруйце QR-код:" STR_SCAN_QR_HINT: "або адсканіруйце QR-код:"
STR_CALIBRE_WIRELESS: "Calibre па Wi-Fi" STR_CALIBRE_WIRELESS: "Calibre па Wi-Fi"
STR_CALIBRE_WEB_URL: "Вэб-адрас Calibre" STR_CALIBRE_WEB_URL: "Вэб-адрас Calibre"
STR_CONNECT_WIRELESS: "Падключыць як бесправадную прыладу"
STR_NETWORK_LEGEND: "* = Абаронена | + = Захавана" STR_NETWORK_LEGEND: "* = Абаронена | + = Захавана"
STR_MAC_ADDRESS: "MAC-адрас:" STR_MAC_ADDRESS: "MAC-адрас:"
STR_CHECKING_WIFI: "Праверка Wi-Fi..." STR_CHECKING_WIFI: "Праверка Wi-Fi..."
STR_ENTER_WIFI_PASSWORD: "Увядзіце пароль Wi-Fi" STR_ENTER_WIFI_PASSWORD: "Увядзіце пароль Wi-Fi"
STR_ENTER_TEXT: "Увядзіце тэкст"
STR_TO_PREFIX: "да " STR_TO_PREFIX: "да "
STR_CALIBRE_DISCOVERING: "Пошук Calibre..."
STR_CALIBRE_CONNECTING_TO: "Падключэнне да "
STR_CALIBRE_CONNECTED_TO: "Падключана да "
STR_CALIBRE_WAITING_COMMANDS: "Чаканне каманд..."
STR_CONNECTION_FAILED_RETRYING: "(Памылка падключэння, паўторная спроба)"
STR_CALIBRE_DISCONNECTED: "Злучэнне з Calibre разарвана"
STR_CALIBRE_WAITING_TRANSFER: "Чаканне перадачы..."
STR_CALIBRE_TRANSFER_HINT: "Калі перадача не ўдаецца"
STR_CALIBRE_RECEIVING: "Атрыманне:" STR_CALIBRE_RECEIVING: "Атрыманне:"
STR_CALIBRE_RECEIVED: "Атрымана:" STR_CALIBRE_RECEIVED: "Атрымана:"
STR_CALIBRE_WAITING_MORE: "Чаканне наступных файлаў..."
STR_CALIBRE_FAILED_CREATE_FILE: "Не ўдалося стварыць файл"
STR_CALIBRE_PASSWORD_REQUIRED: "Патрабуецца пароль"
STR_CALIBRE_TRANSFER_INTERRUPTED: "Перадача перапынена"
STR_CALIBRE_INSTRUCTION_1: "1) Усталюйце плагін CrossPoint Reader" STR_CALIBRE_INSTRUCTION_1: "1) Усталюйце плагін CrossPoint Reader"
STR_CALIBRE_INSTRUCTION_2: "2) Падключыцеся да той жа сеткі Wi-Fi" STR_CALIBRE_INSTRUCTION_2: "2) Падключыцеся да той жа сеткі Wi-Fi"
STR_CALIBRE_INSTRUCTION_3: "3) У Calibre абярыце: «Адправіць на прыладу»" STR_CALIBRE_INSTRUCTION_3: "3) У Calibre абярыце: «Адправіць на прыладу»"
@@ -88,37 +63,25 @@ STR_CAT_CONTROLS: "Кіраванне"
STR_CAT_SYSTEM: "Сістэма" STR_CAT_SYSTEM: "Сістэма"
STR_SLEEP_SCREEN: "Экран сну" STR_SLEEP_SCREEN: "Экран сну"
STR_SLEEP_COVER_MODE: "Рэжым вокладкі сну" STR_SLEEP_COVER_MODE: "Рэжым вокладкі сну"
STR_STATUS_BAR: "Радок стану"
STR_HIDE_BATTERY: "Схаваць % батарэі" STR_HIDE_BATTERY: "Схаваць % батарэі"
STR_EXTRA_SPACING: "Дадат. інтэрвал абзаца" STR_EXTRA_SPACING: "Дадат. інтэрвал абзаца"
STR_TEXT_AA: "Згладжванне тэксту" STR_TEXT_AA: "Згладжванне тэксту"
STR_SHORT_PWR_BTN: "Кароткае націсканне PWR" STR_SHORT_PWR_BTN: "Кароткае націсканне PWR"
STR_ORIENTATION: "Арыентацыя чытання" STR_ORIENTATION: "Арыентацыя чытання"
STR_FRONT_BTN_LAYOUT: "Бакавыя кнопкі"
STR_SIDE_BTN_LAYOUT: "Бакавыя кнопкі" STR_SIDE_BTN_LAYOUT: "Бакавыя кнопкі"
STR_LONG_PRESS_SKIP: "Доўгае націсканне - змена раздзела" STR_LONG_PRESS_SKIP: "Доўгае націсканне - змена раздзела"
STR_FONT_FAMILY: "Шрыфт чытання" STR_FONT_FAMILY: "Шрыфт чытання"
STR_EXT_READER_FONT: "Знешні шрыфт чытання"
STR_EXT_CHINESE_FONT: "Шрыфт CJK"
STR_EXT_UI_FONT: "Шрыфт інтэрфейсу"
STR_FONT_SIZE: "Памер шрыфту інтэрфейсу" STR_FONT_SIZE: "Памер шрыфту інтэрфейсу"
STR_LINE_SPACING: "Міжрадковы інтэрвал" STR_LINE_SPACING: "Міжрадковы інтэрвал"
STR_ASCII_LETTER_SPACING: "Інтэрвал літар ASCII"
STR_ASCII_DIGIT_SPACING: "Інтэрвал лічбаў ASCII"
STR_CJK_SPACING: "Інтэрвал CJK"
STR_COLOR_MODE: "Каляровы рэжым"
STR_SCREEN_MARGIN: "Палі экрана" STR_SCREEN_MARGIN: "Палі экрана"
STR_PARA_ALIGNMENT: "Выраўноўванне абзаца" STR_PARA_ALIGNMENT: "Выраўноўванне абзаца"
STR_HYPHENATION: "Перанос слоў" STR_HYPHENATION: "Перанос слоў"
STR_TIME_TO_SLEEP: "Сон праз" STR_TIME_TO_SLEEP: "Сон праз"
STR_REFRESH_FREQ: "Частата абнаўлення" STR_REFRESH_FREQ: "Частата абнаўлення"
STR_CALIBRE_SETTINGS: "Налады Calibre"
STR_KOREADER_SYNC: "Сінхранізацыя KOReader" STR_KOREADER_SYNC: "Сінхранізацыя KOReader"
STR_CHECK_UPDATES: "Праверыць абнаўленні" STR_CHECK_UPDATES: "Праверыць абнаўленні"
STR_LANGUAGE: "Мова" STR_LANGUAGE: "Мова"
STR_SELECT_WALLPAPER: "Абраць шпалеры"
STR_CLEAR_READING_CACHE: "Ачысціць кэш чытання" STR_CLEAR_READING_CACHE: "Ачысціць кэш чытання"
STR_CALIBRE: "Calibre"
STR_USERNAME: "Імя карыстальніка" STR_USERNAME: "Імя карыстальніка"
STR_PASSWORD: "Пароль" STR_PASSWORD: "Пароль"
STR_SYNC_SERVER_URL: "URL сервера сінхранізацыі" STR_SYNC_SERVER_URL: "URL сервера сінхранізацыі"
@@ -153,8 +116,6 @@ STR_COVER: "Вокладка"
STR_NONE_OPT: "Няма" STR_NONE_OPT: "Няма"
STR_FIT: "Упісаць" STR_FIT: "Упісаць"
STR_CROP: "Абрэзаць" STR_CROP: "Абрэзаць"
STR_NO_PROGRESS: "Без прагрэсу"
STR_FULL_OPT: "Поўная"
STR_NEVER: "Ніколі" STR_NEVER: "Ніколі"
STR_IN_READER: "У рэжыме чытання" STR_IN_READER: "У рэжыме чытання"
STR_ALWAYS: "Заўсёды" STR_ALWAYS: "Заўсёды"
@@ -165,9 +126,6 @@ STR_PORTRAIT: "Партрэт"
STR_LANDSCAPE_CW: "Ландшафт (CW)" STR_LANDSCAPE_CW: "Ландшафт (CW)"
STR_INVERTED: "Інверсія" STR_INVERTED: "Інверсія"
STR_LANDSCAPE_CCW: "Ландшафт (CCW)" STR_LANDSCAPE_CCW: "Ландшафт (CCW)"
STR_FRONT_LAYOUT_BCLR: "Наз, Ок, Лев, Прав"
STR_FRONT_LAYOUT_LRBC: "Лев, Прав, Наз, Ок"
STR_FRONT_LAYOUT_LBCR: "Лев, Наз, Ок, Прав"
STR_PREV_NEXT: "Назад/Наперад" STR_PREV_NEXT: "Назад/Наперад"
STR_NEXT_PREV: "Наперад/Назад" STR_NEXT_PREV: "Наперад/Назад"
STR_BOOKERLY: "Bookerly" STR_BOOKERLY: "Bookerly"
@@ -204,8 +162,6 @@ STR_NO_UPDATE: "Абнаўленняў няма"
STR_UPDATE_FAILED: "Памылка абнаўлення" STR_UPDATE_FAILED: "Памылка абнаўлення"
STR_UPDATE_COMPLETE: "Абнаўленне завершана" STR_UPDATE_COMPLETE: "Абнаўленне завершана"
STR_POWER_ON_HINT: "Утрымлівайце кнопку сілкавання для ўключэння" STR_POWER_ON_HINT: "Утрымлівайце кнопку сілкавання для ўключэння"
STR_EXTERNAL_FONT: "Карыстальніцкі шрыфт"
STR_BUILTIN_DISABLED: "Убудаваны (адключаны)"
STR_NO_ENTRIES: "Запісы не знойдзены" STR_NO_ENTRIES: "Запісы не знойдзены"
STR_DOWNLOADING: "Спампоўка..." STR_DOWNLOADING: "Спампоўка..."
STR_DOWNLOAD_FAILED: "Памылка спампоўкі" STR_DOWNLOAD_FAILED: "Памылка спампоўкі"
@@ -216,7 +172,6 @@ STR_FETCH_FEED_FAILED: "Не ўдалося атрымаць стужку"
STR_PARSE_FEED_FAILED: "Не ўдалося апрацаваць стужку" STR_PARSE_FEED_FAILED: "Не ўдалося апрацаваць стужку"
STR_NETWORK_PREFIX: "Сетка:" STR_NETWORK_PREFIX: "Сетка:"
STR_IP_ADDRESS_PREFIX: "IP-адрас:" STR_IP_ADDRESS_PREFIX: "IP-адрас:"
STR_SCAN_QR_WIFI_HINT: "або адсканіруйце QR-код для падключэння да Wi-Fi."
STR_ERROR_GENERAL_FAILURE: "Памылка: Агульная памылка" STR_ERROR_GENERAL_FAILURE: "Памылка: Агульная памылка"
STR_ERROR_NETWORK_NOT_FOUND: "Памылка: Сетка не знойдзена" STR_ERROR_NETWORK_NOT_FOUND: "Памылка: Сетка не знойдзена"
STR_ERROR_CONNECTION_TIMEOUT: "Памылка: Тайм-аўт злучэння" STR_ERROR_CONNECTION_TIMEOUT: "Памылка: Тайм-аўт злучэння"
@@ -224,7 +179,6 @@ STR_SD_CARD: "SD-карта"
STR_BACK: "« Назад" STR_BACK: "« Назад"
STR_EXIT: "« Выхад" STR_EXIT: "« Выхад"
STR_HOME: "« Галоўная" STR_HOME: "« Галоўная"
STR_SAVE: "« Захаваць"
STR_SELECT: "Абраць" STR_SELECT: "Абраць"
STR_TOGGLE: "Выбар" STR_TOGGLE: "Выбар"
STR_CONFIRM: "Пацв." STR_CONFIRM: "Пацв."
@@ -242,15 +196,9 @@ STR_DIR_LEFT: "Улева"
STR_DIR_RIGHT: "Управа" STR_DIR_RIGHT: "Управа"
STR_DIR_UP: "Уверх" STR_DIR_UP: "Уверх"
STR_DIR_DOWN: "Уніз" STR_DIR_DOWN: "Уніз"
STR_CAPS_ON: "CAPS"
STR_CAPS_OFF: "caps"
STR_OK_BUTTON: "OK" STR_OK_BUTTON: "OK"
STR_SLEEP_COVER_FILTER: "Фільтр экрана сну" STR_SLEEP_COVER_FILTER: "Фільтр экрана сну"
STR_FILTER_CONTRAST: "Кантраст" STR_FILTER_CONTRAST: "Кантраст"
STR_STATUS_BAR_FULL_PERCENT: "Поўная + %"
STR_STATUS_BAR_FULL_BOOK: "Поўная + шкала кнігі"
STR_STATUS_BAR_BOOK_ONLY: "Толькі шкала кнігі"
STR_STATUS_BAR_FULL_CHAPTER: "Поўная + шкала раздзела"
STR_UI_THEME: "Тэма інтэрфейсу" STR_UI_THEME: "Тэма інтэрфейсу"
STR_THEME_CLASSIC: "Класічная" STR_THEME_CLASSIC: "Класічная"
STR_THEME_LYRA: "Lyra" STR_THEME_LYRA: "Lyra"
@@ -259,7 +207,6 @@ STR_SUNLIGHT_FADING_FIX: "Кампенсацыя выцвітання"
STR_REMAP_FRONT_BUTTONS: "Пераназначыць пярэднія кнопкі" STR_REMAP_FRONT_BUTTONS: "Пераназначыць пярэднія кнопкі"
STR_OPDS_BROWSER: "OPDS браўзер" STR_OPDS_BROWSER: "OPDS браўзер"
STR_COVER_CUSTOM: "Вокладка + Свой" STR_COVER_CUSTOM: "Вокладка + Свой"
STR_RECENTS: "Нядаўнія"
STR_MENU_RECENT_BOOKS: "Нядаўнія кнігі" STR_MENU_RECENT_BOOKS: "Нядаўнія кнігі"
STR_NO_RECENT_BOOKS: "Няма нядаўніх кніг" STR_NO_RECENT_BOOKS: "Няма нядаўніх кніг"
STR_CALIBRE_DESC: "Выкарыстоўваць бесправадную перадачу Calibre" STR_CALIBRE_DESC: "Выкарыстоўваць бесправадную перадачу Calibre"
@@ -286,9 +233,6 @@ STR_DELETE_CACHE: "Выдаліць кэш кнігі"
STR_CHAPTER_PREFIX: "Раздзел:" STR_CHAPTER_PREFIX: "Раздзел:"
STR_PAGES_SEPARATOR: "стар. |" STR_PAGES_SEPARATOR: "стар. |"
STR_BOOK_PREFIX: "Кніга:" STR_BOOK_PREFIX: "Кніга:"
STR_KBD_SHIFT: "shift"
STR_KBD_SHIFT_CAPS: "SHIFT"
STR_KBD_LOCK: "LOCK"
STR_CALIBRE_URL_HINT: "Для Calibre дадайце /opds да URL" STR_CALIBRE_URL_HINT: "Для Calibre дадайце /opds да URL"
STR_PERCENT_STEP_HINT: "Улева/Управа: 1% Уверх/Уніз: 10%" STR_PERCENT_STEP_HINT: "Улева/Управа: 1% Уверх/Уніз: 10%"
STR_SYNCING_TIME: "Сінхранізацыя часу..." STR_SYNCING_TIME: "Сінхранізацыя часу..."
+31 -53
View File
@@ -9,12 +9,10 @@ STR_ENTERING_SLEEP: "Entrant en repòs"
STR_BROWSE_FILES: "Explora fitxers" STR_BROWSE_FILES: "Explora fitxers"
STR_FILE_TRANSFER: "Transferència" STR_FILE_TRANSFER: "Transferència"
STR_SETTINGS_TITLE: "Configuració" STR_SETTINGS_TITLE: "Configuració"
STR_CALIBRE_LIBRARY: "Biblioteca del Calibre"
STR_CONTINUE_READING: "Continua llegint" STR_CONTINUE_READING: "Continua llegint"
STR_NO_OPEN_BOOK: "Cap llibre obert" STR_NO_OPEN_BOOK: "Cap llibre obert"
STR_START_READING: "Inicia la lectura a continuació" STR_START_READING: "Inicia la lectura a continuació"
STR_BOOKS: "Llibres" STR_NO_FILES_FOUND: "No s'han trobat fitxers"
STR_NO_BOOKS_FOUND: "No s'ha trobat cap llibre"
STR_SELECT_CHAPTER: "Selecciona el capítol" STR_SELECT_CHAPTER: "Selecciona el capítol"
STR_NO_CHAPTERS: "Sense capítols" STR_NO_CHAPTERS: "Sense capítols"
STR_END_OF_BOOK: "Final del llibre" STR_END_OF_BOOK: "Final del llibre"
@@ -26,10 +24,6 @@ STR_EMPTY_FILE: "Fitxer buit"
STR_OUT_OF_BOUNDS: "Fora de límits" STR_OUT_OF_BOUNDS: "Fora de límits"
STR_LOADING: "S'està carregant..." STR_LOADING: "S'està carregant..."
STR_LOADING_POPUP: "S'està carregant" STR_LOADING_POPUP: "S'està carregant"
STR_LOAD_XTC_FAILED: "No s'ha pogut carregar l'XTC"
STR_LOAD_TXT_FAILED: "No s'ha pogut carregar el TXT"
STR_LOAD_EPUB_FAILED: "No s'ha pogut carregar l'EPUB"
STR_SD_CARD_ERROR: "Error de targeta SD"
STR_WIFI_NETWORKS: "Xarxes WiFi" STR_WIFI_NETWORKS: "Xarxes WiFi"
STR_NO_NETWORKS: "No s'han trobat xarxes" STR_NO_NETWORKS: "No s'han trobat xarxes"
STR_NETWORKS_FOUND: "%zu xarxes trobades" STR_NETWORKS_FOUND: "%zu xarxes trobades"
@@ -37,14 +31,9 @@ STR_SCANNING: "S'està escanejant..."
STR_CONNECTING: "S'està connectant..." STR_CONNECTING: "S'està connectant..."
STR_CONNECTED: "S'ha connectat!" STR_CONNECTED: "S'ha connectat!"
STR_CONNECTION_FAILED: "Error de connexió" STR_CONNECTION_FAILED: "Error de connexió"
STR_CONNECTION_TIMEOUT: "S'ha esgotat el temps de connexió"
STR_FORGET_NETWORK: "Voleu oblidar aquesta xarxa?" STR_FORGET_NETWORK: "Voleu oblidar aquesta xarxa?"
STR_SAVE_PASSWORD: "Voleu desar la contrasenya per a la propera vegada?" STR_SAVE_PASSWORD: "Voleu desar la contrasenya per a la propera vegada?"
STR_REMOVE_PASSWORD: "Voleu suprimir la contrasenya desada?"
STR_PRESS_OK_SCAN: "Premeu OK per tornar a escanejar" STR_PRESS_OK_SCAN: "Premeu OK per tornar a escanejar"
STR_PRESS_ANY_CONTINUE: "Premeu qualsevol botó per continuar"
STR_SELECT_HINT: "ESQUERRA/DRETA: Selecciona | OK: Confirma"
STR_HOW_CONNECT: "Com voleu connectar-vos?"
STR_JOIN_NETWORK: "Uneix-te a una xarxa" STR_JOIN_NETWORK: "Uneix-te a una xarxa"
STR_CREATE_HOTSPOT: "Crea un punt d'accés" STR_CREATE_HOTSPOT: "Crea un punt d'accés"
STR_JOIN_DESC: "Connecta't a una xarxa WiFi existent" STR_JOIN_DESC: "Connecta't a una xarxa WiFi existent"
@@ -57,27 +46,13 @@ STR_OR_HTTP_PREFIX: "o http://"
STR_SCAN_QR_HINT: "o escanegeu el codi QR amb el telèfon:" STR_SCAN_QR_HINT: "o escanegeu el codi QR amb el telèfon:"
STR_CALIBRE_WIRELESS: "Calibre sense fils" STR_CALIBRE_WIRELESS: "Calibre sense fils"
STR_CALIBRE_WEB_URL: "URL web del Calibre" STR_CALIBRE_WEB_URL: "URL web del Calibre"
STR_CONNECT_WIRELESS: "Connecta com a dispositiu sense fils"
STR_NETWORK_LEGEND: "* = Encriptat | + = Desat" STR_NETWORK_LEGEND: "* = Encriptat | + = Desat"
STR_MAC_ADDRESS: "Adreça MAC:" STR_MAC_ADDRESS: "Adreça MAC:"
STR_CHECKING_WIFI: "S'està comprovant el WiFi..." STR_CHECKING_WIFI: "S'està comprovant el WiFi..."
STR_ENTER_WIFI_PASSWORD: "Introduïu la contrasenya WiFi" STR_ENTER_WIFI_PASSWORD: "Introduïu la contrasenya WiFi"
STR_ENTER_TEXT: "Introduïu el text"
STR_TO_PREFIX: "a " STR_TO_PREFIX: "a "
STR_CALIBRE_DISCOVERING: "S'està descobrint el Calibre..."
STR_CALIBRE_CONNECTING_TO: "S'està connectant a "
STR_CALIBRE_CONNECTED_TO: "S'ha connectat a "
STR_CALIBRE_WAITING_COMMANDS: "S'estan esperant les ordres..."
STR_CONNECTION_FAILED_RETRYING: "(La connexió ha fallat, s'està tornant a intentar)"
STR_CALIBRE_DISCONNECTED: "Calibre desconnectat"
STR_CALIBRE_WAITING_TRANSFER: "S'està esperant la transferència..."
STR_CALIBRE_TRANSFER_HINT: "Si la transferència falla, activeu\\n'Ignora l'espai lliure' a la configuració del\\nconnector SmartDevice a Calibre."
STR_CALIBRE_RECEIVING: "S'està rebent: " STR_CALIBRE_RECEIVING: "S'està rebent: "
STR_CALIBRE_RECEIVED: "S'ha rebut: " STR_CALIBRE_RECEIVED: "S'ha rebut: "
STR_CALIBRE_WAITING_MORE: "S'està esperant més..."
STR_CALIBRE_FAILED_CREATE_FILE: "No s'ha pogut crear el fitxer"
STR_CALIBRE_PASSWORD_REQUIRED: "Contrasenya requerida"
STR_CALIBRE_TRANSFER_INTERRUPTED: "Transferència interrompuda"
STR_CALIBRE_INSTRUCTION_1: "1) Instal·leu el connector CrossPoint Reader" STR_CALIBRE_INSTRUCTION_1: "1) Instal·leu el connector CrossPoint Reader"
STR_CALIBRE_INSTRUCTION_2: "2) Estigueu a la mateixa xarxa WiFi" STR_CALIBRE_INSTRUCTION_2: "2) Estigueu a la mateixa xarxa WiFi"
STR_CALIBRE_INSTRUCTION_3: "3) A Calibre: \"Envia a un dispositiu\"" STR_CALIBRE_INSTRUCTION_3: "3) A Calibre: \"Envia a un dispositiu\""
@@ -88,37 +63,29 @@ STR_CAT_CONTROLS: "Controls"
STR_CAT_SYSTEM: "Sistema" STR_CAT_SYSTEM: "Sistema"
STR_SLEEP_SCREEN: "Pantalla de repòs" STR_SLEEP_SCREEN: "Pantalla de repòs"
STR_SLEEP_COVER_MODE: "Mode de pantalla de repòs" STR_SLEEP_COVER_MODE: "Mode de pantalla de repòs"
STR_STATUS_BAR: "Barra d'estat"
STR_HIDE_BATTERY: "Oculta el % de bateria" STR_HIDE_BATTERY: "Oculta el % de bateria"
STR_EXTRA_SPACING: "Espaiat de paràgraf extra" STR_EXTRA_SPACING: "Espaiat de paràgraf extra"
STR_TEXT_AA: "Antialiàsing del text" STR_TEXT_AA: "Antialiàsing del text"
STR_IMAGES: "Imatges"
STR_IMAGES_DISPLAY: "Mostrar"
STR_IMAGES_PLACEHOLDER: "Text de mostra"
STR_IMAGES_SUPPRESS: "Suprimir"
STR_SHORT_PWR_BTN: "Clic curt del botó d'engegada" STR_SHORT_PWR_BTN: "Clic curt del botó d'engegada"
STR_ORIENTATION: "Orientació de lectura" STR_ORIENTATION: "Orientació de lectura"
STR_FRONT_BTN_LAYOUT: "Disposició dels botons frontals"
STR_SIDE_BTN_LAYOUT: "Disposició botons laterals" STR_SIDE_BTN_LAYOUT: "Disposició botons laterals"
STR_LONG_PRESS_SKIP: "Pressió llarga omet el capítol" STR_LONG_PRESS_SKIP: "Pressió llarga omet el capítol"
STR_FONT_FAMILY: "Tipus de lletra" STR_FONT_FAMILY: "Tipus de lletra"
STR_EXT_READER_FONT: "Tipus de lletra extern"
STR_EXT_CHINESE_FONT: "Tipus de lletra"
STR_EXT_UI_FONT: "Tipus de lletra (UI)"
STR_FONT_SIZE: "Mida de la lletra (UI)" STR_FONT_SIZE: "Mida de la lletra (UI)"
STR_LINE_SPACING: "Interlineat del lector" STR_LINE_SPACING: "Interlineat del lector"
STR_ASCII_LETTER_SPACING: "Espaiat de la lletra ASCII"
STR_ASCII_DIGIT_SPACING: "Espaiat del dígit ASCII"
STR_CJK_SPACING: "Espaiat CJK"
STR_COLOR_MODE: "Mode de color"
STR_SCREEN_MARGIN: "Marge de pantalla del lector" STR_SCREEN_MARGIN: "Marge de pantalla del lector"
STR_PARA_ALIGNMENT: "Alineació de paràgrafs del lector" STR_PARA_ALIGNMENT: "Alineació de paràgrafs del lector"
STR_HYPHENATION: "Partició de mots" STR_HYPHENATION: "Partició de mots"
STR_TIME_TO_SLEEP: "Temps per entrar en repòs" STR_TIME_TO_SLEEP: "Temps per entrar en repòs"
STR_REFRESH_FREQ: "Freqüència de refresc" STR_REFRESH_FREQ: "Freqüència de refresc"
STR_CALIBRE_SETTINGS: "Configuració del Calibre"
STR_KOREADER_SYNC: "Sincronització del KOReader" STR_KOREADER_SYNC: "Sincronització del KOReader"
STR_CHECK_UPDATES: "Comprova si hi ha actualitzacions" STR_CHECK_UPDATES: "Comprova si hi ha actualitzacions"
STR_LANGUAGE: "Idioma" STR_LANGUAGE: "Idioma"
STR_SELECT_WALLPAPER: "Selecciona un fons de pantalla"
STR_CLEAR_READING_CACHE: "Esborra la memòria cau de lectura" STR_CLEAR_READING_CACHE: "Esborra la memòria cau de lectura"
STR_CALIBRE: "Calibre"
STR_USERNAME: "Nom d'usuari" STR_USERNAME: "Nom d'usuari"
STR_PASSWORD: "Contrasenya" STR_PASSWORD: "Contrasenya"
STR_SYNC_SERVER_URL: "URL del servidor de sincronització" STR_SYNC_SERVER_URL: "URL del servidor de sincronització"
@@ -153,8 +120,6 @@ STR_COVER: "Portada"
STR_NONE_OPT: "Cap" STR_NONE_OPT: "Cap"
STR_FIT: "Ajustar" STR_FIT: "Ajustar"
STR_CROP: "Retallar" STR_CROP: "Retallar"
STR_NO_PROGRESS: "Sense progrés"
STR_FULL_OPT: "Completa"
STR_NEVER: "Mai" STR_NEVER: "Mai"
STR_IN_READER: "Al lector" STR_IN_READER: "Al lector"
STR_ALWAYS: "Sempre" STR_ALWAYS: "Sempre"
@@ -165,9 +130,6 @@ STR_PORTRAIT: "Vertical"
STR_LANDSCAPE_CW: "Horitzontal horari" STR_LANDSCAPE_CW: "Horitzontal horari"
STR_INVERTED: "Invertit" STR_INVERTED: "Invertit"
STR_LANDSCAPE_CCW: "Horitzontal antihorari" STR_LANDSCAPE_CCW: "Horitzontal antihorari"
STR_FRONT_LAYOUT_BCLR: "Enr, Cnfrm, Esq, Dreta"
STR_FRONT_LAYOUT_LRBC: "Esq, Dreta, Enr, Cnfrm"
STR_FRONT_LAYOUT_LBCR: "Esq, Enr, Cnfrm, Dreta"
STR_PREV_NEXT: "Anterior/Següent" STR_PREV_NEXT: "Anterior/Següent"
STR_NEXT_PREV: "Següent/Anterior" STR_NEXT_PREV: "Següent/Anterior"
STR_BOOKERLY: "Bookerly" STR_BOOKERLY: "Bookerly"
@@ -204,8 +166,6 @@ STR_NO_UPDATE: "No hi ha actualitzacions disponibles"
STR_UPDATE_FAILED: "Ha fallat l'actualització" STR_UPDATE_FAILED: "Ha fallat l'actualització"
STR_UPDATE_COMPLETE: "Actualització completada" STR_UPDATE_COMPLETE: "Actualització completada"
STR_POWER_ON_HINT: "Premeu i manteniu premut el botó d'encesa per tornar a engegar" STR_POWER_ON_HINT: "Premeu i manteniu premut el botó d'encesa per tornar a engegar"
STR_EXTERNAL_FONT: "Tipus de lletra extern"
STR_BUILTIN_DISABLED: "Integrat (desactivat)"
STR_NO_ENTRIES: "No s'ha trobat cap entrada" STR_NO_ENTRIES: "No s'ha trobat cap entrada"
STR_DOWNLOADING: "S'està baixant..." STR_DOWNLOADING: "S'està baixant..."
STR_DOWNLOAD_FAILED: "Ha fallat la baixada" STR_DOWNLOAD_FAILED: "Ha fallat la baixada"
@@ -216,7 +176,6 @@ STR_FETCH_FEED_FAILED: "Ha fallat l'obtenció del feed"
STR_PARSE_FEED_FAILED: "Ha fallat l'anàlisi del feed" STR_PARSE_FEED_FAILED: "Ha fallat l'anàlisi del feed"
STR_NETWORK_PREFIX: "Xarxa: " STR_NETWORK_PREFIX: "Xarxa: "
STR_IP_ADDRESS_PREFIX: "Adreça IP: " STR_IP_ADDRESS_PREFIX: "Adreça IP: "
STR_SCAN_QR_WIFI_HINT: "o escanegeu el codi QR amb el telèfon per connectar el WiFi."
STR_ERROR_GENERAL_FAILURE: "Error: Fallada general" STR_ERROR_GENERAL_FAILURE: "Error: Fallada general"
STR_ERROR_NETWORK_NOT_FOUND: "Error: No s'ha trobat la xarxa" STR_ERROR_NETWORK_NOT_FOUND: "Error: No s'ha trobat la xarxa"
STR_ERROR_CONNECTION_TIMEOUT: "Error: temps de connexió esgotat" STR_ERROR_CONNECTION_TIMEOUT: "Error: temps de connexió esgotat"
@@ -224,8 +183,8 @@ STR_SD_CARD: "Targeta SD"
STR_BACK: "« Enrere" STR_BACK: "« Enrere"
STR_EXIT: "« Surt" STR_EXIT: "« Surt"
STR_HOME: "« Inici" STR_HOME: "« Inici"
STR_SAVE: "« Desa"
STR_SELECT: "Selecciona" STR_SELECT: "Selecciona"
STR_SELECTED: "Seleccionat"
STR_TOGGLE: "Canvia" STR_TOGGLE: "Canvia"
STR_CONFIRM: "Confirma" STR_CONFIRM: "Confirma"
STR_CANCEL: "Cancel·la" STR_CANCEL: "Cancel·la"
@@ -235,6 +194,8 @@ STR_DOWNLOAD: "Descarrega"
STR_RETRY: "Nou intent" STR_RETRY: "Nou intent"
STR_YES: "Sí" STR_YES: "Sí"
STR_NO: "No" STR_NO: "No"
STR_SHOW: "Mostrar"
STR_HIDE: "Amagar"
STR_STATE_ON: "ON" STR_STATE_ON: "ON"
STR_STATE_OFF: "OFF" STR_STATE_OFF: "OFF"
STR_NOT_SET: "No establert" STR_NOT_SET: "No establert"
@@ -242,11 +203,24 @@ STR_DIR_LEFT: "Esquerra"
STR_DIR_RIGHT: "Dreta" STR_DIR_RIGHT: "Dreta"
STR_DIR_UP: "Amunt" STR_DIR_UP: "Amunt"
STR_DIR_DOWN: "Avall" STR_DIR_DOWN: "Avall"
STR_CAPS_ON: "MAJS"
STR_CAPS_OFF: "majs"
STR_OK_BUTTON: "OK" STR_OK_BUTTON: "OK"
STR_SLEEP_COVER_FILTER: "Filtre de pantalla de repòs" STR_SLEEP_COVER_FILTER: "Filtre de pantalla de repòs"
STR_FILTER_CONTRAST: "Contrast" STR_FILTER_CONTRAST: "Contrast"
STR_CUSTOMISE_STATUS_BAR: "Personalitza la barra d'estat"
STR_CHAPTER_PAGE_COUNT: "Comptador de pàgines del capítol"
STR_BOOK_PROGRESS_PERCENTAGE: "Percentatge de progrés del llibre"
STR_PROGRESS_BAR: "Barra de progrés"
STR_PROGRESS_BAR_THICKNESS: "Gruix de la barra de progrés"
STR_PROGRESS_BAR_THIN: "Fina"
STR_PROGRESS_BAR_MEDIUM: "Mitjana"
STR_PROGRESS_BAR_THICK: "Gruixuda"
STR_BOOK: "Llibre"
STR_CHAPTER: "Capítol"
STR_EXAMPLE_CHAPTER: "Capítol 21"
STR_EXAMPLE_BOOK: "Títol del llibre"
STR_PREVIEW: "Vista prèvia"
STR_TITLE: "Títol"
STR_BATTERY: "Bateria"
STR_UI_THEME: "Tema de la interfície" STR_UI_THEME: "Tema de la interfície"
STR_THEME_CLASSIC: "Clàssic" STR_THEME_CLASSIC: "Clàssic"
STR_THEME_LYRA: "Lyra" STR_THEME_LYRA: "Lyra"
@@ -255,7 +229,6 @@ STR_SUNLIGHT_FADING_FIX: "Correcció de l'esvaïment pel sol"
STR_REMAP_FRONT_BUTTONS: "Reassigna els botons frontals" STR_REMAP_FRONT_BUTTONS: "Reassigna els botons frontals"
STR_OPDS_BROWSER: "Navegador OPDS" STR_OPDS_BROWSER: "Navegador OPDS"
STR_COVER_CUSTOM: "Portada + Personalitzat" STR_COVER_CUSTOM: "Portada + Personalitzat"
STR_RECENTS: "Recents"
STR_MENU_RECENT_BOOKS: "Llibres recents" STR_MENU_RECENT_BOOKS: "Llibres recents"
STR_NO_RECENT_BOOKS: "No hi ha llibres recents" STR_NO_RECENT_BOOKS: "No hi ha llibres recents"
STR_CALIBRE_DESC: "Usa les transferències sense fils de Calibre" STR_CALIBRE_DESC: "Usa les transferències sense fils de Calibre"
@@ -279,12 +252,11 @@ STR_GO_TO_PERCENT: "Ves al %"
STR_GO_HOME_BUTTON: "Ves a l'inici" STR_GO_HOME_BUTTON: "Ves a l'inici"
STR_SYNC_PROGRESS: "Sincronitza el progrés" STR_SYNC_PROGRESS: "Sincronitza el progrés"
STR_DELETE_CACHE: "Esborra la memòria cau del llibre" STR_DELETE_CACHE: "Esborra la memòria cau del llibre"
STR_DELETE: "Esborra"
STR_DISPLAY_QR: "Mostra la pàgina com a QR"
STR_CHAPTER_PREFIX: "Capítol: " STR_CHAPTER_PREFIX: "Capítol: "
STR_PAGES_SEPARATOR: " pàgines | " STR_PAGES_SEPARATOR: " pàgines | "
STR_BOOK_PREFIX: "Llibre: " STR_BOOK_PREFIX: "Llibre: "
STR_KBD_SHIFT: "maj"
STR_KBD_SHIFT_CAPS: "MAJ"
STR_KBD_LOCK: "BLOCA"
STR_CALIBRE_URL_HINT: "Per al Calibre, afegiu /opds a la URL" STR_CALIBRE_URL_HINT: "Per al Calibre, afegiu /opds a la URL"
STR_PERCENT_STEP_HINT: "Esquerra/Dreta: 1% Amunt/Avall: 10%" STR_PERCENT_STEP_HINT: "Esquerra/Dreta: 1% Amunt/Avall: 10%"
STR_SYNCING_TIME: "S'està sincronitzant el temps..." STR_SYNCING_TIME: "S'està sincronitzant el temps..."
@@ -311,3 +283,9 @@ STR_UPLOAD: "Puja"
STR_BOOK_S_STYLE: "Estil del llibre" STR_BOOK_S_STYLE: "Estil del llibre"
STR_EMBEDDED_STYLE: "Estil incrustat" STR_EMBEDDED_STYLE: "Estil incrustat"
STR_OPDS_SERVER_URL: "URL del servidor OPDS" STR_OPDS_SERVER_URL: "URL del servidor OPDS"
STR_FOOTNOTES: "Notes al peu"
STR_NO_FOOTNOTES: "No hi ha notes al peu en aquesta pàgina"
STR_LINK: "[enllaç]"
STR_SCREENSHOT_BUTTON: "Fes una captura de pantalla"
STR_AUTO_TURN_ENABLED: "Passar automàtic activat: "
STR_AUTO_TURN_PAGES_PER_MIN: "Passar automàtic (pàgines per minut)"
+2 -53
View File
@@ -9,12 +9,10 @@ STR_ENTERING_SLEEP: "Vstup do režimu spánku"
STR_BROWSE_FILES: "Procházet soubory" STR_BROWSE_FILES: "Procházet soubory"
STR_FILE_TRANSFER: "Přenos souborů" STR_FILE_TRANSFER: "Přenos souborů"
STR_SETTINGS_TITLE: "Nastavení" STR_SETTINGS_TITLE: "Nastavení"
STR_CALIBRE_LIBRARY: "Knihovna Calibre"
STR_CONTINUE_READING: "Pokračovat ve čtení" STR_CONTINUE_READING: "Pokračovat ve čtení"
STR_NO_OPEN_BOOK: "Žádná otevřená kniha" STR_NO_OPEN_BOOK: "Žádná otevřená kniha"
STR_START_READING: "Začněte číst níže" STR_START_READING: "Začněte číst níže"
STR_BOOKS: "Knihy" STR_NO_FILES_FOUND: "Nebyly nalezeny žádné soubory"
STR_NO_BOOKS_FOUND: "Žádné knihy nenalezeny"
STR_SELECT_CHAPTER: "Vybrat kapitolu" STR_SELECT_CHAPTER: "Vybrat kapitolu"
STR_NO_CHAPTERS: "Žádné kapitoly" STR_NO_CHAPTERS: "Žádné kapitoly"
STR_END_OF_BOOK: "Konec knihy" STR_END_OF_BOOK: "Konec knihy"
@@ -26,10 +24,6 @@ STR_EMPTY_FILE: "Prázdný soubor"
STR_OUT_OF_BOUNDS: "Mimo hranice" STR_OUT_OF_BOUNDS: "Mimo hranice"
STR_LOADING: "Načítání..." STR_LOADING: "Načítání..."
STR_LOADING_POPUP: "Načítání" STR_LOADING_POPUP: "Načítání"
STR_LOAD_XTC_FAILED: "Nepodařilo se načíst XTC"
STR_LOAD_TXT_FAILED: "Nepodařilo se načíst TXT"
STR_LOAD_EPUB_FAILED: "Nepodařilo se načíst EPUB"
STR_SD_CARD_ERROR: "Chyba SD karty"
STR_WIFI_NETWORKS: "WiFi sítě" STR_WIFI_NETWORKS: "WiFi sítě"
STR_NO_NETWORKS: "Žádné sítě nenalezeny" STR_NO_NETWORKS: "Žádné sítě nenalezeny"
STR_NETWORKS_FOUND: "Nalezeno %zu sítí" STR_NETWORKS_FOUND: "Nalezeno %zu sítí"
@@ -37,14 +31,9 @@ STR_SCANNING: "Skenování..."
STR_CONNECTING: "Připojování..." STR_CONNECTING: "Připojování..."
STR_CONNECTED: "Připojeno!" STR_CONNECTED: "Připojeno!"
STR_CONNECTION_FAILED: "Připojení se nezdařilo" STR_CONNECTION_FAILED: "Připojení se nezdařilo"
STR_CONNECTION_TIMEOUT: "Časový limit připojení"
STR_FORGET_NETWORK: "Zapomenout síť?" STR_FORGET_NETWORK: "Zapomenout síť?"
STR_SAVE_PASSWORD: "Uložit heslo pro příště?" STR_SAVE_PASSWORD: "Uložit heslo pro příště?"
STR_REMOVE_PASSWORD: "Odstranit uložené heslo?"
STR_PRESS_OK_SCAN: "Stiskněte OK pro přeskenování" STR_PRESS_OK_SCAN: "Stiskněte OK pro přeskenování"
STR_PRESS_ANY_CONTINUE: "Pokračujte stiskem libovolné klávesy"
STR_SELECT_HINT: "VLEVO/VPRAVO: Vybrat | OK: Potvrdit"
STR_HOW_CONNECT: "Jak se chcete připojit?"
STR_JOIN_NETWORK: "Připojit se k síti" STR_JOIN_NETWORK: "Připojit se k síti"
STR_CREATE_HOTSPOT: "Vytvořit hotspot" STR_CREATE_HOTSPOT: "Vytvořit hotspot"
STR_JOIN_DESC: "Připojit se k existující síti WiFi" STR_JOIN_DESC: "Připojit se k existující síti WiFi"
@@ -57,27 +46,13 @@ STR_OR_HTTP_PREFIX: "nebo http://"
STR_SCAN_QR_HINT: "nebo naskenujte QR kód telefonem:" STR_SCAN_QR_HINT: "nebo naskenujte QR kód telefonem:"
STR_CALIBRE_WIRELESS: "Calibre Wireless" STR_CALIBRE_WIRELESS: "Calibre Wireless"
STR_CALIBRE_WEB_URL: "URL webu Calibre" STR_CALIBRE_WEB_URL: "URL webu Calibre"
STR_CONNECT_WIRELESS: "Připojit jako bezdrátové zařízení"
STR_NETWORK_LEGEND: "* = Šifrováno | + = Uloženo" STR_NETWORK_LEGEND: "* = Šifrováno | + = Uloženo"
STR_MAC_ADDRESS: "MAC adresa:" STR_MAC_ADDRESS: "MAC adresa:"
STR_CHECKING_WIFI: "Kontrola WiFi..." STR_CHECKING_WIFI: "Kontrola WiFi..."
STR_ENTER_WIFI_PASSWORD: "Zadejte heslo WiFi" STR_ENTER_WIFI_PASSWORD: "Zadejte heslo WiFi"
STR_ENTER_TEXT: "Zadejte text"
STR_TO_PREFIX: "pro" STR_TO_PREFIX: "pro"
STR_CALIBRE_DISCOVERING: "Prozkoumávání Calibre..."
STR_CALIBRE_CONNECTING_TO: "Připojování k"
STR_CALIBRE_CONNECTED_TO: "Připojeno k"
STR_CALIBRE_WAITING_COMMANDS: "Čekám na příkazy…"
STR_CONNECTION_FAILED_RETRYING: "(Připojení se nezdařilo, opakování pokusu)"
STR_CALIBRE_DISCONNECTED: "Calibre odpojeno"
STR_CALIBRE_WAITING_TRANSFER: "Čekání na přenos..."
STR_CALIBRE_TRANSFER_HINT: "Nezdaří-li se přenos, povolte\\n„Ignorovat volné místo“ v Calibre\\nnastavení pluginu SmartDevice."
STR_CALIBRE_RECEIVING: "Příjem:" STR_CALIBRE_RECEIVING: "Příjem:"
STR_CALIBRE_RECEIVED: "Přijato:" STR_CALIBRE_RECEIVED: "Přijato:"
STR_CALIBRE_WAITING_MORE: "Čekání na další..."
STR_CALIBRE_FAILED_CREATE_FILE: "Nepodařilo se vytvořit soubor"
STR_CALIBRE_PASSWORD_REQUIRED: "Vyžadováno heslo"
STR_CALIBRE_TRANSFER_INTERRUPTED: "Přenos přerušen"
STR_CALIBRE_INSTRUCTION_1: "1) Nainstalujte plugin CrossPoint Reader" STR_CALIBRE_INSTRUCTION_1: "1) Nainstalujte plugin CrossPoint Reader"
STR_CALIBRE_INSTRUCTION_2: "2) Buďte ve stejné síti WiFi" STR_CALIBRE_INSTRUCTION_2: "2) Buďte ve stejné síti WiFi"
STR_CALIBRE_INSTRUCTION_3: "3) V Calibre: „Odeslat do zařízení“" STR_CALIBRE_INSTRUCTION_3: "3) V Calibre: „Odeslat do zařízení“"
@@ -88,37 +63,25 @@ STR_CAT_CONTROLS: "Ovládací prvky"
STR_CAT_SYSTEM: "Systém" STR_CAT_SYSTEM: "Systém"
STR_SLEEP_SCREEN: "Obrazovka spánku" STR_SLEEP_SCREEN: "Obrazovka spánku"
STR_SLEEP_COVER_MODE: "Obrazovka spánku Režim krytu" STR_SLEEP_COVER_MODE: "Obrazovka spánku Režim krytu"
STR_STATUS_BAR: "Stavový řádek"
STR_HIDE_BATTERY: "Skrýt baterii %" STR_HIDE_BATTERY: "Skrýt baterii %"
STR_EXTRA_SPACING: "Extra mezery mezi odstavci" STR_EXTRA_SPACING: "Extra mezery mezi odstavci"
STR_TEXT_AA: "Vyhlazování textu" STR_TEXT_AA: "Vyhlazování textu"
STR_SHORT_PWR_BTN: "Krátké stisknutí tlačítka napájení" STR_SHORT_PWR_BTN: "Krátké stisknutí tlačítka napájení"
STR_ORIENTATION: "Orientace čtení" STR_ORIENTATION: "Orientace čtení"
STR_FRONT_BTN_LAYOUT: "Rozvržení předních tlačítek"
STR_SIDE_BTN_LAYOUT: "Rozvržení bočních tlačítek (čtečka)" STR_SIDE_BTN_LAYOUT: "Rozvržení bočních tlačítek (čtečka)"
STR_LONG_PRESS_SKIP: "Dlouhé stisknutí Přeskočit kapitolu" STR_LONG_PRESS_SKIP: "Dlouhé stisknutí Přeskočit kapitolu"
STR_FONT_FAMILY: "Rodina písem čtečky" STR_FONT_FAMILY: "Rodina písem čtečky"
STR_EXT_READER_FONT: "Písmo externí čtečky"
STR_EXT_CHINESE_FONT: "Písmo čtečky"
STR_EXT_UI_FONT: "Písmo rozhraní"
STR_FONT_SIZE: "Velikost písma rozhraní" STR_FONT_SIZE: "Velikost písma rozhraní"
STR_LINE_SPACING: "Řádkování čtečky" STR_LINE_SPACING: "Řádkování čtečky"
STR_ASCII_LETTER_SPACING: "Mezery písmen ASCII"
STR_ASCII_DIGIT_SPACING: "Mezery číslic ASCII"
STR_CJK_SPACING: "Mezery CJK"
STR_COLOR_MODE: "Režim barev"
STR_SCREEN_MARGIN: "Okraj obrazovky čtečky" STR_SCREEN_MARGIN: "Okraj obrazovky čtečky"
STR_PARA_ALIGNMENT: "Zarovnání odstavců čtečky" STR_PARA_ALIGNMENT: "Zarovnání odstavců čtečky"
STR_HYPHENATION: "Dělení slov" STR_HYPHENATION: "Dělení slov"
STR_TIME_TO_SLEEP: "Čas do uspání" STR_TIME_TO_SLEEP: "Čas do uspání"
STR_REFRESH_FREQ: "Frekvence obnovení" STR_REFRESH_FREQ: "Frekvence obnovení"
STR_CALIBRE_SETTINGS: "Nastavení Calibre"
STR_KOREADER_SYNC: "KOReaderu Sync" STR_KOREADER_SYNC: "KOReaderu Sync"
STR_CHECK_UPDATES: "Zkontrolovat aktualizace" STR_CHECK_UPDATES: "Zkontrolovat aktualizace"
STR_LANGUAGE: "Jazyk" STR_LANGUAGE: "Jazyk"
STR_SELECT_WALLPAPER: "Vybrat tapetu"
STR_CLEAR_READING_CACHE: "Vymazat mezipaměť čtení" STR_CLEAR_READING_CACHE: "Vymazat mezipaměť čtení"
STR_CALIBRE: "Calibre"
STR_USERNAME: "Uživatelské jméno" STR_USERNAME: "Uživatelské jméno"
STR_PASSWORD: "Heslo" STR_PASSWORD: "Heslo"
STR_SYNC_SERVER_URL: "URL synch. serveru" STR_SYNC_SERVER_URL: "URL synch. serveru"
@@ -153,8 +116,6 @@ STR_COVER: "Obálka"
STR_NONE_OPT: "Žádný" STR_NONE_OPT: "Žádný"
STR_FIT: "Přizpůsobit" STR_FIT: "Přizpůsobit"
STR_CROP: "Oříznout" STR_CROP: "Oříznout"
STR_NO_PROGRESS: "Žádný postup"
STR_FULL_OPT: "Plná"
STR_NEVER: "Nikdy" STR_NEVER: "Nikdy"
STR_IN_READER: "Ve čtečce" STR_IN_READER: "Ve čtečce"
STR_ALWAYS: "Vždy" STR_ALWAYS: "Vždy"
@@ -165,9 +126,6 @@ STR_PORTRAIT: "Na výšku"
STR_LANDSCAPE_CW: "Na šířku po směru hod. ručiček" STR_LANDSCAPE_CW: "Na šířku po směru hod. ručiček"
STR_INVERTED: "Invertovaný" STR_INVERTED: "Invertovaný"
STR_LANDSCAPE_CCW: "Na šířku proti směru hod. ručiček" STR_LANDSCAPE_CCW: "Na šířku proti směru hod. ručiček"
STR_FRONT_LAYOUT_BCLR: "Zpět, Potvrdit, Vlevo, Vpravo"
STR_FRONT_LAYOUT_LRBC: "Vlevo, Vpravo, Zpět, Potvrdit"
STR_FRONT_LAYOUT_LBCR: "Vlevo, Zpět, Potvrdit, Vpravo"
STR_PREV_NEXT: "Předchozí/Další" STR_PREV_NEXT: "Předchozí/Další"
STR_NEXT_PREV: "Další/Předchozí" STR_NEXT_PREV: "Další/Předchozí"
STR_BOOKERLY: "Bookerly" STR_BOOKERLY: "Bookerly"
@@ -204,8 +162,6 @@ STR_NO_UPDATE: "Žádná aktualizace k dispozici"
STR_UPDATE_FAILED: "Aktualizace selhala" STR_UPDATE_FAILED: "Aktualizace selhala"
STR_UPDATE_COMPLETE: "Aktualizace dokončena" STR_UPDATE_COMPLETE: "Aktualizace dokončena"
STR_POWER_ON_HINT: "Stiskněte a podržte tlačítko napájení pro opětovné zapnutí" STR_POWER_ON_HINT: "Stiskněte a podržte tlačítko napájení pro opětovné zapnutí"
STR_EXTERNAL_FONT: "Externí písmo"
STR_BUILTIN_DISABLED: "Vestavěné (Zakázáno)"
STR_NO_ENTRIES: "Žádné položky nenalezeny" STR_NO_ENTRIES: "Žádné položky nenalezeny"
STR_DOWNLOADING: "Stahování..." STR_DOWNLOADING: "Stahování..."
STR_DOWNLOAD_FAILED: "Stahování selhalo" STR_DOWNLOAD_FAILED: "Stahování selhalo"
@@ -216,7 +172,6 @@ STR_FETCH_FEED_FAILED: "Načtení kanálu se nezdařilo"
STR_PARSE_FEED_FAILED: "Analyzování kanálu se nezdařilo" STR_PARSE_FEED_FAILED: "Analyzování kanálu se nezdařilo"
STR_NETWORK_PREFIX: "Síť:" STR_NETWORK_PREFIX: "Síť:"
STR_IP_ADDRESS_PREFIX: "IP adresa:" STR_IP_ADDRESS_PREFIX: "IP adresa:"
STR_SCAN_QR_WIFI_HINT: "nebo naskenujte QR kód telefonem pro připojení k WiFi."
STR_ERROR_GENERAL_FAILURE: "Chyba: Obecná chyba" STR_ERROR_GENERAL_FAILURE: "Chyba: Obecná chyba"
STR_ERROR_NETWORK_NOT_FOUND: "Chyba: Síť nenalezena" STR_ERROR_NETWORK_NOT_FOUND: "Chyba: Síť nenalezena"
STR_ERROR_CONNECTION_TIMEOUT: "Chyba: Časový limit připojení" STR_ERROR_CONNECTION_TIMEOUT: "Chyba: Časový limit připojení"
@@ -224,7 +179,6 @@ STR_SD_CARD: "SD karta"
STR_BACK: "« Zpět" STR_BACK: "« Zpět"
STR_EXIT: "« Konec" STR_EXIT: "« Konec"
STR_HOME: "« Domů" STR_HOME: "« Domů"
STR_SAVE: "« Uložit"
STR_SELECT: "Vybrat" STR_SELECT: "Vybrat"
STR_TOGGLE: "Přepnout" STR_TOGGLE: "Přepnout"
STR_CONFIRM: "Potvrdit" STR_CONFIRM: "Potvrdit"
@@ -242,8 +196,6 @@ STR_DIR_LEFT: "Vlevo"
STR_DIR_RIGHT: "Vpravo" STR_DIR_RIGHT: "Vpravo"
STR_DIR_UP: "Nahoru" STR_DIR_UP: "Nahoru"
STR_DIR_DOWN: "Dolů" STR_DIR_DOWN: "Dolů"
STR_CAPS_ON: "PÍSMO"
STR_CAPS_OFF: "písmo"
STR_OK_BUTTON: "OK" STR_OK_BUTTON: "OK"
STR_SLEEP_COVER_FILTER: "Filtr obrazovky spánku" STR_SLEEP_COVER_FILTER: "Filtr obrazovky spánku"
STR_FILTER_CONTRAST: "Kontrast" STR_FILTER_CONTRAST: "Kontrast"
@@ -255,7 +207,6 @@ STR_SUNLIGHT_FADING_FIX: "Oprava blednutí na slunci"
STR_REMAP_FRONT_BUTTONS: "Přemapovat přední tlačítka" STR_REMAP_FRONT_BUTTONS: "Přemapovat přední tlačítka"
STR_OPDS_BROWSER: "Prohlížeč OPDS" STR_OPDS_BROWSER: "Prohlížeč OPDS"
STR_COVER_CUSTOM: "Obálka + Vlastní" STR_COVER_CUSTOM: "Obálka + Vlastní"
STR_RECENTS: "Nedávné"
STR_MENU_RECENT_BOOKS: "Nedávné knihy" STR_MENU_RECENT_BOOKS: "Nedávné knihy"
STR_NO_RECENT_BOOKS: "Žádné nedávné knihy" STR_NO_RECENT_BOOKS: "Žádné nedávné knihy"
STR_CALIBRE_DESC: "Používat přenosy bezdrátových zařízení Calibre" STR_CALIBRE_DESC: "Používat přenosy bezdrátových zařízení Calibre"
@@ -279,12 +230,10 @@ STR_GO_TO_PERCENT: "Přejít na %"
STR_GO_HOME_BUTTON: "Přejít Domů" STR_GO_HOME_BUTTON: "Přejít Domů"
STR_SYNC_PROGRESS: "Průběh synchronizace" STR_SYNC_PROGRESS: "Průběh synchronizace"
STR_DELETE_CACHE: "Smazat mezipaměť knihy" STR_DELETE_CACHE: "Smazat mezipaměť knihy"
STR_DELETE: "Smazat"
STR_CHAPTER_PREFIX: "Kapitola:" STR_CHAPTER_PREFIX: "Kapitola:"
STR_PAGES_SEPARATOR: "stránek |" STR_PAGES_SEPARATOR: "stránek |"
STR_BOOK_PREFIX: "Kniha:" STR_BOOK_PREFIX: "Kniha:"
STR_KBD_SHIFT: "shift"
STR_KBD_SHIFT_CAPS: "SHIFT"
STR_KBD_LOCK: "ZÁMEK"
STR_CALIBRE_URL_HINT: "Pro Calibre přidejte /opds do URL adresy" STR_CALIBRE_URL_HINT: "Pro Calibre přidejte /opds do URL adresy"
STR_PERCENT_STEP_HINT: "Vlevo/Vpravo: 1 % Nahoru/Dolů: 10 %" STR_PERCENT_STEP_HINT: "Vlevo/Vpravo: 1 % Nahoru/Dolů: 10 %"
STR_SYNCING_TIME: "Čas synchronizace..." STR_SYNCING_TIME: "Čas synchronizace..."
+1 -59
View File
@@ -9,12 +9,10 @@ STR_ENTERING_SLEEP: "Går i hvile"
STR_BROWSE_FILES: "Gennemsøg filer" STR_BROWSE_FILES: "Gennemsøg filer"
STR_FILE_TRANSFER: "Filoverførelse" STR_FILE_TRANSFER: "Filoverførelse"
STR_SETTINGS_TITLE: "Indstillinger" STR_SETTINGS_TITLE: "Indstillinger"
STR_CALIBRE_LIBRARY: "Calibre bibliotek"
STR_CONTINUE_READING: "Fortsæt med at læse" STR_CONTINUE_READING: "Fortsæt med at læse"
STR_NO_OPEN_BOOK: "Ingen åben bog" STR_NO_OPEN_BOOK: "Ingen åben bog"
STR_START_READING: "Start læsning nedenfor" STR_START_READING: "Start læsning nedenfor"
STR_BOOKS: "Bøger" STR_NO_FILES_FOUND: "Ingen filer fundet"
STR_NO_BOOKS_FOUND: "Ingen bøger fundet"
STR_SELECT_CHAPTER: "Vælg kapitel" STR_SELECT_CHAPTER: "Vælg kapitel"
STR_NO_CHAPTERS: "Ingen kapitler" STR_NO_CHAPTERS: "Ingen kapitler"
STR_END_OF_BOOK: "Bogen er færdig" STR_END_OF_BOOK: "Bogen er færdig"
@@ -26,10 +24,6 @@ STR_EMPTY_FILE: "Tom fil"
STR_OUT_OF_BOUNDS: "Uden for grænsen" STR_OUT_OF_BOUNDS: "Uden for grænsen"
STR_LOADING: "Indlæser..." STR_LOADING: "Indlæser..."
STR_LOADING_POPUP: "Indlæser" STR_LOADING_POPUP: "Indlæser"
STR_LOAD_XTC_FAILED: "Mislykkedes at indlæse XTC"
STR_LOAD_TXT_FAILED: "Mislykkedes at indlæse TXT"
STR_LOAD_EPUB_FAILED: "Mislykkedes at indlæse EPUB"
STR_SD_CARD_ERROR: "SD kort fejl"
STR_WIFI_NETWORKS: "Trådløse netværk" STR_WIFI_NETWORKS: "Trådløse netværk"
STR_NO_NETWORKS: "Intet netværk fundet" STR_NO_NETWORKS: "Intet netværk fundet"
STR_NETWORKS_FOUND: "%zu netværk fundet" STR_NETWORKS_FOUND: "%zu netværk fundet"
@@ -37,14 +31,9 @@ STR_SCANNING: "Skanner..."
STR_CONNECTING: "Forbinder..." STR_CONNECTING: "Forbinder..."
STR_CONNECTED: "Forbundet!" STR_CONNECTED: "Forbundet!"
STR_CONNECTION_FAILED: "Forbindelsen mislykkedes" STR_CONNECTION_FAILED: "Forbindelsen mislykkedes"
STR_CONNECTION_TIMEOUT: "Forbindelses timeout"
STR_FORGET_NETWORK: "Glem netværk?" STR_FORGET_NETWORK: "Glem netværk?"
STR_SAVE_PASSWORD: "Gem adgangskode til næste gang?" STR_SAVE_PASSWORD: "Gem adgangskode til næste gang?"
STR_REMOVE_PASSWORD: "Fjern gemt adgangskode?"
STR_PRESS_OK_SCAN: "Tryk OK for at scanne igen" STR_PRESS_OK_SCAN: "Tryk OK for at scanne igen"
STR_PRESS_ANY_CONTINUE: "Tryk på en knap for at fortsætte"
STR_SELECT_HINT: "VENSTRE/HØJRE: Vælg | OK: Bekræft"
STR_HOW_CONNECT: "Hvordan vil du oprette forbindelse?"
STR_JOIN_NETWORK: "Tilslut netværk" STR_JOIN_NETWORK: "Tilslut netværk"
STR_CREATE_HOTSPOT: "Opret Hotspot" STR_CREATE_HOTSPOT: "Opret Hotspot"
STR_JOIN_DESC: "Opret forbindelse til et eksisterende WiFi-netværk" STR_JOIN_DESC: "Opret forbindelse til et eksisterende WiFi-netværk"
@@ -57,27 +46,13 @@ STR_OR_HTTP_PREFIX: "eller http://"
STR_SCAN_QR_HINT: "eller scan QR-kode med din telefon:" STR_SCAN_QR_HINT: "eller scan QR-kode med din telefon:"
STR_CALIBRE_WIRELESS: "Calibre Wireless" STR_CALIBRE_WIRELESS: "Calibre Wireless"
STR_CALIBRE_WEB_URL: "Calibre Web URL" STR_CALIBRE_WEB_URL: "Calibre Web URL"
STR_CONNECT_WIRELESS: "Opret forbindelse som trådløs enhed"
STR_NETWORK_LEGEND: "* = Krypteret | + = Gemt" STR_NETWORK_LEGEND: "* = Krypteret | + = Gemt"
STR_MAC_ADDRESS: "MAC-adresse:" STR_MAC_ADDRESS: "MAC-adresse:"
STR_CHECKING_WIFI: "Tjekker WiFi..." STR_CHECKING_WIFI: "Tjekker WiFi..."
STR_ENTER_WIFI_PASSWORD: "Indtast WiFi-adgangskode" STR_ENTER_WIFI_PASSWORD: "Indtast WiFi-adgangskode"
STR_ENTER_TEXT: "Indtast tekst"
STR_TO_PREFIX: "til " STR_TO_PREFIX: "til "
STR_CALIBRE_DISCOVERING: "Opdager Calibre..."
STR_CALIBRE_CONNECTING_TO: "Forbinder til "
STR_CALIBRE_CONNECTED_TO: "Forbundet til "
STR_CALIBRE_WAITING_COMMANDS: "Venter på kommandoer..."
STR_CONNECTION_FAILED_RETRYING: "(Forbindelsen mislykkedes, prøver igen)"
STR_CALIBRE_DISCONNECTED: "Calibre afbrudt"
STR_CALIBRE_WAITING_TRANSFER: "Venter på overførelse..."
STR_CALIBRE_TRANSFER_HINT: "Hvis overførslen mislykkes, aktiver\\n'Ignorer ledig plads' i Calibres\\nSmartDevice-plugin-indstillinger."
STR_CALIBRE_RECEIVING: "Modtager: " STR_CALIBRE_RECEIVING: "Modtager: "
STR_CALIBRE_RECEIVED: "Modtaget: " STR_CALIBRE_RECEIVED: "Modtaget: "
STR_CALIBRE_WAITING_MORE: "Venter på mere..."
STR_CALIBRE_FAILED_CREATE_FILE: "Kunne ikke oprette fil"
STR_CALIBRE_PASSWORD_REQUIRED: "Adgangskode påkrævet"
STR_CALIBRE_TRANSFER_INTERRUPTED: "Overførelse afbrudt"
STR_CALIBRE_INSTRUCTION_1: "1) Installer CrossPoint Reader-plugin" STR_CALIBRE_INSTRUCTION_1: "1) Installer CrossPoint Reader-plugin"
STR_CALIBRE_INSTRUCTION_2: "2) Vær på det samme WiFi-netværk" STR_CALIBRE_INSTRUCTION_2: "2) Vær på det samme WiFi-netværk"
STR_CALIBRE_INSTRUCTION_3: "3) I Calibre: \"Send til enhed\"" STR_CALIBRE_INSTRUCTION_3: "3) I Calibre: \"Send til enhed\""
@@ -88,37 +63,25 @@ STR_CAT_CONTROLS: "Brugerflade"
STR_CAT_SYSTEM: "System" STR_CAT_SYSTEM: "System"
STR_SLEEP_SCREEN: "Hvile-skærm" STR_SLEEP_SCREEN: "Hvile-skærm"
STR_SLEEP_COVER_MODE: "Hvile-skærm omslag-tilstand" STR_SLEEP_COVER_MODE: "Hvile-skærm omslag-tilstand"
STR_STATUS_BAR: "Statuslinje"
STR_HIDE_BATTERY: "Skjul batteri %" STR_HIDE_BATTERY: "Skjul batteri %"
STR_EXTRA_SPACING: "Ekstra afsnitsafstand" STR_EXTRA_SPACING: "Ekstra afsnitsafstand"
STR_TEXT_AA: "Tekst Anti-Aliasing" STR_TEXT_AA: "Tekst Anti-Aliasing"
STR_SHORT_PWR_BTN: "Kort tryk på tænd/sluk-knap" STR_SHORT_PWR_BTN: "Kort tryk på tænd/sluk-knap"
STR_ORIENTATION: "Læseretning" STR_ORIENTATION: "Læseretning"
STR_FRONT_BTN_LAYOUT: "Knaplayout foran"
STR_SIDE_BTN_LAYOUT: "Knaplayout på siden (læser)" STR_SIDE_BTN_LAYOUT: "Knaplayout på siden (læser)"
STR_LONG_PRESS_SKIP: "Langt tryk spring kapitel over" STR_LONG_PRESS_SKIP: "Langt tryk spring kapitel over"
STR_FONT_FAMILY: "Læser skrifttype" STR_FONT_FAMILY: "Læser skrifttype"
STR_EXT_READER_FONT: "Ekstern læserskrifttype"
STR_EXT_CHINESE_FONT: "Læserskrifttype"
STR_EXT_UI_FONT: "Brugergrænseflade skrifttype"
STR_FONT_SIZE: "Brugergrænseflade skriftstørrelse" STR_FONT_SIZE: "Brugergrænseflade skriftstørrelse"
STR_LINE_SPACING: "Linjeafstand" STR_LINE_SPACING: "Linjeafstand"
STR_ASCII_LETTER_SPACING: "ASCII bogstavafstand"
STR_ASCII_DIGIT_SPACING: "ASCII cifreafstand"
STR_CJK_SPACING: "CJK afstand"
STR_COLOR_MODE: "Farvetilstand"
STR_SCREEN_MARGIN: "Skærmmargen" STR_SCREEN_MARGIN: "Skærmmargen"
STR_PARA_ALIGNMENT: "Afsnitsjustering" STR_PARA_ALIGNMENT: "Afsnitsjustering"
STR_HYPHENATION: "Orddeling" STR_HYPHENATION: "Orddeling"
STR_TIME_TO_SLEEP: "Tid til hvile" STR_TIME_TO_SLEEP: "Tid til hvile"
STR_REFRESH_FREQ: "Opdateringsfrekvens" STR_REFRESH_FREQ: "Opdateringsfrekvens"
STR_CALIBRE_SETTINGS: "Calibre-indstillinger"
STR_KOREADER_SYNC: "KOReader Sync" STR_KOREADER_SYNC: "KOReader Sync"
STR_CHECK_UPDATES: "Søg efter opdateringer" STR_CHECK_UPDATES: "Søg efter opdateringer"
STR_LANGUAGE: "Sprog" STR_LANGUAGE: "Sprog"
STR_SELECT_WALLPAPER: "Vælg baggrundsbillede"
STR_CLEAR_READING_CACHE: "Ryd læsecache" STR_CLEAR_READING_CACHE: "Ryd læsecache"
STR_CALIBRE: "Calibre"
STR_USERNAME: "Brugernavn" STR_USERNAME: "Brugernavn"
STR_PASSWORD: "Adgangskode" STR_PASSWORD: "Adgangskode"
STR_SYNC_SERVER_URL: "Synkroniseringsserver-URL" STR_SYNC_SERVER_URL: "Synkroniseringsserver-URL"
@@ -153,8 +116,6 @@ STR_COVER: "Omslag"
STR_NONE_OPT: "Ingen" STR_NONE_OPT: "Ingen"
STR_FIT: "Tilpas" STR_FIT: "Tilpas"
STR_CROP: "Beskær" STR_CROP: "Beskær"
STR_NO_PROGRESS: "Ingen fremskridt"
STR_FULL_OPT: "Fuld"
STR_NEVER: "Aldrig" STR_NEVER: "Aldrig"
STR_IN_READER: "I læseren" STR_IN_READER: "I læseren"
STR_ALWAYS: "Altid" STR_ALWAYS: "Altid"
@@ -165,9 +126,6 @@ STR_PORTRAIT: "Portræt"
STR_LANDSCAPE_CW: "Liggende med uret" STR_LANDSCAPE_CW: "Liggende med uret"
STR_INVERTED: "Inverteret" STR_INVERTED: "Inverteret"
STR_LANDSCAPE_CCW: "Liggende mod uret" STR_LANDSCAPE_CCW: "Liggende mod uret"
STR_FRONT_LAYOUT_BCLR: "Bck, Cnfrm, Lft, Rght"
STR_FRONT_LAYOUT_LRBC: "Lft, Rght, Bck, Cnfrm"
STR_FRONT_LAYOUT_LBCR: "Lft, Bck, Cnfrm, Rght"
STR_PREV_NEXT: "Forrige/Næste" STR_PREV_NEXT: "Forrige/Næste"
STR_NEXT_PREV: "Næste/Forrige" STR_NEXT_PREV: "Næste/Forrige"
STR_BOOKERLY: "Bookerly" STR_BOOKERLY: "Bookerly"
@@ -204,8 +162,6 @@ STR_NO_UPDATE: "Ingen opdatering tilgængelig"
STR_UPDATE_FAILED: "Opdatering mislykkedes" STR_UPDATE_FAILED: "Opdatering mislykkedes"
STR_UPDATE_COMPLETE: "Opdatering færdig" STR_UPDATE_COMPLETE: "Opdatering færdig"
STR_POWER_ON_HINT: "Hold tænd/sluk-knappen nede for at tænde igen" STR_POWER_ON_HINT: "Hold tænd/sluk-knappen nede for at tænde igen"
STR_EXTERNAL_FONT: "Ekstern skrifttype"
STR_BUILTIN_DISABLED: "Indbygget (deaktiveret)"
STR_NO_ENTRIES: "Ingen poster fundet" STR_NO_ENTRIES: "Ingen poster fundet"
STR_DOWNLOADING: "Downloader..." STR_DOWNLOADING: "Downloader..."
STR_DOWNLOAD_FAILED: "Download mislykkedes" STR_DOWNLOAD_FAILED: "Download mislykkedes"
@@ -216,7 +172,6 @@ STR_FETCH_FEED_FAILED: "Kunne ikke hente feed"
STR_PARSE_FEED_FAILED: "Kunne ikke fortolke feed" STR_PARSE_FEED_FAILED: "Kunne ikke fortolke feed"
STR_NETWORK_PREFIX: "Netværk: " STR_NETWORK_PREFIX: "Netværk: "
STR_IP_ADDRESS_PREFIX: "IP-adresse: " STR_IP_ADDRESS_PREFIX: "IP-adresse: "
STR_SCAN_QR_WIFI_HINT: "eller scan QR-kode med din telefon for at oprette forbindelse til WiFi."
STR_ERROR_GENERAL_FAILURE: "Fejl: Generel fejl" STR_ERROR_GENERAL_FAILURE: "Fejl: Generel fejl"
STR_ERROR_NETWORK_NOT_FOUND: "Fejl: Netværk ikke fundet" STR_ERROR_NETWORK_NOT_FOUND: "Fejl: Netværk ikke fundet"
STR_ERROR_CONNECTION_TIMEOUT: "Fejl: Forbindelses timeout" STR_ERROR_CONNECTION_TIMEOUT: "Fejl: Forbindelses timeout"
@@ -224,7 +179,6 @@ STR_SD_CARD: "SD-kort"
STR_BACK: "« Tilbage" STR_BACK: "« Tilbage"
STR_EXIT: "« Afslut" STR_EXIT: "« Afslut"
STR_HOME: "« Hjem" STR_HOME: "« Hjem"
STR_SAVE: "« Gem"
STR_SELECT: "Vælg" STR_SELECT: "Vælg"
STR_TOGGLE: "Skift" STR_TOGGLE: "Skift"
STR_CONFIRM: "Bekræft" STR_CONFIRM: "Bekræft"
@@ -237,22 +191,14 @@ STR_YES: "Ja"
STR_NO: "Nej" STR_NO: "Nej"
STR_STATE_ON: "TÆNDT" STR_STATE_ON: "TÆNDT"
STR_STATE_OFF: "SLUKKET" STR_STATE_OFF: "SLUKKET"
STR_SET: "Indstil"
STR_NOT_SET: "Ikke indstillet" STR_NOT_SET: "Ikke indstillet"
STR_DIR_LEFT: "Venstre" STR_DIR_LEFT: "Venstre"
STR_DIR_RIGHT: "Højre" STR_DIR_RIGHT: "Højre"
STR_DIR_UP: "Op" STR_DIR_UP: "Op"
STR_DIR_DOWN: "Ned" STR_DIR_DOWN: "Ned"
STR_CAPS_ON: "CAPS"
STR_CAPS_OFF: "caps"
STR_OK_BUTTON: "OK" STR_OK_BUTTON: "OK"
STR_ON_MARKER: "[ON]"
STR_SLEEP_COVER_FILTER: "Hvile-skærm omslag-filter" STR_SLEEP_COVER_FILTER: "Hvile-skærm omslag-filter"
STR_FILTER_CONTRAST: "Kontrast" STR_FILTER_CONTRAST: "Kontrast"
STR_STATUS_BAR_FULL_PERCENT: "Fuld m/ procent"
STR_STATUS_BAR_FULL_BOOK: "Fuld m/ boglinje"
STR_STATUS_BAR_BOOK_ONLY: "Kun boglinje"
STR_STATUS_BAR_FULL_CHAPTER: "Fuld m/ kapitellinje"
STR_UI_THEME: "Brugergrænseflade tema" STR_UI_THEME: "Brugergrænseflade tema"
STR_THEME_CLASSIC: "Klassisk" STR_THEME_CLASSIC: "Klassisk"
STR_THEME_LYRA: "Lyra" STR_THEME_LYRA: "Lyra"
@@ -261,7 +207,6 @@ STR_SUNLIGHT_FADING_FIX: "Sollysfading-rettelse"
STR_REMAP_FRONT_BUTTONS: "Omtildel frontknapper" STR_REMAP_FRONT_BUTTONS: "Omtildel frontknapper"
STR_OPDS_BROWSER: "OPDS Browser" STR_OPDS_BROWSER: "OPDS Browser"
STR_COVER_CUSTOM: "Omslag + Brugerdefineret" STR_COVER_CUSTOM: "Omslag + Brugerdefineret"
STR_RECENTS: "Seneste"
STR_MENU_RECENT_BOOKS: "Seneste bøger" STR_MENU_RECENT_BOOKS: "Seneste bøger"
STR_NO_RECENT_BOOKS: "Ingen seneste bøger" STR_NO_RECENT_BOOKS: "Ingen seneste bøger"
STR_CALIBRE_DESC: "Brug Calibre trådløs enhedsoverførelse" STR_CALIBRE_DESC: "Brug Calibre trådløs enhedsoverførelse"
@@ -288,9 +233,6 @@ STR_DELETE_CACHE: "Slet bogcache"
STR_CHAPTER_PREFIX: "Kapitel: " STR_CHAPTER_PREFIX: "Kapitel: "
STR_PAGES_SEPARATOR: " sider | " STR_PAGES_SEPARATOR: " sider | "
STR_BOOK_PREFIX: "Bog: " STR_BOOK_PREFIX: "Bog: "
STR_KBD_SHIFT: "shift"
STR_KBD_SHIFT_CAPS: "SHIFT"
STR_KBD_LOCK: "LOCK"
STR_CALIBRE_URL_HINT: "Tilføj /opds til din URL for Calibre" STR_CALIBRE_URL_HINT: "Tilføj /opds til din URL for Calibre"
STR_PERCENT_STEP_HINT: "Venstre/Højre: 1% Op/Ned: 10%" STR_PERCENT_STEP_HINT: "Venstre/Højre: 1% Op/Ned: 10%"
STR_SYNCING_TIME: "Synkroniserer tid..." STR_SYNCING_TIME: "Synkroniserer tid..."

Some files were not shown because too many files have changed in this diff Show More