Merge branch 'master' into feat-koysnc-xpath

This commit is contained in:
jpirnay
2026-03-19 16:39:50 +01:00
committed by GitHub
184 changed files with 191169 additions and 203260 deletions
+14 -14
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
+3 -1
View File
@@ -36,6 +36,7 @@ This project is **not affiliated with Xteink**; it's built as a community projec
- [x] Cover sleep screen - [x] Cover sleep screen
- [x] Wifi book upload - [x] Wifi book upload
- [x] Wifi OTA updates - [x] Wifi OTA updates
- [x] KOReader Sync integration for cross-device reading progress
- [x] Configurable font, layout, and display options - [x] Configurable font, layout, and display options
- [ ] User provided fonts - [ ] User provided fonts
- [ ] Full UTF support - [ ] Full UTF support
@@ -43,7 +44,8 @@ This project is **not affiliated with Xteink**; it's built as a community projec
Multi-language support: Read EPUBs in various languages, including English, Spanish, French, German, Italian, Portuguese, Russian, Ukrainian, Polish, Swedish, Norwegian, [and more](./USER_GUIDE.md#supported-languages). Multi-language support: Read EPUBs in various languages, including English, Spanish, French, German, Italian, Portuguese, Russian, Ukrainian, Polish, Swedish, Norwegian, [and more](./USER_GUIDE.md#supported-languages).
See [the user guide](./USER_GUIDE.md) for instructions on operating CrossPoint. See [the user guide](./USER_GUIDE.md) for instructions on operating CrossPoint, including the
[KOReader Sync quick setup](./USER_GUIDE.md#365-koreader-sync-quick-setup).
For more details about the scope of the project, see the [SCOPE.md](SCOPE.md) document. For more details about the scope of the project, see the [SCOPE.md](SCOPE.md) document.
+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
+132 -6
View File
@@ -20,6 +20,7 @@ Welcome to the **CrossPoint** firmware. This guide outlines the hardware control
- [3.6.2 Reader](#362-reader) - [3.6.2 Reader](#362-reader)
- [3.6.3 Controls](#363-controls) - [3.6.3 Controls](#363-controls)
- [3.6.4 System](#364-system) - [3.6.4 System](#364-system)
- [3.6.5 KOReader Sync Quick Setup](#365-koreader-sync-quick-setup)
- [3.7 Sleep Screen](#37-sleep-screen) - [3.7 Sleep Screen](#37-sleep-screen)
- [4. Reading Mode](#4-reading-mode) - [4. Reading Mode](#4-reading-mode)
- [Page Turning](#page-turning) - [Page Turning](#page-turning)
@@ -83,7 +84,8 @@ See [Reading Mode](#4-reading-mode) below for more information.
The Browse Files screen acts as a file and folder browser. 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
@@ -197,15 +199,139 @@ The Settings screen allows you to configure the device's behavior. There are a f
- **Check for updates**: Check for Crosspoint firmware updates over WiFi. - **Check for updates**: Check for Crosspoint firmware updates over WiFi.
- **Language**: Set the system language (see **[Supported Languages](#supported-languages)** for more information). - **Language**: Set the system language (see **[Supported Languages](#supported-languages)** for more information).
### 3.7 Sleep Screen #### 3.6.5 KOReader Sync Quick Setup
You can customize the sleep screen by placing custom images in specific locations on the SD card: CrossPoint can sync reading progress with KOReader-compatible sync servers.
It also interoperates with KOReader apps/devices when they use the same server and credentials.
- **Single Image:** Place a file named `sleep.bmp` in the root directory. ##### Option A: Free Public Server (`sync.koreader.rocks`)
- **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.
1. Register a user once (only if needed):
```bash
USERNAME="user"
PASSWORD="pass"
PASSWORD_MD5="$(printf '%s' "$PASSWORD" | openssl md5 | awk '{print $2}')"
curl -i "https://sync.koreader.rocks/users/create" \
-H "Accept: application/vnd.koreader.v1+json" \
-H "Content-Type: application/json" \
--data "{\"username\":\"$USERNAME\",\"password\":\"$PASSWORD_MD5\"}"
```
Already have KOReader Sync credentials? Skip registration; basic sync only requires using the same existing username/password on all devices.
When this returns `HTTP 402` with `{"code":2002,"message":"Username is already registered."}`, pick a different username or use that existing account.
2. On each CrossPoint device:
- Go to **Settings -> System -> KOReader Sync**.
- Set **Username** and **Password** (enter the plain password; CrossPoint computes MD5 internally, and use the same values on all devices).
- Set **Sync Server URL** to `https://sync.koreader.rocks`, or leave it empty (both use the same default KOReader sync server).
- Run **Authenticate**.
3. While reading, press **Confirm** to open the reader menu, then select **Sync Progress**.
- Choose **Apply Remote** to jump to remote progress.
- Choose **Upload Local** to push current progress.
##### Option B: Self-Hosted Server (Docker Compose)
1. Start a sync server:
```bash
mkdir -p kosync-quickstart
cd kosync-quickstart
cat > compose.yaml <<'YAML'
services:
kosync:
image: koreader/kosync:latest
ports:
- "7200:7200"
- "17200:17200"
volumes:
- ./data/redis:/var/lib/redis
environment:
- ENABLE_USER_REGISTRATION=true
restart: unless-stopped
YAML
# Docker
docker compose up -d
# Podman (alternative)
podman compose up -d
```
> [!NOTE] > [!NOTE]
> You'll need to set the **Sleep Screen** setting to **Custom** in order to use these images. > `ENABLE_USER_REGISTRATION=true` is convenient for first setup. After creating your users, set it to `false` (or remove it) to avoid unexpected registrations.
2. Verify the server:
```bash
curl -H "Accept: application/vnd.koreader.v1+json" "http://<server-ip>:17200/healthcheck"
# Expected: {"state":"OK"}
```
3. Register a user once.
CrossPoint authenticates against KOReader Sync (`koreader/kosync`) using an MD5 key, so register using the MD5 of your password:
> [!WARNING]
> Sending a reusable MD5-derived password over plain HTTP is insecure.
> Create unique sync-only credentials and do not reuse main account passwords.
> Prefer `https://<server-ip>:7200` whenever traffic leaves a fully trusted LAN or when using untrusted networks.
> Use `curl -k` only for self-signed certificate testing.
```bash
USERNAME="user"
PASSWORD="pass"
PASSWORD_MD5="$(printf '%s' "$PASSWORD" | openssl md5 | awk '{print $2}')"
curl -i "http://<server-ip>:17200/users/create" \
-H "Accept: application/vnd.koreader.v1+json" \
-H "Content-Type: application/json" \
--data "{\"username\":\"$USERNAME\",\"password\":\"$PASSWORD_MD5\"}"
```
If this returns `HTTP 402` with `{"code":2002,"message":"Username is already registered."}`, the account already exists.
4. On each CrossPoint device:
- Go to **Settings -> System -> KOReader Sync**.
- Set **Username** and **Password** (enter the plain password; CrossPoint computes MD5 internally, and use the same values on all devices).
- Set **Sync Server URL** to `http://<server-ip>:17200`.
- Run **Authenticate**.
If you use the HTTPS listener, use `https://<server-ip>:7200` (`curl -k` only for self-signed certificate testing).
5. While reading, press **Confirm** to open the reader menu, then select **Sync Progress**.
- Choose **Apply Remote** to jump to remote progress.
- Choose **Upload Local** to push current progress.
### 3.7 Sleep Screen
The **Sleep Screen** setting controls what is displayed when the device goes to sleep:
| Mode | Behavior |
|------|----------|
| **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. |
#### Cover settings
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:
+472
View File
@@ -0,0 +1,472 @@
# Activity & ActivityManager Migration Guide
This document explains the refactoring from the original per-activity render task model to the centralized `ActivityManager` introduced in [PR #1016](https://github.com/crosspoint-reader/crosspoint-reader/pull/1016). It covers the architectural differences, what changed for activity authors, and the FreeRTOS task and locking model that underpins the system.
## Overview of Changes
| Aspect | Old Model | New Model |
|--------|-----------|-----------|
| Render task | One per activity (8KB stack each) | Single shared task in `ActivityManager` |
| Render mutex | Per-activity `renderingMutex` | Single global mutex in `ActivityManager` |
| `RenderLock` | Inner class of `Activity` | Standalone class, acquires global mutex |
| Subactivities | `ActivityWithSubactivity` base class | Activity stack managed by `ActivityManager` |
| Navigation | Free functions in `main.cpp` | `activityManager.goHome()`, `goToReader()`, etc. |
| Subactivity results | Callback lambdas stored in parent | `startActivityForResult()` / `setResult()` / `finish()` |
| `requestUpdate()` | Notifies activity's own render task | Delegates to `ActivityManager` (immediate or deferred) |
## Architecture
### Old Model: Per-Activity Render Tasks
Each activity created its own FreeRTOS render task on entry and destroyed it on exit:
```text
┌─────────────────────────────────────────────────────────┐
│ Main Task (Arduino loop) │
│ ┌───────────────────────────────────────────────────┐ │
│ │ currentActivity->loop() │ │
│ │ ├── handle input │ │
│ │ ├── update state (under RenderLock) │ │
│ │ └── requestUpdate() ──notify──► Render Task │ │
│ │ (per-activity)│ │
│ │ 8KB stack │ │
│ │ owns mutex │ │
│ └───────────────────────────────────────────────────┘ │
│ │
│ ActivityWithSubactivity: │
│ ┌──────────────┐ ┌──────────────┐ │
│ │ Parent │────►│ SubActivity │ │
│ │ (has render │ │ (has own │ │
│ │ task) │ │ render task) │ │
│ └──────────────┘ └──────────────┘ │
└─────────────────────────────────────────────────────────┘
```
Problems with this approach:
- **8KB per render task**: Each activity allocated an 8KB FreeRTOS stack for its render task, even though only one renders at a time
- **Dangerous deletion patterns**: `exitActivity()` + `enterNewActivity()` in callbacks led to `delete this` situations where the caller was destroyed while its code was still on the stack
- **Subactivity coupling**: Parents stored callbacks to child results, creating tight coupling and lifetime hazards
### New Model: Centralized ActivityManager
A single `ActivityManager` owns the render task and manages an activity stack:
```text
┌──────────────────────────────────────────────────────────┐
│ Main Task (Arduino loop) │
│ │
│ activityManager.loop() │
│ │ │
│ ├── currentActivity->loop() │
│ │ ├── handle input │
│ │ ├── update state (under RenderLock) │
│ │ └── requestUpdate() │
│ │ │
│ ├── process pending actions (Push / Pop / Replace) │
│ │ │
│ └── if requestedUpdate: ──notify──► Render Task │
│ (single, shared) │
│ 8KB stack │
│ global mutex │
│ │
│ Activity Stack: │
│ ┌──────────┬──────────┬──────────┐ ┌──────────┐ │
│ │ Home │ Settings │ Wifi │ │ Keyboard │ │
│ │ (stack) │ (stack) │ (stack) │ │ (current)│ │
│ └──────────┴──────────┴──────────┘ └──────────┘ │
│ stackActivities[] currentActivity │
└──────────────────────────────────────────────────────────┘
```
## Migration Checklist
### 1. Change Base Class
If your activity extended `ActivityWithSubactivity`, change it to extend `Activity`:
```cpp
// BEFORE
class MyActivity final : public ActivityWithSubactivity {
MyActivity(GfxRenderer& r, MappedInputManager& m, std::function<void()> goBack)
: ActivityWithSubactivity("MyActivity", r, m), goBack(goBack) {}
};
// AFTER
class MyActivity final : public Activity {
MyActivity(GfxRenderer& r, MappedInputManager& m)
: Activity("MyActivity", r, m) {}
};
```
Note that navigation callbacks like `goBack` are no longer stored — use `finish()` or `activityManager.goHome()` instead.
### 2. Replace Navigation Functions
The free functions `exitActivity()` / `enterNewActivity()` in `main.cpp` are gone. Use `ActivityManager` methods:
```cpp
// BEFORE (in main.cpp or via stored callbacks)
exitActivity();
enterNewActivity(new SettingsActivity(renderer, mappedInput, onGoHome));
// AFTER (from any Activity method)
activityManager.goToSettings();
// or for arbitrary navigation:
activityManager.replaceActivity(std::make_unique<MyActivity>(renderer, mappedInput));
```
`replaceActivity()` destroys the current activity and clears the stack. Use it for top-level navigation (home, reader, settings, etc.).
### 3. Replace Subactivity Pattern
The `enterNewActivity()` / `exitActivity()` subactivity pattern is replaced by a stack with typed results:
```cpp
// BEFORE
void MyActivity::launchWifi() {
enterNewActivity(new WifiSelectionActivity(renderer, mappedInput,
[this](bool connected) { onWifiDone(connected); }));
}
// Child calls: onComplete(true); // triggers callback, which may call exitActivity()
// AFTER
void MyActivity::launchWifi() {
startActivityForResult(
std::make_unique<WifiSelectionActivity>(renderer, mappedInput),
[this](const ActivityResult& result) {
if (result.isCancelled) return;
auto& wifi = std::get<WifiResult>(result.data);
onWifiDone(wifi.connected);
});
}
// Child calls:
// setResult(WifiResult{.connected = true, .ssid = ssid});
// finish();
```
Key differences:
- **`startActivityForResult()`** pushes the current activity onto the stack and launches the child
- **`setResult()`** stores a typed result on the child activity
- **`finish()`** signals the manager to pop the child, call the result handler, and resume the parent
- The parent is never deleted during this process — it's safely stored on the stack
### 4. Update `render()` Signature
The `RenderLock` type changed from `Activity::RenderLock` (inner class) to standalone `RenderLock`:
```cpp
// BEFORE
void render(Activity::RenderLock&&) override;
// AFTER
void render(RenderLock&&) override;
```
Include `RenderLock.h` if not transitively included via `Activity.h`.
### 5. Update `onEnter()` / `onExit()`
Activities no longer create or destroy render tasks:
```cpp
// BEFORE
void MyActivity::onEnter() {
Activity::onEnter(); // created render task + logged
// ... allocate resources
requestUpdate();
}
void MyActivity::onExit() {
// ... free resources
Activity::onExit(); // acquired RenderLock, deleted render task
}
// AFTER
void MyActivity::onEnter() {
Activity::onEnter(); // just logs
// ... allocate resources
requestUpdate();
}
void MyActivity::onExit() {
// ... free resources
Activity::onExit(); // just logs
}
```
The render task lifecycle is handled entirely by `ActivityManager::begin()`.
### 6. Update `requestUpdate()` Calls
The signature changed to accept an `immediate` flag:
```cpp
// BEFORE
void requestUpdate(); // always immediate notification to per-activity render task
// AFTER
void requestUpdate(bool immediate = false);
// immediate=false (default): deferred until end of current loop iteration
// immediate=true: sends notification to render task right away
```
**When to use `immediate`**: Almost never. Deferred updates are batched — if `loop()` triggers multiple state changes that each call `requestUpdate()`, only one render happens. Use `immediate` only when you need the render to start before the current function returns (e.g., before a blocking network call).
**`requestUpdateAndWait()`**: Blocks the calling task until the render completes. Use sparingly — it's designed for cases where you need the screen to reflect new state before proceeding (e.g., showing "Checking for update..." before calling a network API).
### 7. Remove Stored Navigation Callbacks
Old activities often stored `std::function` callbacks for navigation:
```cpp
// BEFORE
class SettingsActivity : public ActivityWithSubactivity {
const std::function<void()> goBack; // stored callback
const std::function<void()> goHome; // stored callback
public:
SettingsActivity(GfxRenderer& r, MappedInputManager& m,
std::function<void()> goBack, std::function<void()> goHome)
: ActivityWithSubactivity("Settings", r, m), goBack(goBack), goHome(goHome) {}
};
// AFTER
class SettingsActivity : public Activity {
public:
SettingsActivity(GfxRenderer& r, MappedInputManager& m)
: Activity("Settings", r, m) {}
// Use finish() to go back, activityManager.goHome() to go home
};
```
This removes `std::function` overhead (~2-4KB per unique signature) and eliminates lifetime risks from captured `this` pointers.
## Technical Details
### FreeRTOS Task Model
The firmware runs on an ESP32-C3, a single-core RISC-V microcontroller. FreeRTOS provides cooperative and preemptive multitasking on this single core — only one task executes at any moment, and the scheduler switches between tasks at yield points (blocking calls, `vTaskDelay`, `taskYIELD`) or when a tick interrupt promotes a higher-priority task.
There are two tasks relevant to the activity system:
```text
┌──────────────────────┐ ┌──────────────────────────┐
│ Main Task │ │ Render Task │
│ (Arduino loop) │ │ (ActivityManager-owned) │
│ Priority: 1 │ │ Priority: 1 │
│ │ │ │
│ Runs: │ │ Runs: │
│ - gpio.update() │ │ - ulTaskNotifyTake() │
│ - activity->loop() │ │ (blocks until notified)│
│ - pending actions │ │ - RenderLock (mutex) │
│ - sleep/power mgmt │ │ - activity->render() │
│ - requestUpdate →────┼─────┼─► xTaskNotify() │
│ (end of loop) │ │ │
└──────────────────────┘ └──────────────────────────┘
```
Both tasks run at priority 1. Since the ESP32-C3 is single-core, they alternate execution: the main task runs `loop()`, then at the end of the loop iteration, notifies the render task if an update was requested. The render task wakes, acquires the mutex, calls `render()`, releases the mutex, and blocks again.
Do not use `xTaskCreate` inside activities. If you have a use case that seems to require a background task, open a discussion to propose a lifecycle-aware `Worker` abstraction first.
### The Render Mutex and RenderLock
A single FreeRTOS mutex (`renderingMutex`) protects shared state between `loop()` and `render()`. Since these run on different tasks, any state read by `render()` and written by `loop()` must be guarded.
`RenderLock` is an RAII wrapper:
```cpp
// Standalone class (not tied to any specific activity)
class RenderLock {
bool isLocked = false;
public:
explicit RenderLock(); // acquires activityManager.renderingMutex
explicit RenderLock(Activity&); // same — Activity& param kept for compatibility
~RenderLock(); // releases mutex if still held
void unlock(); // early release
};
```
**Usage patterns:**
```cpp
// In loop(): protect state mutations that render() reads
void MyActivity::loop() {
if (somethingChanged) {
RenderLock lock;
state = newState; // safe — render() can't run while lock is held
}
requestUpdate(); // trigger render after lock is released
}
// In render(): lock is passed in, held for duration of render
void MyActivity::render(RenderLock&&) {
// Lock is held — safe to read shared state
renderer.clearScreen();
renderer.drawText(..., stateString, ...);
renderer.displayBuffer();
// Lock released when RenderLock destructor runs
}
```
**Critical rule**: Never call `requestUpdateAndWait()` while holding a `RenderLock`. The render task needs the mutex to call `render()`, so holding it while waiting for the render to complete is a deadlock:
```text
Main Task Render Task
────────── ───────────
RenderLock lock; (blocked on mutex)
requestUpdateAndWait();
→ notify render task
→ block waiting for
render to complete → wakes up
→ tries to acquire mutex
→ DEADLOCK: main holds mutex,
waits for render; render
waits for mutex
```
### requestUpdate() vs requestUpdateAndWait()
```text
requestUpdate(false) requestUpdate(true)
───────────────── ─────────────────
Sets flag only. Notifies render task
Render happens after immediately.
loop() returns and Render may start
ActivityManager checks before the calling
the flag. function returns.
(Does NOT wait for
render to complete.)
requestUpdateAndWait()
──────────────────────
Notifies render task AND
blocks calling task until
render is done. Uses
FreeRTOS direct-to-task
notification on the
caller's task handle.
```
`requestUpdateAndWait()` flow in detail:
```text
Calling Task Render Task
──────────── ───────────
requestUpdateAndWait()
├─ assert: not render task
├─ assert: not holding RenderLock
├─ store waitingTaskHandle
├─ xTaskNotify(renderTask) → wakes render task
└─ ulTaskNotifyTake() ─┐
(blocked) │ RenderLock lock;
│ activity->render();
│ // render complete
│ taskENTER_CRITICAL
│ waiter = waitingTaskHandle
│ waitingTaskHandle = nullptr
│ taskEXIT_CRITICAL
│ xTaskNotify(waiter) ───┐
│ │
┌──────────────────────┘ │
│ (woken by notification) ◄────────────────────────┘
└─ return
```
### Activity Lifecycle Under ActivityManager
```text
activityManager.replaceActivity(make_unique<MyActivity>(...))
╔═══════════════════════════════════════════════════╗
║ pendingAction = Replace ║
║ pendingActivity = MyActivity ║
╚═══════════════════════════════════════════════════╝
▼ (next loop iteration)
ActivityManager::loop()
├── currentActivity->loop() // old activity's last loop
├── process pending action:
│ ├── RenderLock lock;
│ ├── oldActivity->onExit() // cleanup under lock
│ ├── delete oldActivity
│ ├── clear stack
│ ├── currentActivity = MyActivity
│ ├── lock.unlock()
│ └── MyActivity->onEnter() // init new activity
└── if requestedUpdate:
└── notify render task
```
For push/pop (subactivity) navigation:
```text
Parent calls: startActivityForResult(make_unique<Child>(...), handler)
╔══════════════════════════════════════╗
║ pendingAction = Push ║
║ pendingActivity = Child ║
║ parent->resultHandler = handler ║
╚══════════════════════════════════════╝
▼ (next loop iteration)
├── Parent moved to stackActivities[]
├── currentActivity = Child
└── Child->onEnter()
... child runs ...
Child calls: setResult(MyResult{...}); finish();
╔══════════════════════════════════════╗
║ pendingAction = Pop ║
║ child->result = MyResult{...} ║
╚══════════════════════════════════════╝
▼ (next loop iteration)
├── result = child->result
├── Child->onExit(); delete Child
├── currentActivity = Parent (popped from stack)
├── Parent->resultHandler(result)
└── requestUpdate() // automatic re-render for parent
```
### Common Pitfalls
**Calling `finish()` and continuing to access `this`**: `finish()` sets `pendingAction = Pop` but does not immediately destroy the activity. The activity is destroyed on the next `ActivityManager::loop()` iteration. It's safe to access member variables after `finish()` within the same function, but don't rely on the activity surviving past the current `loop()` call.
**Modifying shared state without `RenderLock`**: If `render()` reads a variable and `loop()` writes it, the write must be under a `RenderLock`. Without it, `render()` could see a half-written value (e.g., a partially updated string or struct).
**Creating background tasks that outlive the activity**: Any FreeRTOS task created in `onEnter()` must be deleted in `onExit()` before the activity is destroyed. The `ActivityManager` does not track or clean up background tasks.
**Holding `RenderLock` across blocking calls**: The render task is blocked on the mutex while you hold the lock. Keep critical sections short — acquire, mutate state, release, then do blocking work.
```cpp
// WRONG — blocks render for the entire network call
void MyActivity::doNetworkStuff() {
RenderLock lock;
state = LOADING;
auto result = http.get(url); // blocks for seconds with lock held
state = DONE;
}
// CORRECT — release lock before blocking
void MyActivity::doNetworkStuff() {
{
RenderLock lock;
state = LOADING;
}
requestUpdate(true); // render "Loading..." immediately, before we block
auto result = http.get(url); // lock is not held
{
RenderLock lock;
state = DONE;
}
requestUpdate();
}
```
+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
--- ---
+6 -1
View File
@@ -1,6 +1,6 @@
# Translators # Translators
Below is a list of users and languages CrossPoint may support in the future. Below is a list of users and languages CrossPoint may support in the future.
Note because a language is below does not mean there is official support for the language at this time. Note because a language is below does not mean there is official support for the language at this time.
## Contributing ## Contributing
@@ -20,6 +20,9 @@ If you'd like to add your name to this list, please open a PR adding yourself an
## Portuguese (Brazil) ## Portuguese (Brazil)
- [yagofarias](https://github.com/yagofarias) - [yagofarias](https://github.com/yagofarias)
## Portuguese (Portugal)
- [victordomingos](https://github.com/victordomingos)
## Italian ## Italian
- [andreaturchet](https://github.com/andreaturchet) - [andreaturchet](https://github.com/andreaturchet)
- [fragolinux](https://github.com/fragolinux) - [fragolinux](https://github.com/fragolinux)
@@ -32,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)
+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;
+415 -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() {
free(pageBuffer);
pageBuffer = nullptr;
free(pageGlyphs);
pageGlyphs = nullptr;
pageFont = nullptr;
pageGlyphCount = 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,412 @@ 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 first (populated by prewarmCache)
if (pageBuffer && pageFont == fontData && pageGlyphCount > 0) {
int left = 0, right = pageGlyphCount - 1;
while (left <= right) {
int mid = left + (right - left) / 2;
if (pageGlyphs[mid].glyphIndex == glyphIndex) {
if (pageGlyphs[mid].bufferOffset != UINT32_MAX) {
stats.cacheHits++;
stats.getBitmapTimeUs += micros() - tStart;
return &pageBuffer[pageGlyphs[mid].bufferOffset];
}
break; // Not extracted during prewarm; fall through to hot-group path
}
if (pageGlyphs[mid].glyphIndex < glyphIndex)
left = mid + 1;
else
right = mid - 1;
}
}
// 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) {
freePageBuffer();
if (!fontData || !fontData->groups || !utf8Text) return 0;
// 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
pageBuffer = static_cast<uint8_t*>(malloc(totalBytes));
pageGlyphs = static_cast<PageGlyphEntry*>(malloc(glyphCount * sizeof(PageGlyphEntry)));
if (!pageBuffer || !pageGlyphs) {
LOG_ERR("FDC", "Failed to allocate page buffer (%u bytes, %u glyphs)", totalBytes, glyphCount);
freePageBuffer();
return glyphCount;
}
stats.pageBufferBytes = totalBytes;
stats.pageGlyphsBytes = glyphCount * sizeof(PageGlyphEntry);
pageFont = fontData;
pageGlyphCount = glyphCount;
// Initialize lookup entries (bufferOffset = UINT32_MAX means not yet extracted)
for (uint16_t i = 0; i < glyphCount; i++) {
pageGlyphs[i] = {neededGlyphs[i], UINT32_MAX, 0};
}
// Sort by glyphIndex for binary search in getBitmap()
for (uint16_t i = 1; i < glyphCount; i++) {
PageGlyphEntry key = pageGlyphs[i];
int j = i - 1;
while (j >= 0 && pageGlyphs[j].glyphIndex > key.glyphIndex) {
pageGlyphs[j + 1] = pageGlyphs[j];
j--;
}
pageGlyphs[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 pageGlyphs to find if glyph i is needed
int left = 0, right = (int)pageGlyphCount - 1;
while (left <= right) {
const int mid = left + (right - left) / 2;
if (pageGlyphs[mid].glyphIndex == i) {
pageGlyphs[mid].alignedOffset = groupAlignedTracker[gpPos];
break;
}
if (pageGlyphs[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)pageGlyphCount - 1;
while (left <= right) {
const int mid = left + (right - left) / 2;
if (pageGlyphs[mid].glyphIndex == glyphI) {
pageGlyphs[mid].alignedOffset = alignedOff;
break;
}
if (pageGlyphs[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 < pageGlyphCount; i++) {
if (pageGlyphs[i].bufferOffset != UINT32_MAX) continue; // already extracted
if (getGroupIndex(fontData, pageGlyphs[i].glyphIndex) != groupIdx) continue;
const EpdGlyph& glyph = fontData->glyph[pageGlyphs[i].glyphIndex];
compactSingleGlyph(&tempBuf[pageGlyphs[i].alignedOffset], &pageBuffer[writeOffset], glyph.width, glyph.height);
pageGlyphs[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();
} }
+58 -19
View File
@@ -2,39 +2,78 @@
#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;
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: flat array of prewarmed glyph bitmaps with sorted lookup
uint16_t getGroupIndex(const EpdFontData* fontData, uint16_t glyphIndex); struct PageGlyphEntry {
CacheEntry* findInCache(const EpdFontData* fontData, uint16_t groupIndex); uint32_t glyphIndex;
CacheEntry* findEvictionCandidate(); uint32_t bufferOffset;
bool decompressGroup(const EpdFontData* fontData, uint16_t groupIndex, CacheEntry* entry); uint32_t alignedOffset; // byte-aligned offset within its decompressed group (set during prewarm pre-scan)
};
uint8_t* pageBuffer = nullptr;
const EpdFontData* pageFont = nullptr;
PageGlyphEntry* pageGlyphs = nullptr;
uint16_t pageGlyphCount = 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);
+63 -14
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;
} }
@@ -84,6 +87,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);
@@ -92,11 +96,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) {
file.close(); file.close();
LOG_ERR("SCT", "Deserialization failed: Parameters do not match"); LOG_ERR("SCT", "Deserialization failed: Parameters do not match");
clearCache(); clearCache();
@@ -129,7 +135,7 @@ bool Section::clearCache() const {
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";
@@ -179,7 +185,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
@@ -201,7 +207,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();
@@ -234,10 +240,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();
@@ -250,7 +266,7 @@ std::unique_ptr<Page> Section::loadPageFromSectionFile() {
return nullptr; return nullptr;
} }
file.seek(HEADER_SIZE - sizeof(uint32_t)); file.seek(HEADER_SIZE - sizeof(uint32_t) * 2);
uint32_t lutOffset; uint32_t lutOffset;
serialization::readPod(file, lutOffset); serialization::readPod(file, lutOffset);
file.seek(lutOffset + sizeof(uint32_t) * currentPage); file.seek(lutOffset + sizeof(uint32_t) * currentPage);
@@ -262,3 +278,36 @@ std::unique_ptr<Page> Section::loadPageFromSectionFile() {
file.close(); file.close();
return page; return page;
} }
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 "Epub.h" #include "Epub.h"
@@ -16,7 +18,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:
@@ -30,10 +32,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() const; bool clearCache() const;
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");
} }
@@ -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);
@@ -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];
} }
} }
} }
@@ -243,7 +257,14 @@ 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;
}
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 +299,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()) {
@@ -368,6 +388,7 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char*
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");
@@ -505,7 +526,7 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char*
} }
} }
const float emSize = static_cast<float>(self->renderer.getLineHeight(self->fontId)) * self->lineCompression; const float emSize = static_cast<float>(self->renderer.getFontAscenderSize(self->fontId));
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 +759,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 +794,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 +1030,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 +1046,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 -59
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: "Пацв."
@@ -237,22 +191,14 @@ STR_YES: "Так"
STR_NO: "Не" STR_NO: "Не"
STR_STATE_ON: "УКЛ" STR_STATE_ON: "УКЛ"
STR_STATE_OFF: "ВЫКЛ" STR_STATE_OFF: "ВЫКЛ"
STR_SET: "Устаноўлена"
STR_NOT_SET: "Не ўстаноўлена" STR_NOT_SET: "Не ўстаноўлена"
STR_DIR_LEFT: "Улева" 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_ON_MARKER: "[УКЛ]"
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"
@@ -261,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"
@@ -288,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 -55
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,20 +194,33 @@ 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_SET: "Establert"
STR_NOT_SET: "No establert" STR_NOT_SET: "No establert"
STR_DIR_LEFT: "Esquerra" 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_ON_MARKER: "[ON]"
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"
@@ -257,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"
@@ -281,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..."
@@ -313,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 -55
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"
@@ -237,16 +191,12 @@ STR_YES: "Ano"
STR_NO: "Ne" STR_NO: "Ne"
STR_STATE_ON: "ZAP" STR_STATE_ON: "ZAP"
STR_STATE_OFF: "VYP" STR_STATE_OFF: "VYP"
STR_SET: "Nastavit"
STR_NOT_SET: "Nenastaveno" STR_NOT_SET: "Nenastaveno"
STR_DIR_LEFT: "Vlevo" 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_ON_MARKER: "[ZAP]"
STR_SLEEP_COVER_FILTER: "Filtr obrazovky spánku" STR_SLEEP_COVER_FILTER: "Filtr obrazovky spánku"
STR_FILTER_CONTRAST: "Kontrast" STR_FILTER_CONTRAST: "Kontrast"
STR_UI_THEME: "Šablona rozhraní" STR_UI_THEME: "Šablona rozhraní"
@@ -257,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"
@@ -281,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..."
+2 -60
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..."
@@ -317,4 +259,4 @@ STR_UPLOAD: "Upload"
STR_BOOK_S_STYLE: "Bogens stil" STR_BOOK_S_STYLE: "Bogens stil"
STR_EMBEDDED_STYLE: "Indlejret stil" STR_EMBEDDED_STYLE: "Indlejret stil"
STR_OPDS_SERVER_URL: "OPDS Server URL" STR_OPDS_SERVER_URL: "OPDS Server URL"
STR_SCREENSHOT_BUTTON: "Tag skærmbillede" STR_SCREENSHOT_BUTTON: "Tag skærmbillede"
+283
View File
@@ -0,0 +1,283 @@
_language_name: "Nederlands"
_language_code: "NL"
_order: "16"
STR_CROSSPOINT: "CrossPoint"
STR_BOOTING: "OPSTARTEN"
STR_SLEEPING: "SLAAPSTAND"
STR_ENTERING_SLEEP: "Gaat in slaapstand"
STR_BROWSE_FILES: "Bestanden bladeren"
STR_FILE_TRANSFER: "Bestandsoverdracht"
STR_SETTINGS_TITLE: "Instellingen"
STR_CONTINUE_READING: "Verder lezen"
STR_NO_OPEN_BOOK: "Geen geopend boek"
STR_START_READING: "Begin hieronder met lezen"
STR_NO_FILES_FOUND: "Geen bestanden gevonden"
STR_SELECT_CHAPTER: "Selecteer hoofdstuk"
STR_NO_CHAPTERS: "Geen hoofdstukken"
STR_END_OF_BOOK: "Einde van boek"
STR_EMPTY_CHAPTER: "Leeg hoofdstuk"
STR_INDEXING: "Indexeren"
STR_MEMORY_ERROR: "Geheugenfout"
STR_PAGE_LOAD_ERROR: "Fout bij laden pagina"
STR_EMPTY_FILE: "Leeg bestand"
STR_OUT_OF_BOUNDS: "Buiten bereik"
STR_LOADING: "Laden..."
STR_LOADING_POPUP: "Laden"
STR_WIFI_NETWORKS: "Wifi-netwerken"
STR_NO_NETWORKS: "Geen netwerken gevonden"
STR_NETWORKS_FOUND: "%zu netwerken gevonden"
STR_SCANNING: "Scannen..."
STR_CONNECTING: "Verbinden..."
STR_CONNECTED: "Verbonden!"
STR_CONNECTION_FAILED: "Verbinding mislukt"
STR_FORGET_NETWORK: "Netwerk vergeten?"
STR_SAVE_PASSWORD: "Wachtwoord opslaan voor volgende keer?"
STR_PRESS_OK_SCAN: "Druk op OK om opnieuw te scannen"
STR_JOIN_NETWORK: "Verbind met netwerk"
STR_CREATE_HOTSPOT: "Hotspot maken"
STR_JOIN_DESC: "Verbind met een bestaand wifi-netwerk"
STR_HOTSPOT_DESC: "Maak een wifi-netwerk waar anderen mee kunnen verbinden"
STR_STARTING_HOTSPOT: "Hotspot starten..."
STR_HOTSPOT_MODE: "Hotspot-modus"
STR_CONNECT_WIFI_HINT: "Verbind je apparaat met dit wifi-netwerk"
STR_OPEN_URL_HINT: "Open deze URL in je browser"
STR_OR_HTTP_PREFIX: "of http://"
STR_SCAN_QR_HINT: "of scan de QR-code met je telefoon:"
STR_CALIBRE_WIRELESS: "Calibre Wireless"
STR_CALIBRE_WEB_URL: "Calibre Web URL"
STR_NETWORK_LEGEND: "* = Beveiligd | + = Opgeslagen"
STR_MAC_ADDRESS: "MAC-adres:"
STR_CHECKING_WIFI: "Wifi controleren..."
STR_ENTER_WIFI_PASSWORD: "Voer wifi-wachtwoord in"
STR_TO_PREFIX: "met "
STR_CALIBRE_RECEIVING: "Bezig met ontvangen: "
STR_CALIBRE_RECEIVED: "Ontvangen: "
STR_CALIBRE_INSTRUCTION_1: "1) Installeer CrossPoint Reader plugin"
STR_CALIBRE_INSTRUCTION_2: "2) Gebruik hetzelfde wifi-netwerk"
STR_CALIBRE_INSTRUCTION_3: "3) In Calibre: \"Send to device\""
STR_CALIBRE_INSTRUCTION_4: "\"Houd dit scherm open tijdens verzenden\""
STR_CAT_DISPLAY: "Scherm"
STR_CAT_READER: "Lezer"
STR_CAT_CONTROLS: "Bediening"
STR_CAT_SYSTEM: "Systeem"
STR_SLEEP_SCREEN: "Slaapscherm"
STR_SLEEP_COVER_MODE: "Slaapscherm omslag-modus"
STR_HIDE_BATTERY: "Batterij % verbergen"
STR_EXTRA_SPACING: "Extra regelafstand alinea"
STR_TEXT_AA: "Tekst Anti-Aliasing"
STR_SHORT_PWR_BTN: "Korte klik aan/uit-knop"
STR_ORIENTATION: "Leesstand"
STR_SIDE_BTN_LAYOUT: "Indeling zijknoppen (lezer)"
STR_LONG_PRESS_SKIP: "Hoofdstuk overslaan (lang indrukken)"
STR_FONT_FAMILY: "Lettertype lezer"
STR_FONT_SIZE: "Lettergrootte lezer"
STR_LINE_SPACING: "Regelafstand lezer"
STR_SCREEN_MARGIN: "Schermmarge lezer"
STR_PARA_ALIGNMENT: "Uitlijning alinea lezer"
STR_HYPHENATION: "Woordafbreking"
STR_TIME_TO_SLEEP: "Tijd tot slaapstand"
STR_REFRESH_FREQ: "Verversingsfrequentie"
STR_KOREADER_SYNC: "KOReader Sync"
STR_CHECK_UPDATES: "Controleren op updates"
STR_LANGUAGE: "Taal"
STR_CLEAR_READING_CACHE: "Leescache wissen"
STR_USERNAME: "Gebruikersnaam"
STR_PASSWORD: "Wachtwoord"
STR_SYNC_SERVER_URL: "Sync-server URL"
STR_DOCUMENT_MATCHING: "Documentkoppeling"
STR_AUTHENTICATE: "Authenticatie"
STR_KOREADER_USERNAME: "KOReader gebruikersnaam"
STR_KOREADER_PASSWORD: "KOReader wachtwoord"
STR_FILENAME: "Bestandsnaam"
STR_BINARY: "Binair"
STR_SET_CREDENTIALS_FIRST: "Stel eerst inloggegevens in"
STR_WIFI_CONN_FAILED: "Wifi-verbinding mislukt"
STR_AUTHENTICATING: "Authenticeren..."
STR_AUTH_SUCCESS: "Authenticatie geslaagd!"
STR_KOREADER_AUTH: "KOReader-authenticatie"
STR_SYNC_READY: "KOReader sync is klaar voor gebruik"
STR_AUTH_FAILED: "Authenticatie mislukt"
STR_DONE: "Klaar"
STR_CLEAR_CACHE_WARNING_1: "Dit wist alle gecachte boekgegevens."
STR_CLEAR_CACHE_WARNING_2: "Alle leesvoortgang gaat verloren!"
STR_CLEAR_CACHE_WARNING_3: "Boeken moeten opnieuw worden geïndexeerd"
STR_CLEAR_CACHE_WARNING_4: "wanneer ze weer worden geopend."
STR_CLEARING_CACHE: "Cache wissen..."
STR_CACHE_CLEARED: "Cache gewist"
STR_ITEMS_REMOVED: "items verwijderd"
STR_FAILED_LOWER: "mislukt"
STR_CLEAR_CACHE_FAILED: "Cache wissen mislukt"
STR_CHECK_SERIAL_OUTPUT: "Check seriële output voor details"
STR_DARK: "Donker"
STR_LIGHT: "Licht"
STR_CUSTOM: "Aangepast"
STR_COVER: "Omslag"
STR_NONE_OPT: "Geen"
STR_FIT: "Passend"
STR_CROP: "Bijsnijden"
STR_NEVER: "Nooit"
STR_IN_READER: "In lezer"
STR_ALWAYS: "Altijd"
STR_IGNORE: "Negeren"
STR_SLEEP: "Slaap"
STR_PAGE_TURN: "Pagina omslaan"
STR_PORTRAIT: "Staand"
STR_LANDSCAPE_CW: "Liggend (rechtsom)"
STR_INVERTED: "Omgekeerd"
STR_LANDSCAPE_CCW: "Liggend (linksom)"
STR_PREV_NEXT: "Vorige/Volgende"
STR_NEXT_PREV: "Volgende/Vorige"
STR_BOOKERLY: "Bookerly"
STR_NOTO_SANS: "Noto Sans"
STR_OPEN_DYSLEXIC: "Open Dyslexic"
STR_SMALL: "Klein"
STR_MEDIUM: "Gemiddeld"
STR_LARGE: "Groot"
STR_X_LARGE: "Extra groot"
STR_TIGHT: "Smal"
STR_NORMAL: "Normaal"
STR_WIDE: "Breed"
STR_JUSTIFY: "Uitvullen"
STR_ALIGN_LEFT: "Links"
STR_CENTER: "Centreren"
STR_ALIGN_RIGHT: "Rechts"
STR_MIN_1: "1 min"
STR_MIN_5: "5 min"
STR_MIN_10: "10 min"
STR_MIN_15: "15 min"
STR_MIN_30: "30 min"
STR_PAGES_1: "1 pagina"
STR_PAGES_5: "5 pagina's"
STR_PAGES_10: "10 pagina's"
STR_PAGES_15: "15 pagina's"
STR_PAGES_30: "30 pagina's"
STR_UPDATE: "Update"
STR_CHECKING_UPDATE: "Controleren op update..."
STR_NEW_UPDATE: "Nieuwe update beschikbaar!"
STR_CURRENT_VERSION: "Huidige versie: "
STR_NEW_VERSION: "Nieuwe versie: "
STR_UPDATING: "Updaten..."
STR_NO_UPDATE: "Geen update beschikbaar"
STR_UPDATE_FAILED: "Update mislukt"
STR_UPDATE_COMPLETE: "Update voltooid"
STR_POWER_ON_HINT: "Houd de aan/uit-knop ingedrukt om in te schakelen"
STR_NO_ENTRIES: "Geen items gevonden"
STR_DOWNLOADING: "Downloaden..."
STR_DOWNLOAD_FAILED: "Download mislukt"
STR_ERROR_MSG: "Fout:"
STR_UNNAMED: "Naamloos"
STR_NO_SERVER_URL: "Geen server-URL ingesteld"
STR_FETCH_FEED_FAILED: "Ophalen feed mislukt"
STR_PARSE_FEED_FAILED: "Verwerken feed mislukt"
STR_NETWORK_PREFIX: "Netwerk: "
STR_IP_ADDRESS_PREFIX: "IP-adres: "
STR_ERROR_GENERAL_FAILURE: "Fout: Algemene fout"
STR_ERROR_NETWORK_NOT_FOUND: "Fout: Netwerk niet gevonden"
STR_ERROR_CONNECTION_TIMEOUT: "Fout: Verbindingstime-out"
STR_SD_CARD: "SD-kaart"
STR_BACK: "« Terug"
STR_EXIT: "« Sluit"
STR_HOME: "« Home"
STR_SELECT: "Kies"
STR_TOGGLE: "Wissel"
STR_CONFIRM: "Bevestig"
STR_CANCEL: "Annuleer"
STR_CONNECT: "Verbind"
STR_OPEN: "Open"
STR_DOWNLOAD: "Download"
STR_RETRY: "Opnieuw"
STR_YES: "Ja"
STR_NO: "Nee"
STR_SHOW: "Toon"
STR_HIDE: "Verberg"
STR_STATE_ON: "AAN"
STR_STATE_OFF: "UIT"
STR_NOT_SET: "Niet ingesteld"
STR_DIR_LEFT: "Links"
STR_DIR_RIGHT: "Rechts"
STR_DIR_UP: "Omhoog"
STR_DIR_DOWN: "Omlaag"
STR_OK_BUTTON: "OK"
STR_SLEEP_COVER_FILTER: "Slaapscherm omslag-filter"
STR_FILTER_CONTRAST: "Contrast"
STR_CUSTOMISE_STATUS_BAR: "Statusbalk aanpassen"
STR_CHAPTER_PAGE_COUNT: "Paginanummering hoofdstuk"
STR_BOOK_PROGRESS_PERCENTAGE: "Percentage voortgang boek"
STR_PROGRESS_BAR: "Voortgangsbalk"
STR_PROGRESS_BAR_THICKNESS: "Dikte voortgangsbalk"
STR_PROGRESS_BAR_THIN: "Dun"
STR_PROGRESS_BAR_MEDIUM: "Gemiddeld"
STR_PROGRESS_BAR_THICK: "Dik"
STR_BOOK: "Boek"
STR_CHAPTER: "Hoofdstuk"
STR_EXAMPLE_CHAPTER: "Hoofdstuk 21"
STR_EXAMPLE_BOOK: "Boektitel"
STR_PREVIEW: "Voorbeeld"
STR_TITLE: "Titel"
STR_BATTERY: "Batterij"
STR_UI_THEME: "UI Thema"
STR_THEME_CLASSIC: "Klassiek"
STR_THEME_LYRA: "Lyra"
STR_THEME_LYRA_EXTENDED: "Lyra Uitgebreid"
STR_SUNLIGHT_FADING_FIX: "Zonlicht vervaging fix"
STR_REMAP_FRONT_BUTTONS: "Knoppen voorzijde wijzigen"
STR_OPDS_BROWSER: "OPDS-browser"
STR_COVER_CUSTOM: "Omslag + Aangepast"
STR_MENU_RECENT_BOOKS: "Recente boeken"
STR_NO_RECENT_BOOKS: "Geen recente boeken"
STR_CALIBRE_DESC: "Gebruik Calibre draadloze overdracht"
STR_FORGET_AND_REMOVE: "Netwerk vergeten en wachtwoord verwijderen?"
STR_FORGET_BUTTON: "Vergeet"
STR_CALIBRE_STARTING: "Calibre starten..."
STR_CALIBRE_SETUP: "Installatie"
STR_CALIBRE_STATUS: "Status"
STR_CLEAR_BUTTON: "Wis"
STR_DEFAULT_VALUE: "Standaard"
STR_REMAP_PROMPT: "Druk op een knop voorop voor elke functie"
STR_UNASSIGNED: "Niet toegewezen"
STR_ALREADY_ASSIGNED: "Al toegewezen"
STR_REMAP_RESET_HINT: "Zijknop Omhoog: Standaardindeling herstellen"
STR_REMAP_CANCEL_HINT: "Zijknop Omlaag: Toewijzen annuleren"
STR_HW_BACK_LABEL: "Terug (1e knop)"
STR_HW_CONFIRM_LABEL: "Bevestig (2e knop)"
STR_HW_LEFT_LABEL: "Links (3e knop)"
STR_HW_RIGHT_LABEL: "Rechts (4e knop)"
STR_GO_TO_PERCENT: "Ga naar %"
STR_GO_HOME_BUTTON: "Naar Home"
STR_SYNC_PROGRESS: "Voortgang synchroniseren"
STR_DELETE_CACHE: "Boekcache verwijderen"
STR_DISPLAY_QR: "Pagina als QR tonen"
STR_CHAPTER_PREFIX: "Hoofdstuk: "
STR_PAGES_SEPARATOR: " pagina's | "
STR_BOOK_PREFIX: "Boek: "
STR_CALIBRE_URL_HINT: "Voeg voor Calibre /opds toe aan de URL"
STR_PERCENT_STEP_HINT: "Links/Rechts: 1% Omhoog/Omlaag: 10%"
STR_SYNCING_TIME: "Tijd synchroniseren..."
STR_CALC_HASH: "Document-hash berekenen..."
STR_HASH_FAILED: "Document-hash berekenen mislukt"
STR_FETCH_PROGRESS: "Voortgang ophalen..."
STR_UPLOAD_PROGRESS: "Voortgang uploaden..."
STR_NO_CREDENTIALS_MSG: "Geen inloggegevens ingesteld"
STR_KOREADER_SETUP_HINT: "Stel KOReader-account in bij Instellingen"
STR_PROGRESS_FOUND: "Voortgang gevonden!"
STR_REMOTE_LABEL: "Extern:"
STR_LOCAL_LABEL: "Lokaal:"
STR_PAGE_OVERALL_FORMAT: "Pagina %d, %.2f%% totaal"
STR_PAGE_TOTAL_OVERALL_FORMAT: "Pagina %d/%d, %.2f%% totaal"
STR_DEVICE_FROM_FORMAT: " Van: %s"
STR_APPLY_REMOTE: "Externe voortgang toepassen"
STR_UPLOAD_LOCAL: "Lokale voortgang uploaden"
STR_NO_REMOTE_MSG: "Geen externe voortgang gevonden"
STR_UPLOAD_PROMPT: "Huidige positie uploaden?"
STR_UPLOAD_SUCCESS: "Voortgang geüpload!"
STR_SYNC_FAILED_MSG: "Sync mislukt"
STR_SECTION_PREFIX: "Sectie "
STR_UPLOAD: "Uploaden"
STR_BOOK_S_STYLE: "Stijl van boek"
STR_EMBEDDED_STYLE: "Ingebedde stijl"
STR_OPDS_SERVER_URL: "OPDS-server URL"
STR_FOOTNOTES: "Voetnoten"
STR_NO_FOOTNOTES: "Geen voetnoten op deze pagina"
STR_LINK: "[link]"
STR_SCREENSHOT_BUTTON: "Screenshot maken"
+10 -55
View File
@@ -9,12 +9,10 @@ STR_ENTERING_SLEEP: "Going to sleep"
STR_BROWSE_FILES: "Browse Files" STR_BROWSE_FILES: "Browse Files"
STR_FILE_TRANSFER: "File Transfer" STR_FILE_TRANSFER: "File Transfer"
STR_SETTINGS_TITLE: "Settings" STR_SETTINGS_TITLE: "Settings"
STR_CALIBRE_LIBRARY: "Calibre Library"
STR_CONTINUE_READING: "Continue Reading" STR_CONTINUE_READING: "Continue Reading"
STR_NO_OPEN_BOOK: "No open book" STR_NO_OPEN_BOOK: "No open book"
STR_START_READING: "Start reading below" STR_START_READING: "Start reading below"
STR_BOOKS: "Books" STR_NO_FILES_FOUND: "No files found"
STR_NO_BOOKS_FOUND: "No books found"
STR_SELECT_CHAPTER: "Select Chapter" STR_SELECT_CHAPTER: "Select Chapter"
STR_NO_CHAPTERS: "No chapters" STR_NO_CHAPTERS: "No chapters"
STR_END_OF_BOOK: "End of book" STR_END_OF_BOOK: "End of book"
@@ -26,10 +24,6 @@ STR_EMPTY_FILE: "Empty file"
STR_OUT_OF_BOUNDS: "Out of bounds" STR_OUT_OF_BOUNDS: "Out of bounds"
STR_LOADING: "Loading..." STR_LOADING: "Loading..."
STR_LOADING_POPUP: "Loading" STR_LOADING_POPUP: "Loading"
STR_LOAD_XTC_FAILED: "Failed to load XTC"
STR_LOAD_TXT_FAILED: "Failed to load TXT"
STR_LOAD_EPUB_FAILED: "Failed to load EPUB"
STR_SD_CARD_ERROR: "SD card error"
STR_WIFI_NETWORKS: "WiFi Networks" STR_WIFI_NETWORKS: "WiFi Networks"
STR_NO_NETWORKS: "No networks found" STR_NO_NETWORKS: "No networks found"
STR_NETWORKS_FOUND: "%zu networks found" STR_NETWORKS_FOUND: "%zu networks found"
@@ -37,14 +31,9 @@ STR_SCANNING: "Scanning..."
STR_CONNECTING: "Connecting..." STR_CONNECTING: "Connecting..."
STR_CONNECTED: "Connected!" STR_CONNECTED: "Connected!"
STR_CONNECTION_FAILED: "Connection Failed" STR_CONNECTION_FAILED: "Connection Failed"
STR_CONNECTION_TIMEOUT: "Connection timeout"
STR_FORGET_NETWORK: "Forget Network?" STR_FORGET_NETWORK: "Forget Network?"
STR_SAVE_PASSWORD: "Save password for next time?" STR_SAVE_PASSWORD: "Save password for next time?"
STR_REMOVE_PASSWORD: "Remove saved password?"
STR_PRESS_OK_SCAN: "Press OK to scan again" STR_PRESS_OK_SCAN: "Press OK to scan again"
STR_PRESS_ANY_CONTINUE: "Press any button to continue"
STR_SELECT_HINT: "LEFT/RIGHT: Select | OK: Confirm"
STR_HOW_CONNECT: "How would you like to connect?"
STR_JOIN_NETWORK: "Join a Network" STR_JOIN_NETWORK: "Join a Network"
STR_CREATE_HOTSPOT: "Create Hotspot" STR_CREATE_HOTSPOT: "Create Hotspot"
STR_JOIN_DESC: "Connect to an existing WiFi network" STR_JOIN_DESC: "Connect to an existing WiFi network"
@@ -57,27 +46,13 @@ STR_OR_HTTP_PREFIX: "or http://"
STR_SCAN_QR_HINT: "or scan QR code with your phone:" STR_SCAN_QR_HINT: "or scan QR code with your phone:"
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: "Connect as Wireless Device"
STR_NETWORK_LEGEND: "* = Encrypted | + = Saved" STR_NETWORK_LEGEND: "* = Encrypted | + = Saved"
STR_MAC_ADDRESS: "MAC address:" STR_MAC_ADDRESS: "MAC address:"
STR_CHECKING_WIFI: "Checking WiFi..." STR_CHECKING_WIFI: "Checking WiFi..."
STR_ENTER_WIFI_PASSWORD: "Enter WiFi Password" STR_ENTER_WIFI_PASSWORD: "Enter WiFi Password"
STR_ENTER_TEXT: "Enter Text"
STR_TO_PREFIX: "to " STR_TO_PREFIX: "to "
STR_CALIBRE_DISCOVERING: "Discovering Calibre..."
STR_CALIBRE_CONNECTING_TO: "Connecting to "
STR_CALIBRE_CONNECTED_TO: "Connected to "
STR_CALIBRE_WAITING_COMMANDS: "Waiting for commands..."
STR_CONNECTION_FAILED_RETRYING: "(Connection failed, retrying)"
STR_CALIBRE_DISCONNECTED: "Calibre disconnected"
STR_CALIBRE_WAITING_TRANSFER: "Waiting for transfer..."
STR_CALIBRE_TRANSFER_HINT: "If transfer fails, enable\\n'Ignore free space' in Calibre's\\nSmartDevice plugin settings."
STR_CALIBRE_RECEIVING: "Receiving: " STR_CALIBRE_RECEIVING: "Receiving: "
STR_CALIBRE_RECEIVED: "Received: " STR_CALIBRE_RECEIVED: "Received: "
STR_CALIBRE_WAITING_MORE: "Waiting for more..."
STR_CALIBRE_FAILED_CREATE_FILE: "Failed to create file"
STR_CALIBRE_PASSWORD_REQUIRED: "Password required"
STR_CALIBRE_TRANSFER_INTERRUPTED: "Transfer interrupted"
STR_CALIBRE_INSTRUCTION_1: "1) Install CrossPoint Reader plugin" STR_CALIBRE_INSTRUCTION_1: "1) Install CrossPoint Reader plugin"
STR_CALIBRE_INSTRUCTION_2: "2) Be on the same WiFi network" STR_CALIBRE_INSTRUCTION_2: "2) Be on the same WiFi network"
STR_CALIBRE_INSTRUCTION_3: "3) In Calibre: \"Send to device\"" STR_CALIBRE_INSTRUCTION_3: "3) In Calibre: \"Send to device\""
@@ -88,37 +63,30 @@ STR_CAT_CONTROLS: "Controls"
STR_CAT_SYSTEM: "System" STR_CAT_SYSTEM: "System"
STR_SLEEP_SCREEN: "Sleep Screen" STR_SLEEP_SCREEN: "Sleep Screen"
STR_SLEEP_COVER_MODE: "Sleep Screen Cover Mode" STR_SLEEP_COVER_MODE: "Sleep Screen Cover Mode"
STR_STATUS_BAR: "Status Bar"
STR_HIDE_BATTERY: "Hide Battery %" STR_HIDE_BATTERY: "Hide Battery %"
STR_EXTRA_SPACING: "Extra Paragraph Spacing" STR_EXTRA_SPACING: "Extra Paragraph Spacing"
STR_TEXT_AA: "Text Anti-Aliasing" STR_TEXT_AA: "Text Anti-Aliasing"
STR_IMAGES: "Images"
STR_IMAGES_DISPLAY: "Display"
STR_IMAGES_PLACEHOLDER: "Placeholder"
STR_IMAGES_SUPPRESS: "Suppress"
STR_SHORT_PWR_BTN: "Short Power Button Click" STR_SHORT_PWR_BTN: "Short Power Button Click"
STR_ORIENTATION: "Reading Orientation" STR_ORIENTATION: "Reading Orientation"
STR_FRONT_BTN_LAYOUT: "Front Button Layout"
STR_SIDE_BTN_LAYOUT: "Side Button Layout (reader)" STR_SIDE_BTN_LAYOUT: "Side Button Layout (reader)"
STR_LONG_PRESS_SKIP: "Long-press Chapter Skip" STR_LONG_PRESS_SKIP: "Long-press Chapter Skip"
STR_FONT_FAMILY: "Reader Font Family" STR_FONT_FAMILY: "Reader Font Family"
STR_EXT_READER_FONT: "External Reader Font"
STR_EXT_CHINESE_FONT: "Reader Font"
STR_EXT_UI_FONT: "UI Font"
STR_FONT_SIZE: "Reader Font Size" STR_FONT_SIZE: "Reader Font Size"
STR_LINE_SPACING: "Reader Line Spacing" STR_LINE_SPACING: "Reader Line Spacing"
STR_ASCII_LETTER_SPACING: "ASCII Letter Spacing"
STR_ASCII_DIGIT_SPACING: "ASCII Digit Spacing"
STR_CJK_SPACING: "CJK Spacing"
STR_COLOR_MODE: "Color Mode"
STR_SCREEN_MARGIN: "Reader Screen Margin" STR_SCREEN_MARGIN: "Reader Screen Margin"
STR_PARA_ALIGNMENT: "Reader Paragraph Alignment" STR_PARA_ALIGNMENT: "Reader Paragraph Alignment"
STR_HYPHENATION: "Hyphenation" STR_HYPHENATION: "Hyphenation"
STR_TIME_TO_SLEEP: "Time to Sleep" STR_TIME_TO_SLEEP: "Time to Sleep"
STR_SHOW_HIDDEN_FILES: "Show Hidden Files"
STR_REFRESH_FREQ: "Refresh Frequency" STR_REFRESH_FREQ: "Refresh Frequency"
STR_CALIBRE_SETTINGS: "Calibre Settings"
STR_KOREADER_SYNC: "KOReader Sync" STR_KOREADER_SYNC: "KOReader Sync"
STR_CHECK_UPDATES: "Check for updates" STR_CHECK_UPDATES: "Check for updates"
STR_LANGUAGE: "Language" STR_LANGUAGE: "Language"
STR_SELECT_WALLPAPER: "Select Wallpaper"
STR_CLEAR_READING_CACHE: "Clear Reading Cache" STR_CLEAR_READING_CACHE: "Clear Reading Cache"
STR_CALIBRE: "Calibre"
STR_USERNAME: "Username" STR_USERNAME: "Username"
STR_PASSWORD: "Password" STR_PASSWORD: "Password"
STR_SYNC_SERVER_URL: "Sync Server URL" STR_SYNC_SERVER_URL: "Sync Server URL"
@@ -153,8 +121,6 @@ STR_COVER: "Cover"
STR_NONE_OPT: "None" STR_NONE_OPT: "None"
STR_FIT: "Fit" STR_FIT: "Fit"
STR_CROP: "Crop" STR_CROP: "Crop"
STR_NO_PROGRESS: "No Progress"
STR_FULL_OPT: "Full"
STR_NEVER: "Never" STR_NEVER: "Never"
STR_IN_READER: "In Reader" STR_IN_READER: "In Reader"
STR_ALWAYS: "Always" STR_ALWAYS: "Always"
@@ -165,9 +131,6 @@ STR_PORTRAIT: "Portrait"
STR_LANDSCAPE_CW: "Landscape CW" STR_LANDSCAPE_CW: "Landscape CW"
STR_INVERTED: "Inverted" STR_INVERTED: "Inverted"
STR_LANDSCAPE_CCW: "Landscape CCW" STR_LANDSCAPE_CCW: "Landscape CCW"
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: "Prev/Next" STR_PREV_NEXT: "Prev/Next"
STR_NEXT_PREV: "Next/Prev" STR_NEXT_PREV: "Next/Prev"
STR_BOOKERLY: "Bookerly" STR_BOOKERLY: "Bookerly"
@@ -204,8 +167,6 @@ STR_NO_UPDATE: "No update available"
STR_UPDATE_FAILED: "Update failed" STR_UPDATE_FAILED: "Update failed"
STR_UPDATE_COMPLETE: "Update complete" STR_UPDATE_COMPLETE: "Update complete"
STR_POWER_ON_HINT: "Press and hold power button to turn back on" STR_POWER_ON_HINT: "Press and hold power button to turn back on"
STR_EXTERNAL_FONT: "External Font"
STR_BUILTIN_DISABLED: "Built-in (Disabled)"
STR_NO_ENTRIES: "No entries found" STR_NO_ENTRIES: "No entries found"
STR_DOWNLOADING: "Downloading..." STR_DOWNLOADING: "Downloading..."
STR_DOWNLOAD_FAILED: "Download failed" STR_DOWNLOAD_FAILED: "Download failed"
@@ -216,7 +177,6 @@ STR_FETCH_FEED_FAILED: "Failed to fetch feed"
STR_PARSE_FEED_FAILED: "Failed to parse feed" STR_PARSE_FEED_FAILED: "Failed to parse feed"
STR_NETWORK_PREFIX: "Network: " STR_NETWORK_PREFIX: "Network: "
STR_IP_ADDRESS_PREFIX: "IP Address: " STR_IP_ADDRESS_PREFIX: "IP Address: "
STR_SCAN_QR_WIFI_HINT: "or scan QR code with your phone to connect to Wifi."
STR_ERROR_GENERAL_FAILURE: "Error: General failure" STR_ERROR_GENERAL_FAILURE: "Error: General failure"
STR_ERROR_NETWORK_NOT_FOUND: "Error: Network not found" STR_ERROR_NETWORK_NOT_FOUND: "Error: Network not found"
STR_ERROR_CONNECTION_TIMEOUT: "Error: Connection timeout" STR_ERROR_CONNECTION_TIMEOUT: "Error: Connection timeout"
@@ -224,8 +184,8 @@ STR_SD_CARD: "SD card"
STR_BACK: "« Back" STR_BACK: "« Back"
STR_EXIT: "« Exit" STR_EXIT: "« Exit"
STR_HOME: "« Home" STR_HOME: "« Home"
STR_SAVE: "« Save"
STR_SELECT: "Select" STR_SELECT: "Select"
STR_SELECTED: "Selected"
STR_TOGGLE: "Toggle" STR_TOGGLE: "Toggle"
STR_CONFIRM: "Confirm" STR_CONFIRM: "Confirm"
STR_CANCEL: "Cancel" STR_CANCEL: "Cancel"
@@ -239,16 +199,12 @@ STR_SHOW: "Show"
STR_HIDE: "Hide" STR_HIDE: "Hide"
STR_STATE_ON: "ON" STR_STATE_ON: "ON"
STR_STATE_OFF: "OFF" STR_STATE_OFF: "OFF"
STR_SET: "Set"
STR_NOT_SET: "Not Set" STR_NOT_SET: "Not Set"
STR_DIR_LEFT: "Left" STR_DIR_LEFT: "Left"
STR_DIR_RIGHT: "Right" STR_DIR_RIGHT: "Right"
STR_DIR_UP: "Up" STR_DIR_UP: "Up"
STR_DIR_DOWN: "Down" STR_DIR_DOWN: "Down"
STR_CAPS_ON: "CAPS"
STR_CAPS_OFF: "caps"
STR_OK_BUTTON: "OK" STR_OK_BUTTON: "OK"
STR_ON_MARKER: "[ON]"
STR_SLEEP_COVER_FILTER: "Sleep Screen Cover Filter" STR_SLEEP_COVER_FILTER: "Sleep Screen Cover Filter"
STR_FILTER_CONTRAST: "Contrast" STR_FILTER_CONTRAST: "Contrast"
STR_CUSTOMISE_STATUS_BAR: "Customise Status Bar" STR_CUSTOMISE_STATUS_BAR: "Customise Status Bar"
@@ -274,7 +230,6 @@ STR_SUNLIGHT_FADING_FIX: "Sunlight Fading Fix"
STR_REMAP_FRONT_BUTTONS: "Remap Front Buttons" STR_REMAP_FRONT_BUTTONS: "Remap Front Buttons"
STR_OPDS_BROWSER: "OPDS Browser" STR_OPDS_BROWSER: "OPDS Browser"
STR_COVER_CUSTOM: "Cover + Custom" STR_COVER_CUSTOM: "Cover + Custom"
STR_RECENTS: "Recents"
STR_MENU_RECENT_BOOKS: "Recent Books" STR_MENU_RECENT_BOOKS: "Recent Books"
STR_NO_RECENT_BOOKS: "No recent books" STR_NO_RECENT_BOOKS: "No recent books"
STR_CALIBRE_DESC: "Use Calibre wireless device transfers" STR_CALIBRE_DESC: "Use Calibre wireless device transfers"
@@ -298,13 +253,11 @@ STR_GO_TO_PERCENT: "Go to %"
STR_GO_HOME_BUTTON: "Go Home" STR_GO_HOME_BUTTON: "Go Home"
STR_SYNC_PROGRESS: "Sync Progress" STR_SYNC_PROGRESS: "Sync Progress"
STR_DELETE_CACHE: "Delete Book Cache" STR_DELETE_CACHE: "Delete Book Cache"
STR_DELETE: "Delete"
STR_DISPLAY_QR: "Show page as QR" STR_DISPLAY_QR: "Show page as QR"
STR_CHAPTER_PREFIX: "Chapter: " STR_CHAPTER_PREFIX: "Chapter: "
STR_PAGES_SEPARATOR: " pages | " STR_PAGES_SEPARATOR: " pages | "
STR_BOOK_PREFIX: "Book: " STR_BOOK_PREFIX: "Book: "
STR_KBD_SHIFT: "shift"
STR_KBD_SHIFT_CAPS: "SHIFT"
STR_KBD_LOCK: "LOCK"
STR_CALIBRE_URL_HINT: "For Calibre, add /opds to your URL" STR_CALIBRE_URL_HINT: "For Calibre, add /opds to your URL"
STR_PERCENT_STEP_HINT: "Left/Right: 1% Up/Down: 10%" STR_PERCENT_STEP_HINT: "Left/Right: 1% Up/Down: 10%"
STR_SYNCING_TIME: "Syncing time..." STR_SYNCING_TIME: "Syncing time..."
@@ -335,3 +288,5 @@ STR_FOOTNOTES: "Footnotes"
STR_NO_FOOTNOTES: "No footnotes on this page" STR_NO_FOOTNOTES: "No footnotes on this page"
STR_LINK: "[link]" STR_LINK: "[link]"
STR_SCREENSHOT_BUTTON: "Take screenshot" STR_SCREENSHOT_BUTTON: "Take screenshot"
STR_AUTO_TURN_ENABLED: "Auto Turn Enabled: "
STR_AUTO_TURN_PAGES_PER_MIN: "Auto Turn (Pages Per Minute)"
+1 -59
View File
@@ -9,12 +9,10 @@ STR_ENTERING_SLEEP: "Siirrytään lepotilaan"
STR_BROWSE_FILES: "Selaa tiedostoja" STR_BROWSE_FILES: "Selaa tiedostoja"
STR_FILE_TRANSFER: "Tiedostonsiirto" STR_FILE_TRANSFER: "Tiedostonsiirto"
STR_SETTINGS_TITLE: "Asetukset" STR_SETTINGS_TITLE: "Asetukset"
STR_CALIBRE_LIBRARY: "Calibre-kirjasto"
STR_CONTINUE_READING: "Jatka lukemista" STR_CONTINUE_READING: "Jatka lukemista"
STR_NO_OPEN_BOOK: "Kirjaa ei valittu" STR_NO_OPEN_BOOK: "Kirjaa ei valittu"
STR_START_READING: "Aloita lukeminen" STR_START_READING: "Aloita lukeminen"
STR_BOOKS: "Kirjat" STR_NO_FILES_FOUND: "Ei tiedostoja löytynyt"
STR_NO_BOOKS_FOUND: "Kirjoja ei löytynyt"
STR_SELECT_CHAPTER: "Valitse luku" STR_SELECT_CHAPTER: "Valitse luku"
STR_NO_CHAPTERS: "Ei lukuja" STR_NO_CHAPTERS: "Ei lukuja"
STR_END_OF_BOOK: "Kirjan loppu" STR_END_OF_BOOK: "Kirjan loppu"
@@ -26,10 +24,6 @@ STR_EMPTY_FILE: "Tyhjä tiedosto"
STR_OUT_OF_BOUNDS: "Alueen ulkopuolella" STR_OUT_OF_BOUNDS: "Alueen ulkopuolella"
STR_LOADING: "Ladataan..." STR_LOADING: "Ladataan..."
STR_LOADING_POPUP: "Ladataan" STR_LOADING_POPUP: "Ladataan"
STR_LOAD_XTC_FAILED: "XTC:n lataus epäonnistui"
STR_LOAD_TXT_FAILED: "TXT:n lataus epäonnistui"
STR_LOAD_EPUB_FAILED: "EPUB:n lataus epäonnistui"
STR_SD_CARD_ERROR: "SD-korttivirhe"
STR_WIFI_NETWORKS: "WiFi-verkot" STR_WIFI_NETWORKS: "WiFi-verkot"
STR_NO_NETWORKS: "Verkkoja ei löytynyt" STR_NO_NETWORKS: "Verkkoja ei löytynyt"
STR_NETWORKS_FOUND: "%zu verkkoa löydetty" STR_NETWORKS_FOUND: "%zu verkkoa löydetty"
@@ -37,14 +31,9 @@ STR_SCANNING: "Etsitään..."
STR_CONNECTING: "Yhdistetään..." STR_CONNECTING: "Yhdistetään..."
STR_CONNECTED: "Yhdistetty!" STR_CONNECTED: "Yhdistetty!"
STR_CONNECTION_FAILED: "Yhteys epäonnistui" STR_CONNECTION_FAILED: "Yhteys epäonnistui"
STR_CONNECTION_TIMEOUT: "Yhteys aikakatkaistiin"
STR_FORGET_NETWORK: "Unohda verkko?" STR_FORGET_NETWORK: "Unohda verkko?"
STR_SAVE_PASSWORD: "Tallenna salasana seuraavaa kertaa varten?" STR_SAVE_PASSWORD: "Tallenna salasana seuraavaa kertaa varten?"
STR_REMOVE_PASSWORD: "Poista tallennettu salasana?"
STR_PRESS_OK_SCAN: "Paina OK etsiäksesi uudelleen" STR_PRESS_OK_SCAN: "Paina OK etsiäksesi uudelleen"
STR_PRESS_ANY_CONTINUE: "Paina mitä tahansa painiketta jatkaaksesi"
STR_SELECT_HINT: "VASEN/OIKEA: Valitse | OK: Vahvista"
STR_HOW_CONNECT: "Miten haluat yhdistää?"
STR_JOIN_NETWORK: "Liity verkkoon" STR_JOIN_NETWORK: "Liity verkkoon"
STR_CREATE_HOTSPOT: "Luo yhteyspiste" STR_CREATE_HOTSPOT: "Luo yhteyspiste"
STR_JOIN_DESC: "Yhdistä olemassa olevaan WiFi-verkkoon" STR_JOIN_DESC: "Yhdistä olemassa olevaan WiFi-verkkoon"
@@ -57,27 +46,13 @@ STR_OR_HTTP_PREFIX: "tai http://"
STR_SCAN_QR_HINT: "tai skannaa QR-koodi puhelimellasi:" STR_SCAN_QR_HINT: "tai skannaa QR-koodi puhelimellasi:"
STR_CALIBRE_WIRELESS: "Calibre langaton" STR_CALIBRE_WIRELESS: "Calibre langaton"
STR_CALIBRE_WEB_URL: "Calibre-verkko-osoite" STR_CALIBRE_WEB_URL: "Calibre-verkko-osoite"
STR_CONNECT_WIRELESS: "Yhdistä langattomana laitteena"
STR_NETWORK_LEGEND: "* = Salattu | + = Tallennettu" STR_NETWORK_LEGEND: "* = Salattu | + = Tallennettu"
STR_MAC_ADDRESS: "MAC-osoite:" STR_MAC_ADDRESS: "MAC-osoite:"
STR_CHECKING_WIFI: "Tarkistetaan WiFi..." STR_CHECKING_WIFI: "Tarkistetaan WiFi..."
STR_ENTER_WIFI_PASSWORD: "Syötä WiFi-salasana" STR_ENTER_WIFI_PASSWORD: "Syötä WiFi-salasana"
STR_ENTER_TEXT: "Syötä teksti"
STR_TO_PREFIX: "verkkoon " STR_TO_PREFIX: "verkkoon "
STR_CALIBRE_DISCOVERING: "Etsitään Calibrea..."
STR_CALIBRE_CONNECTING_TO: "Yhdistetään: "
STR_CALIBRE_CONNECTED_TO: "Yhdistetty: "
STR_CALIBRE_WAITING_COMMANDS: "Odotetaan komentoja..."
STR_CONNECTION_FAILED_RETRYING: "(Yhteys epäonnistui, yritetään uudelleen)"
STR_CALIBRE_DISCONNECTED: "Yhteys Calibreen katkaistiin"
STR_CALIBRE_WAITING_TRANSFER: "Odotetaan siirtoa..."
STR_CALIBRE_TRANSFER_HINT: "Jos siirto epäonnistuu, ota käyttöön\\n'Ignore free space' Calibren\\nSmartDevice-lisäosan asetuksissa."
STR_CALIBRE_RECEIVING: "Vastaanotetaan: " STR_CALIBRE_RECEIVING: "Vastaanotetaan: "
STR_CALIBRE_RECEIVED: "Vastaanotettu: " STR_CALIBRE_RECEIVED: "Vastaanotettu: "
STR_CALIBRE_WAITING_MORE: "Odotetaan lisää..."
STR_CALIBRE_FAILED_CREATE_FILE: "Tiedoston luonti epäonnistui"
STR_CALIBRE_PASSWORD_REQUIRED: "Salasana vaaditaan"
STR_CALIBRE_TRANSFER_INTERRUPTED: "Siirto keskeytyi"
STR_CALIBRE_INSTRUCTION_1: "1) Asenna CrossPoint Reader -lisäosa" STR_CALIBRE_INSTRUCTION_1: "1) Asenna CrossPoint Reader -lisäosa"
STR_CALIBRE_INSTRUCTION_2: "2) Ole samassa WiFi-verkossa" STR_CALIBRE_INSTRUCTION_2: "2) Ole samassa WiFi-verkossa"
STR_CALIBRE_INSTRUCTION_3: "3) Calibressa: \"Lähetä laitteelle\"" STR_CALIBRE_INSTRUCTION_3: "3) Calibressa: \"Lähetä laitteelle\""
@@ -88,37 +63,25 @@ STR_CAT_CONTROLS: "Ohjaimet"
STR_CAT_SYSTEM: "Järjestelmä" STR_CAT_SYSTEM: "Järjestelmä"
STR_SLEEP_SCREEN: "Lepotilanäyttö" STR_SLEEP_SCREEN: "Lepotilanäyttö"
STR_SLEEP_COVER_MODE: "Lepotilanäytön kansitila" STR_SLEEP_COVER_MODE: "Lepotilanäytön kansitila"
STR_STATUS_BAR: "Tilapalkki"
STR_HIDE_BATTERY: "Piilota akun %" STR_HIDE_BATTERY: "Piilota akun %"
STR_EXTRA_SPACING: "Kappaleiden lisäväli" STR_EXTRA_SPACING: "Kappaleiden lisäväli"
STR_TEXT_AA: "Tekstin reunanpehmennys" STR_TEXT_AA: "Tekstin reunanpehmennys"
STR_SHORT_PWR_BTN: "Lyhyt virtapainikkeen painallus" STR_SHORT_PWR_BTN: "Lyhyt virtapainikkeen painallus"
STR_ORIENTATION: "Lukusuunta" STR_ORIENTATION: "Lukusuunta"
STR_FRONT_BTN_LAYOUT: "Etupainikkeiden asettelu"
STR_SIDE_BTN_LAYOUT: "Sivupainikkeiden asettelu (lukija)" STR_SIDE_BTN_LAYOUT: "Sivupainikkeiden asettelu (lukija)"
STR_LONG_PRESS_SKIP: "Pitkä painallus: lukuhyppy" STR_LONG_PRESS_SKIP: "Pitkä painallus: lukuhyppy"
STR_FONT_FAMILY: "Lukijan fonttiperhe" STR_FONT_FAMILY: "Lukijan fonttiperhe"
STR_EXT_READER_FONT: "Ulkoinen lukijafontti"
STR_EXT_CHINESE_FONT: "Lukijafontti"
STR_EXT_UI_FONT: "Käyttöliittymäfontti"
STR_FONT_SIZE: "Käyttöliittymän fonttikoko" STR_FONT_SIZE: "Käyttöliittymän fonttikoko"
STR_LINE_SPACING: "Lukijan riviväli" STR_LINE_SPACING: "Lukijan riviväli"
STR_ASCII_LETTER_SPACING: "ASCII-kirjainväli"
STR_ASCII_DIGIT_SPACING: "ASCII-numeroväli"
STR_CJK_SPACING: "CJK-välistys"
STR_COLOR_MODE: "Väritila"
STR_SCREEN_MARGIN: "Lukijan näyttömarginaali" STR_SCREEN_MARGIN: "Lukijan näyttömarginaali"
STR_PARA_ALIGNMENT: "Lukijan kappaletasaus" STR_PARA_ALIGNMENT: "Lukijan kappaletasaus"
STR_HYPHENATION: "Tavutus" STR_HYPHENATION: "Tavutus"
STR_TIME_TO_SLEEP: "Aika lepotilaan" STR_TIME_TO_SLEEP: "Aika lepotilaan"
STR_REFRESH_FREQ: "Päivitystaajuus" STR_REFRESH_FREQ: "Päivitystaajuus"
STR_CALIBRE_SETTINGS: "Calibre-asetukset"
STR_KOREADER_SYNC: "KOReader-synkronointi" STR_KOREADER_SYNC: "KOReader-synkronointi"
STR_CHECK_UPDATES: "Tarkista päivitykset" STR_CHECK_UPDATES: "Tarkista päivitykset"
STR_LANGUAGE: "Kieli" STR_LANGUAGE: "Kieli"
STR_SELECT_WALLPAPER: "Valitse taustakuva"
STR_CLEAR_READING_CACHE: "Tyhjennä lukuvälimuisti" STR_CLEAR_READING_CACHE: "Tyhjennä lukuvälimuisti"
STR_CALIBRE: "Calibre"
STR_USERNAME: "Käyttäjänimi" STR_USERNAME: "Käyttäjänimi"
STR_PASSWORD: "Salasana" STR_PASSWORD: "Salasana"
STR_SYNC_SERVER_URL: "Synkronointipalvelimen osoite" STR_SYNC_SERVER_URL: "Synkronointipalvelimen osoite"
@@ -153,8 +116,6 @@ STR_COVER: "Kansi"
STR_NONE_OPT: "Ei mitään" STR_NONE_OPT: "Ei mitään"
STR_FIT: "Sovita" STR_FIT: "Sovita"
STR_CROP: "Rajaa" STR_CROP: "Rajaa"
STR_NO_PROGRESS: "Ei edistymistä"
STR_FULL_OPT: "Täysi"
STR_NEVER: "Ei koskaan" STR_NEVER: "Ei koskaan"
STR_IN_READER: "Lukijassa" STR_IN_READER: "Lukijassa"
STR_ALWAYS: "Aina" STR_ALWAYS: "Aina"
@@ -165,9 +126,6 @@ STR_PORTRAIT: "Pysty"
STR_LANDSCAPE_CW: "Vaaka myötäpäivään" STR_LANDSCAPE_CW: "Vaaka myötäpäivään"
STR_INVERTED: "Käännetty" STR_INVERTED: "Käännetty"
STR_LANDSCAPE_CCW: "Vaaka vastapäivään" STR_LANDSCAPE_CCW: "Vaaka vastapäivään"
STR_FRONT_LAYOUT_BCLR: "Tak, Vah, Vas, Oik"
STR_FRONT_LAYOUT_LRBC: "Vas, Oik, Tak, Vah"
STR_FRONT_LAYOUT_LBCR: "Vas, Tak, Vah, Oik"
STR_PREV_NEXT: "Edell/Seur" STR_PREV_NEXT: "Edell/Seur"
STR_NEXT_PREV: "Seur/Edell" STR_NEXT_PREV: "Seur/Edell"
STR_BOOKERLY: "Bookerly" STR_BOOKERLY: "Bookerly"
@@ -204,8 +162,6 @@ STR_NO_UPDATE: "Ei päivitystä saatavilla"
STR_UPDATE_FAILED: "Päivitys epäonnistui" STR_UPDATE_FAILED: "Päivitys epäonnistui"
STR_UPDATE_COMPLETE: "Päivitys valmis" STR_UPDATE_COMPLETE: "Päivitys valmis"
STR_POWER_ON_HINT: "Pidä virtapainiketta pohjassa käynnistääksesi" STR_POWER_ON_HINT: "Pidä virtapainiketta pohjassa käynnistääksesi"
STR_EXTERNAL_FONT: "Ulkoinen fontti"
STR_BUILTIN_DISABLED: "Sisäänrakennettu (pois käytöstä)"
STR_NO_ENTRIES: "Merkintöjä ei löytynyt" STR_NO_ENTRIES: "Merkintöjä ei löytynyt"
STR_DOWNLOADING: "Ladataan..." STR_DOWNLOADING: "Ladataan..."
STR_DOWNLOAD_FAILED: "Lataus epäonnistui" STR_DOWNLOAD_FAILED: "Lataus epäonnistui"
@@ -216,7 +172,6 @@ STR_FETCH_FEED_FAILED: "Syötteen haku epäonnistui"
STR_PARSE_FEED_FAILED: "Syötteen käsittely epäonnistui" STR_PARSE_FEED_FAILED: "Syötteen käsittely epäonnistui"
STR_NETWORK_PREFIX: "Verkko: " STR_NETWORK_PREFIX: "Verkko: "
STR_IP_ADDRESS_PREFIX: "IP-osoite: " STR_IP_ADDRESS_PREFIX: "IP-osoite: "
STR_SCAN_QR_WIFI_HINT: "tai skannaa QR-koodi puhelimellasi yhdistääksesi WiFiin."
STR_ERROR_GENERAL_FAILURE: "Virhe: Yleinen virhe" STR_ERROR_GENERAL_FAILURE: "Virhe: Yleinen virhe"
STR_ERROR_NETWORK_NOT_FOUND: "Virhe: Verkkoa ei löytynyt" STR_ERROR_NETWORK_NOT_FOUND: "Virhe: Verkkoa ei löytynyt"
STR_ERROR_CONNECTION_TIMEOUT: "Virhe: Yhteys aikakatkaistiin" STR_ERROR_CONNECTION_TIMEOUT: "Virhe: Yhteys aikakatkaistiin"
@@ -224,7 +179,6 @@ STR_SD_CARD: "SD-kortti"
STR_BACK: "« Takaisin" STR_BACK: "« Takaisin"
STR_EXIT: "« Poistu" STR_EXIT: "« Poistu"
STR_HOME: "« Koti" STR_HOME: "« Koti"
STR_SAVE: "« Tallenna"
STR_SELECT: "Valitse" STR_SELECT: "Valitse"
STR_TOGGLE: "Vaihda" STR_TOGGLE: "Vaihda"
STR_CONFIRM: "Vahvista" STR_CONFIRM: "Vahvista"
@@ -237,22 +191,14 @@ STR_YES: "Kyllä"
STR_NO: "Ei" STR_NO: "Ei"
STR_STATE_ON: "PÄÄLLÄ" STR_STATE_ON: "PÄÄLLÄ"
STR_STATE_OFF: "POIS" STR_STATE_OFF: "POIS"
STR_SET: "Asetettu"
STR_NOT_SET: "Ei asetettu" STR_NOT_SET: "Ei asetettu"
STR_DIR_LEFT: "Vasen" STR_DIR_LEFT: "Vasen"
STR_DIR_RIGHT: "Oikea" STR_DIR_RIGHT: "Oikea"
STR_DIR_UP: "Ylös" STR_DIR_UP: "Ylös"
STR_DIR_DOWN: "Alas" STR_DIR_DOWN: "Alas"
STR_CAPS_ON: "ISOT"
STR_CAPS_OFF: "pienet"
STR_OK_BUTTON: "OK" STR_OK_BUTTON: "OK"
STR_ON_MARKER: "[PÄÄLLÄ]"
STR_SLEEP_COVER_FILTER: "Lepotilanäytön kansisuodatin" STR_SLEEP_COVER_FILTER: "Lepotilanäytön kansisuodatin"
STR_FILTER_CONTRAST: "Kontrasti" STR_FILTER_CONTRAST: "Kontrasti"
STR_STATUS_BAR_FULL_PERCENT: "Täysi + prosentti"
STR_STATUS_BAR_FULL_BOOK: "Täysi + kirjapalkki"
STR_STATUS_BAR_BOOK_ONLY: "Vain kirjapalkki"
STR_STATUS_BAR_FULL_CHAPTER: "Täysi + lukupalkki"
STR_UI_THEME: "Käyttöliittymäteema" STR_UI_THEME: "Käyttöliittymäteema"
STR_THEME_CLASSIC: "Klassinen" STR_THEME_CLASSIC: "Klassinen"
STR_THEME_LYRA: "Lyra" STR_THEME_LYRA: "Lyra"
@@ -261,7 +207,6 @@ STR_SUNLIGHT_FADING_FIX: "Auringonvalon haalistumiskorjaus"
STR_REMAP_FRONT_BUTTONS: "Uudelleenmääritä etupainikkeet" STR_REMAP_FRONT_BUTTONS: "Uudelleenmääritä etupainikkeet"
STR_OPDS_BROWSER: "OPDS-selain" STR_OPDS_BROWSER: "OPDS-selain"
STR_COVER_CUSTOM: "Kansi + mukautettu" STR_COVER_CUSTOM: "Kansi + mukautettu"
STR_RECENTS: "Viimeisimmät"
STR_MENU_RECENT_BOOKS: "Viimeisimmät kirjat" STR_MENU_RECENT_BOOKS: "Viimeisimmät kirjat"
STR_NO_RECENT_BOOKS: "Ei viimeisimpiä kirjoja" STR_NO_RECENT_BOOKS: "Ei viimeisimpiä kirjoja"
STR_CALIBRE_DESC: "Käytä Calibren langatonta laiteyhteyttä" STR_CALIBRE_DESC: "Käytä Calibren langatonta laiteyhteyttä"
@@ -288,9 +233,6 @@ STR_DELETE_CACHE: "Poista kirjan välimuisti"
STR_CHAPTER_PREFIX: "Luku: " STR_CHAPTER_PREFIX: "Luku: "
STR_PAGES_SEPARATOR: " sivua | " STR_PAGES_SEPARATOR: " sivua | "
STR_BOOK_PREFIX: "Kirja: " STR_BOOK_PREFIX: "Kirja: "
STR_KBD_SHIFT: "shift"
STR_KBD_SHIFT_CAPS: "SHIFT"
STR_KBD_LOCK: "LOCK"
STR_CALIBRE_URL_HINT: "Calibrelle lisää /opds osoitteeseen" STR_CALIBRE_URL_HINT: "Calibrelle lisää /opds osoitteeseen"
STR_PERCENT_STEP_HINT: "Vasen/Oikea: 1% Ylös/Alas: 10%" STR_PERCENT_STEP_HINT: "Vasen/Oikea: 1% Ylös/Alas: 10%"
STR_SYNCING_TIME: "Synkronoidaan aikaa..." STR_SYNCING_TIME: "Synkronoidaan aikaa..."

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