Compare commits

..
Author SHA1 Message Date
Justin Mitchell bf9fea5923 Fix image decode overflow and viewport clipping issues
Prevent integer overflow when validating large image dimensions by using 64-bit arithmetic. Add periodic yielding during JPEG/PNG decode to prevent watchdog timeouts on multi-second operations. Clamp image top margins to prevent full-height images from overflowing page bottom and failing bounds checks. Increase max source pixels from 3MP to 8MP to support high-resolution ebook covers while keeping decode times reasonable.
2026-08-10 14:48:47 -04:00
132 changed files with 4058 additions and 7991 deletions

No files matched your search

+2 -1
View File
@@ -1,6 +1,7 @@
name: "PR Formatting" name: "PR Formatting"
on: on:
pull_request:
pull_request_target: pull_request_target:
types: types:
- opened - opened
@@ -8,7 +9,7 @@ on:
- edited - edited
permissions: permissions:
pull-requests: read statuses: write
jobs: jobs:
title-check: title-check:
+1 -1
View File
@@ -1 +1 @@
.skills/SKILL.md /home/urit/Documents/CrossPoint/crosspoint-reader/.skills/SKILL.md
+2 -2
View File
@@ -36,7 +36,7 @@ CrossPoint is open-source e-reader firmware - community-built, fully hackable, f
- OPDS browser with saved servers (up to 8), search, pagination, and direct download - OPDS browser with saved servers (up to 8), search, pagination, and direct download
- OTA update checks and installs from GitHub releases - OTA update checks and installs from GitHub releases
- **Customization**: multiple themes (Classic, Lyra, Lyra Extended, RoundedRaff), sleep screen modes including transparent overlays, front/side button remapping, status bar controls, power-button behavior, refresh cadence, and more. - **Customization**: multiple themes (Classic, Lyra, Lyra Extended, RoundedRaff), sleep screen modes, front/side button remapping, status bar controls, power-button behavior, refresh cadence, and more.
- **Localization**: 24 UI languages and counting. RTL support. - **Localization**: 24 UI languages and counting. RTL support.
@@ -136,7 +136,7 @@ Conversion runs the firmware repo's `lib/EpdFont/scripts/fontconvert_sdcard.py`
- [Web server endpoints](./docs/webserver-endpoints.md) - [Web server endpoints](./docs/webserver-endpoints.md)
- [Project scope](./SCOPE.md) - [Project scope](./SCOPE.md)
- [Contributing docs](./docs/contributing/README.md) - [Contributing docs](./docs/contributing/README.md)
- [Touch and UI development](./docs/contributing/touch-and-ui.md) - how to build new screens on the FreeInkUI activity bases (UiListActivity and friends), plus build envs for the non-Xteink touch devices - [Touch and UI development](./docs/contributing/touch-and-ui.md) - FreeInkUI components for new screens, the touch bridge for existing ones, and build envs for the non-Xteink touch devices
--- ---
+4 -14
View File
@@ -186,7 +186,7 @@ The Settings screen allows you to configure the device's behavior. There are a f
- "None" - A blank screen - "None" - A blank screen
- "Cover + Custom" - The book cover image while actively reading, falls back to "Custom" behavior otherwise - "Cover + Custom" - The book cover image while actively reading, falls back to "Custom" behavior otherwise
- "Quick resume" - The text of the last page read will be displayed on the sleep screen and a moon icon is shown on the edge of the screen. Waking up the device will return to the same page of the opened book. This is useful for quickly resuming reading without waiting for the device to fully wake up and load the book. - "Quick resume" - The text of the last page read will be displayed on the sleep screen and a moon icon is shown on the edge of the screen. Waking up the device will return to the same page of the opened book. This is useful for quickly resuming reading without waiting for the device to fully wake up and load the book.
- "Transparent" - A transparent overlay image drawn over the current screen; see [Sleep Screen](#37-sleep-screen) below for more information
- **Sleep Screen Cover Mode**: How to display the book cover when "Cover" sleep screen is selected: - **Sleep Screen Cover Mode**: How to display the book cover when "Cover" sleep screen is selected:
- "Fit" (default) - Scale the image down to fit centered on the screen, padding with white borders as necessary - "Fit" (default) - Scale the image down to fit centered on the screen, padding with white borders as necessary
@@ -501,7 +501,6 @@ The **Sleep Screen** setting controls what is displayed when the device goes to
| **Custom** | A custom image from the SD card (see below). Falls back to **Dark** if no custom image is found. | | **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** | 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, shown only while actively reading. Falls back to **Custom** behavior when not reading. | | **Cover + Custom** | The cover of the currently open book, shown only while actively reading. Falls back to **Custom** behavior when not reading. |
| **Transparent** | A BMP or PNG overlay drawn over the current screen. Supports PNG and 32-bit BGRA alpha transparency, and treats white as transparent in regular BMPs. Falls back to **Dark** if no valid overlay image is found. |
| **None** | A blank screen. | | **None** | A blank screen. |
#### Cover settings #### Cover settings
@@ -516,21 +515,12 @@ When using **Cover** or **Cover + Custom**, two additional settings apply:
To use custom sleep images, set the sleep screen mode to **Custom** or **Cover + Custom**, then place images on the SD card: 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.) - **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 takes priority over the `.sleep`/`sleep` directories. - **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.
#### Transparent overlay images
To use transparent sleep overlays, set the sleep screen mode to **Transparent**, then place BMP or PNG files on the SD card:
- **Multiple Images (recommended):** Create a `.sleep-overlay` directory in the root of the SD card and place any number of valid overlay `.bmp` or `.png` images inside. One will be randomly selected each time the device sleeps. A directory named `sleep-overlay` is also accepted as a fallback.
- **Single Image:** Place `sleep-overlay.bmp` or `sleep-overlay.png` in the root directory. A root BMP takes priority over a root PNG, and both take priority over the `.sleep-overlay`/`sleep-overlay` directories.
Transparent overlay files are intentionally separate from normal sleep images. Regular BMP formats supported by CrossPoint are accepted; white pixels leave the existing screen unchanged. For per-pixel alpha transparency, use a PNG with an alpha channel or a 32-bit BGRA BMP with both visible and non-opaque pixels. Opaque white pixels in alpha images erase the content behind them.
> [!TIP] > [!TIP]
> For best results: > For best results:
> - For non-transparent **Custom** mode, use uncompressed BMP files with 24-bit color depth. >
> - For **Transparent** mode, use a PNG or uncompressed 32-bit BGRA BMP for per-pixel alpha, or a regular BMP for white-as-transparent artwork. > - Use uncompressed BMP files with 24-bit color depth
> - X4: Use a resolution of 480x800 pixels to match the device's screen resolution. > - X4: Use a resolution of 480x800 pixels to match the device's screen resolution.
> - X3: Use a resolution of 528x792 pixels to match the device's screen resolution. > - X3: Use a resolution of 528x792 pixels to match the device's screen resolution.
+113 -94
View File
@@ -1,88 +1,23 @@
# Touch and UI Development # Touch and UI Development
CrossPoint runs on touch devices (Seeed Sticky, M5Paper, LilyGo T5) alongside the button-only Xteink X3/X4. Every screen must work with both input styles. CrossPoint now runs on touch devices (Seeed Sticky, M5Paper, LilyGo T5) alongside the button-only Xteink X3/X4. Every screen must work with both input styles. There are two supported ways to get there:
**There is one supported way to build a new screen: FreeInkUI, hosted through the firmware base classes below.** Touch hit-testing, tap highlighting, long-press, swipe scrolling, and button focus navigation all come from the shared stack; you never hand-roll coordinate math. 1. **New screens: build them with FreeInkUI components.** Touch hit-testing, tap highlighting, long-press, and button focus navigation come with the component; you never hand-roll coordinate math.
2. **Existing screens and in-flight features: use the MappedInputManager touch bridge.** A small set of helpers adds tap/hold/swipe support to hand-rolled rendering without restructuring the activity.
The old bridge helpers (`rowTouch`, `colTouch`, `wasTapInRect`, manual rect `contains()` checks) are legacy. They survive only for the two remaining hand-rolled surfaces (the theme-driven home screen and the reader page) and must not appear in new code. PRs that add new uses will be asked to convert. If you are starting a new activity, use FreeInkUI. If you have a feature branch with a hand-rolled screen already working on buttons, use the bridge; do not rewrite mid-flight.
--- ---
## Picking the right host ## Path 1: New screens with FreeInkUI
| Your screen is... | Use | In-tree reference | FreeInkUI (`freeink-sdk/libs/ui/FreeInkUI`, namespace `freeink::ui`) is an immediate-mode component library. The core idea:
|---|---|---|
| A single list of rows | subclass `UiListActivity` | [`LanguageSelectActivity`](../../src/activities/settings/LanguageSelectActivity.cpp) (minimal), [`RecentBooksActivity`](../../src/activities/home/RecentBooksActivity.cpp) (long-press) |
| Tabbed lists | subclass `UiTabListActivity` | [`SettingsActivity`](../../src/activities/settings/SettingsActivity.cpp) |
| Custom FUI layout (sliders, prompts, state machines) | inherit `UiAppHost` directly | [`EpubReaderPercentSelectionActivity`](../../src/activities/reader/EpubReaderPercentSelectionActivity.cpp), [`WifiSelectionActivity`](../../src/activities/network/WifiSelectionActivity.cpp) |
| A modal picker or confirm inside a legacy activity | `OptionPopup` (or push `ConfirmationActivity`) | [`OtaUpdateActivity`](../../src/activities/settings/OtaUpdateActivity.cpp) |
| A Yes/No prompt inside a FUI screen | build `fui::optionDialog` into the screen | `WifiSelectionActivity::buildPromptDialog` |
All four paths route input through the same SDK interaction table, so touch and physical buttons fire the same actions with no per-screen coordinate code. - While a component renders, it registers its tappable areas ("hit rects") into the frame's interaction buffer via `Frame::hit(rect, action, value, inputMask, state)`.
- Each loop you build an `InputSnapshot` from the input manager and route it against that buffer. If a tap (or mapped button press) lands in a registered rect, the routed `ActionEvent` tells you which action fired and with what value.
- Components query `frame.stateFor(action, value)` while painting, so touch-down highlight and focus states render correctly without any per-activity code.
## The hosting stack You get touch, hold highlighting, long-press, minimum touch-target sizing, and orientation-aware coordinates for free. The same action IDs fire from physical buttons, so one code path serves both input styles.
`UiAppHost` ([src/components/UiAppHost.h](../../src/components/UiAppHost.h)) owns what every FUI screen shares: the font-bound render target, the `FreeInkApp`, and the `uiReady` handshake that lets the loop task route touch snapshots against the interaction table the render task rebuilds. Never re-implement that handshake; it is a cross-task protocol and lives in exactly one place.
`UiListActivity` ([src/activities/UiListActivity.h](../../src/activities/UiListActivity.h)) layers the list protocol on top: swipes scroll the viewport without moving the selection, buttons move the selection and pull the viewport along (`fui::ListNav`), plus the render skeleton (header chrome, app, footer hints). `UiTabListActivity` is the tab-bar sibling; its selection ring puts the tab bar at position 0 and the rows after it.
### New list screen skeleton
Subclasses supply the data; the base owns the loop. The whole contract:
```cpp
class MyListActivity final : public UiListActivity {
public:
MyListActivity(GfxRenderer& r, MappedInputManager& in) : UiListActivity("MyList", r, in) {}
private:
std::vector<Entry> entries; // the activity's data, loaded in onEnter
std::vector<fui::ListItem> rows; // activity-owned row cache: rebuilt only when entries changes, reused every render
int listCount() const override { return entries.size(); }
const char* headerTitle() const override { return tr(STR_MY_TITLE); }
void onEnter() override {
UiListActivity::onEnter();
entries = /* ... load from wherever ... */;
rebuildRows();
}
// Called whenever entries changes (here, only onEnter; a mutable list would
// also call this after any add/remove). NOT called from buildScreen().
void rebuildRows() {
rows.clear();
rows.reserve(entries.size());
// ... one fui::ListItem per entry (label, actionValue = index) ...
}
void buildScreen(UiScreen& screen) override {
// set content margin from the theme safe area, then:
fui::ListProps props;
props.items = rows.data();
props.count = rows.size();
props.action = ACTION_ROW;
props.inputMask = fui::InputTouch; // physical buttons stay in the base loop
syncListViewport(screen, props); // selection/viewport handoff, always right before list()
screen.list(props);
}
void activateIndex(int index) override {
app.clearTapFlash(); // leaving the screen: a lingering flash would gray the next render
// ... open the thing ...
}
};
```
See [`FileBrowserActivity`](../../src/activities/home/FileBrowserActivity.cpp)'s `rebuildRowItems()` for this pattern applied to a directory listing that can run into the hundreds of entries, and `LanguageSelectActivity.cpp` for the rest of the skeleton (content-margin math, footer, etc.) — note that file still builds its row vector locally inside `buildScreen()`; match `FileBrowserActivity`, not that file, for the row cache. Optional overrides: `onRowLongPress(index)`, `drawFooter()`, `handleButtons()` for extra physical-button handling, and `ACTION_USER`-and-up action ids for non-row elements (register handlers in `onEnter` after the base's).
### Rules that apply to every FUI screen
- Register actions with `app.on(...)` in `onEnter` (after `resetUi()` for direct `UiAppHost` users). Handlers receive a `void* user` you cast back to the activity.
- Handlers that leave the current screen call `app.clearTapFlash()` first.
- Theme tokens are shared and bound by `resetUi()`; never call `app.setTheme` yourself. Metrics flow from the active UITheme through [`UIThemeTokens.h`](../../src/components/UIThemeTokens.h), including the per-board bezel insets that keep scrollbars visible.
- `TextStyle.maxLines` defaults to 1 and truncates with an ellipsis. Set `maxLines` explicitly on any dialog headline or message that can wrap.
- Everything stays allocation-free in steady state. A local `std::vector` inside `buildScreen()` is **not** allocation-free even with `reserve()` first: it starts at zero capacity on every call, `reserve()` allocates, and the destructor frees that storage before the call returns — real allocator work and fragmentation risk on every repaint (cursor move, tap flash, ...), not just on data changes. Build `ListItem` rows into activity-owned storage instead, reserved once when the underlying data loads (`onEnter()`/a `load*()` — see the skeleton above and `FileBrowserActivity::rebuildRowItems()`), and reused unchanged by every `buildScreen()` call. Use a fixed-capacity array (e.g. `ListItem rows[MAX]`, as `OptionPopup` and `KOReaderSyncActivity`'s action rows do) when the count is small and bounded. Do not hold FUI `props` across renders — only the row storage they point into.
### Component inventory ### Component inventory
@@ -98,11 +33,106 @@ All under `freeink-sdk/libs/ui/FreeInkUI/include/components/`:
| Media | `book-card`, `cover-grid`, `cover-carousel`, `metric-card` | | Media | `book-card`, `cover-grid`, `cover-carousel`, `metric-card` |
| Text | `text-field`, `text-area` | | Text | `text-field`, `text-area` |
One exception to "always go through a host": `KeyboardEntryActivity` drives the keyboard component with a raw `fui::Frame` and `TouchHoldRouter` because per-key hold repeat needs its own routing. If you think your screen needs that, raise it in the PR first. ### Integration skeleton
The in-tree reference is [`src/activities/util/KeyboardEntryActivity.cpp`](../../src/activities/util/KeyboardEntryActivity.cpp); it drives the FreeInkUI keyboard component inside a normal Activity. The shape:
```cpp
#include <FreeInkUIGfxRenderer.h>
namespace fui = freeink::ui;
// Render step: draw the component and (re)register its hit rects.
fui::GfxRendererTarget target(renderer); // adapts GfxRenderer to FreeInkUI's DrawTarget
target.setFont(fui::GfxRendererTarget::FONT_BODY, UI_12_FONT_ID);
const fui::DeviceContext device = target.deviceContext(); // orientation, safe area, touch sizing
fui::Frame<48> frame(target, device, fui::InputSnapshot{}, interactions); // 48 = max hit rects
fui::KeyboardProps props;
props.layout = &currentLayout();
props.keyAction = ACTION_KEY; // one action id; the key is the event value
props.inputMask = fui::InputTouch | fui::InputLongPress;
fui::keyboard(frame, kbRect, props); // draws AND registers hit rects
```
```cpp
// Input step (each loop): feed touch state, act on routed events.
int tx, ty, tapX, tapY, hx, hy;
const bool pressedDown = mappedInput.wasScreenTouchDown(tx, ty);
const bool tapped = mappedInput.wasScreenTapped(tapX, tapY);
const bool inContact = mappedInput.isScreenTouchHeld(hx, hy);
const auto result = touchRouter.update(interactions, pressedDown, tx, ty,
tapped, tapX, tapY, inContact, millis());
if (result.event) {
activateValue(result.event.value, result.event.longPress);
requestUpdate();
} else if (result.activeChanged) {
requestUpdate(); // repaint touch-down highlight
}
```
Notes:
- `Frame<N>` is templated on the max number of hit rects; the buffer is stack/static, no heap.
- `fui::TouchHoldRouter` (see `components/keyboard/keyboard.h`) synthesizes long-press while the finger is still down and swallows the eventual release. Use it whenever a component distinguishes tap from long-press.
- For simpler screens without long-press, `FreeInkApp` / `Screen<N>` (`FreeInkApp.h`) is the ergonomic layer: it owns the interaction buffer, offers a top-to-bottom builder (`takeTop`, footer helpers, etc.), and its `render(input)` call routes the snapshot and dispatches registered action callbacks in one step. `snapshotFrom(input, device)` in `FreeInkUIInputManager.h` builds the `InputSnapshot` with orientation-aware touch mapping.
- Everything stays allocation-free and works on the button-only devices unchanged: physical buttons route through the same interaction table via each hit rect's `inputMask`.
--- ---
## Global gestures: do not reimplement these ## Path 2: Adding touch to existing hand-rolled screens
For activities that draw their own rows, menus, and buttons, `MappedInputManager` ([src/MappedInputManager.h](../../src/MappedInputManager.h)) exposes bridge helpers. They all return **logical screen coordinates** (orientation already applied via `GfxRenderer::tapToLogical`); never touch raw normalized panel coordinates or the SDK `InputManager` directly.
| Helper | Use for |
|---|---|
| `wasScreenTapped(x, y)` | A completed tap (press + release), with logical coords |
| `wasScreenTouchDown(x, y)` | Touch-down (held > 90 ms, not yet released): draw selection highlight |
| `isScreenTouchHeld(x, y)` | Live contact position while the finger is down (drag tracking) |
| `wasTapInRect(x, y, w, h)` | One-off hit test on a rectangle (a single button, a banner) |
| `wasListItemTapped(index, count, selected, listTop, listHeight, hasSubtitle)` | Taps on a standard UITheme list; does the row/paging math for you |
| `wasListItemTouchedDown(...)` | Same geometry, touch-down phase (highlight before activate) |
| `rowTouch(row, top, rowStep, rowCount, xStart, xEnd, rowHeight)` | Any custom band of equal-height rows; returns `RowTouch::None/Down/Tap` |
| `colTouch(col, left, colStep, colCount, yStart, yEnd, colWidth)` | Horizontal button bands (dialogs, prompts) |
| `wasSwipe()` | Raw swipe direction if you need one beyond the global gestures |
| `hasTouch()` | True when the device has a touch panel (rarely needed; helpers simply never fire without one) |
### Pattern A: standard themed list
One call per loop; UITheme owns the row geometry ([EpubReaderBookmarksActivity.cpp:123](../../src/activities/reader/EpubReaderBookmarksActivity.cpp)):
```cpp
int tapped = -1;
if (mappedInput.wasListItemTapped(tapped, bookmarks.size(), selectorIndex, listY, listHeight, true)) {
selectorIndex = tapped;
openBookmark();
return;
}
```
### Pattern B: custom rows with hold highlight
For non-theme row layouts, use `rowTouch` and distinguish `Down` (highlight) from `Tap` (activate), as in [EpubReaderFootnotesActivity.cpp](../../src/activities/reader/EpubReaderFootnotesActivity.cpp):
```cpp
int row = -1;
const auto touch = mappedInput.rowTouch(row, listTop, lineHeight, visibleCount,
contentX, contentX + contentWidth);
if (touch != MappedInputManager::RowTouch::None) {
const int touched = scrollOffset + row;
if (touch == MappedInputManager::RowTouch::Down) {
if (selectedIndex != touched) { selectedIndex = touched; requestUpdate(); }
} else { // RowTouch::Tap
selectedIndex = touched;
activateSelection();
}
return;
}
```
The `Down` state exists because e-ink repaints are slow: highlight on touch-down gives immediate feedback, activation happens on release.
### Global gestures: do not reimplement these
Three gestures are handled once, for every screen. Activities must not add their own edge-swipe handling: Three gestures are handled once, for every screen. Activities must not add their own edge-swipe handling:
@@ -112,25 +142,14 @@ Three gestures are handled once, for every screen. Activities must not add their
| Home | Up-swipe starting in the bottom 14% | `ActivityManager::loop()`; pops to Home (activities can override via `handleHomeGesture()`) | | Home | Up-swipe starting in the bottom 14% | `ActivityManager::loop()`; pops to Home (activities can override via `handleHomeGesture()`) |
| Menu | Down-swipe starting in the top 14% | Activities that have a menu check `wasMenuGesture()` themselves (the reader does this) | | Menu | Down-swipe starting in the top 14% | Activities that have a menu check `wasMenuGesture()` themselves (the reader does this) |
Because the back gesture arrives as `Button::Back`, most button-era activities gain back-swipe support with zero changes. Because the back gesture arrives as `Button::Back`, most button-era activities gain back-swipe support with zero changes. That is the bar to aim for: bridge helpers should make touch an additive layer over the button flow, not a second input state machine.
--- ### Bridge rules
## Legacy bridge helpers (do not use in new code) - Handle touch in `loop()` next to the existing button handling, one helper call per interaction zone, and `return` after consuming an event (mirrors the button pattern).
- Never call the SDK `InputManager` or read GPIO directly; the HAL rule from the main guide applies to touch too.
`MappedInputManager` still exposes raw touch accessors. The FUI stack consumes them internally (via `touchSnapshotFrom` in [`UiAppHelpers.h`](../../src/components/UiAppHelpers.h)); activities should not. - Coordinates from the helpers are logical and orientation-correct on all four rotations; test at least Portrait and one Landscape mode before PR.
- Nothing to clean up in `onExit()`; the helpers are stateless from the activity's point of view.
| Helper | Status |
|---|---|
| `wasScreenTapped` / `wasScreenTouchDown` / `isScreenTouchHeld` | Consumed by the FUI snapshot builder. Direct use only in the two legacy surfaces |
| `wasTapInRect(x, y, w, h)` | Legacy one-off hit test |
| `rowTouch` / `colTouch` | Legacy row/column band math. Sole remaining user: `HomeActivity` (theme-driven layout) |
| `wasSwipe()` | Raw swipe direction, for behavior beyond the global gestures (reader page turns) |
| `hasTouch()` | Still fine anywhere: gate touch-only chrome (on-screen Cancel/OK pairs) on it |
The `wasListItemTapped` / `wasListItemTouchedDown` helpers from the button-era bridge have been removed; every themed list is a `UiListActivity` now. If you are porting a branch that still calls them, convert the screen to `UiListActivity` rather than resurrecting the helpers.
As with all input: never call the SDK `InputManager` or read GPIO directly. The HAL rule from the main guide applies to touch too.
--- ---
+1 -3
View File
@@ -13,7 +13,7 @@ A dictionary folder must contain:
- `.syn` — synonym index (optional; maps alternate spellings and irregular forms to their headword) - `.syn` — synonym index (optional; maps alternate spellings and irregular forms to their headword)
- `.ifo` — metadata (optional) - `.ifo` — metadata (optional)
Not supported: dictionaries with 64-bit index offsets (`idxoffsetbits=64` in the `.ifo` — rare, and rejected with an error). Not supported: dictionaries with 64-bit index offsets (`idxoffsetbits=64` in the `.ifo` — rare, and rejected with an error), and HTML-formatted definitions render as raw markup rather than styled text.
## Setting Up a Dictionary ## Setting Up a Dictionary
@@ -49,8 +49,6 @@ On the very first lookup with a dictionary (and again whenever the `.idx` or `.s
When a word is found, the definition screen shows the matched headword at the top and the definition text below, with a page counter for long definitions. When a word is found, the definition screen shows the matched headword at the top and the definition text below, with a page counter for long definitions.
HTML dictionaries that declare `sametypesequence=h` use the EPUB text-layout engine for semantic formatting such as headings, bold, italics, lists, and line breaks. Images and CSS styling are ignored. Definitions that are too large or cannot be laid out within the available memory fall back to plain text.
- **Left/Right** or side **Up/Down** — previous / next page - **Left/Right** or side **Up/Down** — previous / next page
- **Back** — return to word selection - **Back** — return to word selection
+4 -1
View File
@@ -33,7 +33,10 @@ namespace {
// v35: Persist a uint32_t visible-text start offset for every page. // v35: Persist a uint32_t visible-text start offset for every page.
// v36: Ruby and CJK justification layout changes invalidate cached word positions. // v36: Ruby and CJK justification layout changes invalidate cached word positions.
// v37: Footnote href records grew from 96 to 256 bytes. // v37: Footnote href records grew from 96 to 256 bytes.
constexpr uint8_t SECTION_FILE_VERSION = 37; // v38: Image top margin is clamped so a full-viewport-height image cannot
// overflow the page bottom; older caches can hold placements that panels
// with no bottom inset refuse to draw.
constexpr uint8_t SECTION_FILE_VERSION = 38;
// Written into the version field while a build is in progress; patched to // Written into the version field while a build is in progress; patched to
// SECTION_FILE_VERSION only when the build is finalized. An abandoned / // SECTION_FILE_VERSION only when the build is finalized. An abandoned /
// crash-interrupted .bin therefore carries version 0, which loadSectionFile rejects // crash-interrupted .bin therefore carries version 0, which loadSectionFile rejects
+3 -3
View File
@@ -144,14 +144,14 @@ struct DirectPixelWriter {
// Write a single 2-bit dithered pixel value to the framebuffer. // Write a single 2-bit dithered pixel value to the framebuffer.
// Must be called after beginRow() for the current row. // Must be called after beginRow() for the current row.
// No bounds checking — caller guarantees coordinates are valid. // No bounds checking — caller guarantees coordinates are valid.
inline void writePixel(int logicalX, uint8_t pixelValue, bool writeWhiteInBw = false) const { inline void writePixel(int logicalX, uint8_t pixelValue) const {
// Determine whether to draw based on render mode // Determine whether to draw based on render mode
bool draw; bool draw;
bool state; bool state;
switch (mode) { switch (mode) {
case GfxRenderer::BW: case GfxRenderer::BW:
draw = writeWhiteInBw || pixelValue < 3; draw = (pixelValue < 3);
state = pixelValue < 3; state = true;
break; break;
case GfxRenderer::GRAYSCALE_MSB: case GfxRenderer::GRAYSCALE_MSB:
draw = (pixelValue == 1 || pixelValue == 2); draw = (pixelValue == 1 || pixelValue == 2);
@@ -1,16 +1,28 @@
#include "ImageToFramebufferDecoder.h" #include "ImageToFramebufferDecoder.h"
#include <Arduino.h>
#include <Logging.h> #include <Logging.h>
bool ImageToFramebufferDecoder::validateImageDimensions(int width, int height, const std::string& format) { bool ImageToFramebufferDecoder::validateImageDimensions(int width, int height, const std::string& format) {
if (width * height > MAX_SOURCE_PIXELS) { // 64-bit multiply: a malformed header (e.g. 40000x60000) overflows a plain
LOG_ERR("IMG", "Image too large (%dx%d = %d pixels %s), max supported: %d pixels", width, height, width * height, // int product before the comparison can reject it.
format.c_str(), MAX_SOURCE_PIXELS); const int64_t pixels = static_cast<int64_t>(width) * static_cast<int64_t>(height);
if (pixels > MAX_SOURCE_PIXELS) {
LOG_ERR("IMG", "Image too large (%dx%d = %lld pixels %s), max supported: %lld pixels", width, height,
static_cast<long long>(pixels), format.c_str(), static_cast<long long>(MAX_SOURCE_PIXELS));
return false; return false;
} }
return true; return true;
} }
void ImageToFramebufferDecoder::yieldDuringDecode(uint32_t& lastYieldMs) {
const uint32_t now = millis();
if (now - lastYieldMs >= 250) {
lastYieldMs = now;
vTaskDelay(1);
}
}
void ImageToFramebufferDecoder::warnUnsupportedFeature(const std::string& feature, const std::string& imagePath) { void ImageToFramebufferDecoder::warnUnsupportedFeature(const std::string& feature, const std::string& imagePath) {
LOG_ERR("IMG", "Warning: Unsupported feature '%s' in image '%s'. Image may not display correctly.", feature.c_str(), LOG_ERR("IMG", "Warning: Unsupported feature '%s' in image '%s'. Image may not display correctly.", feature.c_str(),
imagePath.c_str()); imagePath.c_str());
@@ -18,9 +18,6 @@ struct RenderConfig {
bool useDithering = true; bool useDithering = true;
bool performanceMode = false; bool performanceMode = false;
bool useExactDimensions = false; // If true, use maxWidth/maxHeight as exact output size (no recalculation) bool useExactDimensions = false; // If true, use maxWidth/maxHeight as exact output size (no recalculation)
float sourceCropX = 0.0f; // Fraction cropped equally from the left and right edges
float sourceCropY = 0.0f; // Fraction cropped equally from the top and bottom edges
bool preserveAlpha = false; // Skip transparent pixels instead of compositing them against white
std::string cachePath; // If non-empty, decoder will write pixel cache to this path std::string cachePath; // If non-empty, decoder will write pixel cache to this path
}; };
@@ -34,9 +31,21 @@ class ImageToFramebufferDecoder {
virtual const char* getFormatName() const = 0; virtual const char* getFormatName() const = 0;
// Call from per-row/per-MCU decode callbacks (free functions, hence public):
// yields one tick at most every 250 ms so multi-second decodes keep the idle
// task (and its watchdog) fed. `lastYieldMs` is caller-held state,
// initialized to the decode start time.
static void yieldDuringDecode(uint32_t& lastYieldMs);
protected: protected:
// Size validation helpers // Size validation helpers. The cap bounds decode TIME, not memory: both decoders
static constexpr int MAX_SOURCE_PIXELS = 3145728; // 2048 * 1536 // stream (JPEG in MCU bands at 1/2..1/8 coarse scale, PNG scanline-by-scanline
// with its own width-based row-buffer guard), so RAM never scales with source
// area. 8 MP admits real-world ebook covers (KDP recommends 1600x2560 and
// 2000x3000) while keeping a worst-case single decode in single-digit seconds;
// the row callbacks yield periodically so a long decode cannot starve the idle
// task's watchdog.
static constexpr int64_t MAX_SOURCE_PIXELS = 8388608; // 8 MP (e.g. 2048 * 4096)
bool validateImageDimensions(int width, int height, const std::string& format); bool validateImageDimensions(int width, int height, const std::string& format);
void warnUnsupportedFeature(const std::string& feature, const std::string& imagePath); void warnUnsupportedFeature(const std::string& feature, const std::string& imagePath);
@@ -46,6 +46,8 @@ struct JpegContext {
PixelCache cache; PixelCache cache;
bool caching{false}; bool caching{false};
uint32_t lastYieldMs{0}; // throttle state for yieldDuringDecode()
}; };
// File I/O callbacks use pFile->fHandle to access the HalFile*, // File I/O callbacks use pFile->fHandle to access the HalFile*,
@@ -122,6 +124,8 @@ int jpegDrawCallback(JPEGDRAW* pDraw) {
JpegContext* ctx = reinterpret_cast<JpegContext*>(pDraw->pUser); JpegContext* ctx = reinterpret_cast<JpegContext*>(pDraw->pUser);
if (!ctx || !ctx->config || !ctx->renderer) return 0; if (!ctx || !ctx->config || !ctx->renderer) return 0;
ImageToFramebufferDecoder::yieldDuringDecode(ctx->lastYieldMs);
// In EIGHT_BIT_GRAYSCALE mode, pPixels contains 8-bit grayscale values // In EIGHT_BIT_GRAYSCALE mode, pPixels contains 8-bit grayscale values
// Buffer is densely packed: stride = pDraw->iWidth, valid columns = pDraw->iWidthUsed // Buffer is densely packed: stride = pDraw->iWidth, valid columns = pDraw->iWidthUsed
uint8_t* pixels = reinterpret_cast<uint8_t*>(pDraw->pPixels); uint8_t* pixels = reinterpret_cast<uint8_t*>(pDraw->pPixels);
@@ -494,6 +498,7 @@ bool JpegToFramebufferConverter::decodeToFramebuffer(const std::string& imagePat
} }
unsigned long decodeStart = millis(); unsigned long decodeStart = millis();
ctx.lastYieldMs = decodeStart;
rc = jpeg->decode(0, 0, jpegScaleOption); rc = jpeg->decode(0, 0, jpegScaleOption);
unsigned long decodeTime = millis() - decodeStart; unsigned long decodeTime = millis() - decodeStart;
@@ -7,7 +7,6 @@
#include <Memory.h> #include <Memory.h>
#include <PNGdec.h> #include <PNGdec.h>
#include <algorithm>
#include <cstdlib> #include <cstdlib>
#include <memory> #include <memory>
#include <new> #include <new>
@@ -22,7 +21,6 @@ namespace {
// The draw callback receives this via pDraw->pUser (set by png.decode()). // The draw callback receives this via pDraw->pUser (set by png.decode()).
// The file I/O callbacks receive the HalFile* via pFile->fHandle (set by pngOpen()). // The file I/O callbacks receive the HalFile* via pFile->fHandle (set by pngOpen()).
struct PngContext { struct PngContext {
PNG* decoder{nullptr};
GfxRenderer* renderer{nullptr}; GfxRenderer* renderer{nullptr};
const RenderConfig* config{nullptr}; const RenderConfig* config{nullptr};
int screenWidth{0}; int screenWidth{0};
@@ -32,18 +30,16 @@ struct PngContext {
float scale{1.f}; float scale{1.f};
int srcWidth{0}; int srcWidth{0};
int srcHeight{0}; int srcHeight{0};
int cropLeft{0};
int cropTop{0};
int visibleWidth{0};
int visibleHeight{0};
int dstWidth{0}; int dstWidth{0};
int dstHeight{0}; int dstHeight{0};
int lastDstY{-1}; // Track last rendered destination Y to avoid duplicates int lastDstY{-1}; // Track last rendered destination Y to avoid duplicates
PixelCache cache; PixelCache cache;
bool caching{false}; bool caching{false};
uint8_t* grayLineBuffer{nullptr}; uint8_t* grayLineBuffer{nullptr};
uint8_t* alphaLineBuffer{nullptr};
uint32_t lastYieldMs{0}; // throttle state for yieldDuringDecode()
}; };
// File I/O callbacks use pFile->fHandle to access the HalFile*, // File I/O callbacks use pFile->fHandle to access the HalFile*,
@@ -137,31 +133,19 @@ uint8_t expandSampleToByte(uint8_t sample, int bitsPerSample) {
return static_cast<uint8_t>((sample * 255U) / maxSample); return static_cast<uint8_t>((sample * 255U) / maxSample);
} }
uint8_t alphaThreshold4x4(const int x, const int y) { // Convert entire source line to grayscale with alpha blending to white background.
static constexpr uint8_t BAYER_4X4[16] = {0, 128, 32, 160, 192, 64, 224, 96, 48, 176, 16, 144, 240, 112, 208, 80};
return BAYER_4X4[((y & 0x03) << 2) | (x & 0x03)];
}
// Convert an entire source line to grayscale and optionally retain alpha.
// Low-bit-depth grayscale/indexed scanlines are packed most-significant sample first. // Low-bit-depth grayscale/indexed scanlines are packed most-significant sample first.
// For indexed PNGs with tRNS chunk, alpha values are stored at palette[768] onwards. // For indexed PNGs with tRNS chunk, alpha values are stored at palette[768] onwards.
// Processing the whole line at once improves cache locality and reduces per-pixel overhead. // Processing the whole line at once improves cache locality and reduces per-pixel overhead.
void convertLineToGray(const uint8_t* pPixels, uint8_t* grayLine, int width, int pixelType, int bitsPerSample, void convertLineToGray(const uint8_t* pPixels, uint8_t* grayLine, int width, int pixelType, int bitsPerSample,
uint8_t* palette, int hasAlpha, uint32_t transparentColor, uint8_t* alphaLine) { uint8_t* palette, int hasAlpha) {
const auto store = [grayLine, alphaLine](const int x, const uint8_t gray, const uint8_t alpha) {
grayLine[x] = alphaLine ? gray : static_cast<uint8_t>((gray * alpha + 255u * (255u - alpha)) / 255u);
if (alphaLine) alphaLine[x] = alpha;
};
switch (pixelType) { switch (pixelType) {
case PNG_PIXEL_GRAYSCALE: case PNG_PIXEL_GRAYSCALE:
if (bitsPerSample == 8 && !hasAlpha && !alphaLine) { if (bitsPerSample == 8) {
memcpy(grayLine, pPixels, width); memcpy(grayLine, pPixels, width);
} else { } else {
for (int x = 0; x < width; x++) { for (int x = 0; x < width; x++) {
const uint8_t sample = readPackedSample(pPixels, x, bitsPerSample); grayLine[x] = expandSampleToByte(readPackedSample(pPixels, x, bitsPerSample), bitsPerSample);
const uint8_t gray = expandSampleToByte(sample, bitsPerSample);
store(x, gray, hasAlpha && sample == static_cast<uint8_t>(transparentColor) ? 0 : 255);
} }
} }
break; break;
@@ -169,44 +153,53 @@ void convertLineToGray(const uint8_t* pPixels, uint8_t* grayLine, int width, int
case PNG_PIXEL_TRUECOLOR: case PNG_PIXEL_TRUECOLOR:
for (int x = 0; x < width; x++) { for (int x = 0; x < width; x++) {
const uint8_t* p = &pPixels[x * 3]; const uint8_t* p = &pPixels[x * 3];
const uint8_t gray = static_cast<uint8_t>((p[0] * 77 + p[1] * 150 + p[2] * 29) >> 8); grayLine[x] = (uint8_t)((p[0] * 77 + p[1] * 150 + p[2] * 29) >> 8);
const uint32_t color = (static_cast<uint32_t>(p[0]) << 16) | (static_cast<uint32_t>(p[1]) << 8) | p[2];
store(x, gray, hasAlpha && color == transparentColor ? 0 : 255);
} }
break; break;
case PNG_PIXEL_INDEXED: case PNG_PIXEL_INDEXED:
if (palette) { if (palette) {
for (int x = 0; x < width; x++) { if (hasAlpha) {
const uint8_t idx = readPackedSample(pPixels, x, bitsPerSample); for (int x = 0; x < width; x++) {
const uint8_t* p = &palette[idx * 3]; uint8_t idx = readPackedSample(pPixels, x, bitsPerSample);
const uint8_t gray = static_cast<uint8_t>((p[0] * 77 + p[1] * 150 + p[2] * 29) >> 8); uint8_t* p = &palette[idx * 3];
store(x, gray, hasAlpha ? palette[768 + idx] : 255); uint8_t gray = (uint8_t)((p[0] * 77 + p[1] * 150 + p[2] * 29) >> 8);
uint8_t alpha = palette[768 + idx];
grayLine[x] = (uint8_t)((gray * alpha + 255 * (255 - alpha)) / 255);
}
} else {
for (int x = 0; x < width; x++) {
uint8_t idx = readPackedSample(pPixels, x, bitsPerSample);
uint8_t* p = &palette[idx * 3];
grayLine[x] = (uint8_t)((p[0] * 77 + p[1] * 150 + p[2] * 29) >> 8);
}
} }
} else { } else {
for (int x = 0; x < width; x++) { for (int x = 0; x < width; x++) {
store(x, expandSampleToByte(readPackedSample(pPixels, x, bitsPerSample), bitsPerSample), 255); grayLine[x] = expandSampleToByte(readPackedSample(pPixels, x, bitsPerSample), bitsPerSample);
} }
} }
break; break;
case PNG_PIXEL_GRAY_ALPHA: case PNG_PIXEL_GRAY_ALPHA:
for (int x = 0; x < width; x++) { for (int x = 0; x < width; x++) {
store(x, pPixels[x * 2], pPixels[x * 2 + 1]); uint8_t gray = pPixels[x * 2];
uint8_t alpha = pPixels[x * 2 + 1];
grayLine[x] = (uint8_t)((gray * alpha + 255 * (255 - alpha)) / 255);
} }
break; break;
case PNG_PIXEL_TRUECOLOR_ALPHA: case PNG_PIXEL_TRUECOLOR_ALPHA:
for (int x = 0; x < width; x++) { for (int x = 0; x < width; x++) {
const uint8_t* p = &pPixels[x * 4]; const uint8_t* p = &pPixels[x * 4];
const uint8_t gray = static_cast<uint8_t>((p[0] * 77 + p[1] * 150 + p[2] * 29) >> 8); uint8_t gray = (uint8_t)((p[0] * 77 + p[1] * 150 + p[2] * 29) >> 8);
store(x, gray, p[3]); uint8_t alpha = p[3];
grayLine[x] = (uint8_t)((gray * alpha + 255 * (255 - alpha)) / 255);
} }
break; break;
default: default:
memset(grayLine, 128, width); memset(grayLine, 128, width);
if (alphaLine) memset(alphaLine, 255, width);
break; break;
} }
} }
@@ -215,31 +208,29 @@ int pngDrawCallback(PNGDRAW* pDraw) {
PngContext* ctx = reinterpret_cast<PngContext*>(pDraw->pUser); PngContext* ctx = reinterpret_cast<PngContext*>(pDraw->pUser);
if (!ctx || !ctx->config || !ctx->renderer || !ctx->grayLineBuffer) return 0; if (!ctx || !ctx->config || !ctx->renderer || !ctx->grayLineBuffer) return 0;
ImageToFramebufferDecoder::yieldDuringDecode(ctx->lastYieldMs);
int srcY = pDraw->y; int srcY = pDraw->y;
int srcWidth = ctx->srcWidth; int srcWidth = ctx->srcWidth;
if (srcY < ctx->cropTop || srcY >= ctx->cropTop + ctx->visibleHeight) return 1;
const int visibleSrcY = srcY - ctx->cropTop;
// Map source rows with the exact output-height ratio. During downscaling, // Map source rows with the exact output-height ratio. During downscaling,
// multiple source rows can select the same output row; during upscaling, one // multiple source rows can select the same output row; during upscaling, one
// source row must be repeated across every output row in its range. Emitting // source row must be repeated across every output row in its range. Emitting
// only the first row of an upscale leaves zero-filled (black) gaps in the // only the first row of an upscale leaves zero-filled (black) gaps in the
// streamed pixel cache. // streamed pixel cache.
int firstDstY = (visibleSrcY * ctx->dstHeight) / ctx->visibleHeight; int firstDstY = (srcY * ctx->dstHeight) / ctx->srcHeight;
int endDstY = firstDstY + 1; int endDstY = firstDstY + 1;
if (ctx->dstHeight > ctx->visibleHeight) { if (ctx->dstHeight > ctx->srcHeight) {
endDstY = ((visibleSrcY + 1) * ctx->dstHeight) / ctx->visibleHeight; endDstY = ((srcY + 1) * ctx->dstHeight) / ctx->srcHeight;
} }
if (firstDstY <= ctx->lastDstY) firstDstY = ctx->lastDstY + 1; if (firstDstY <= ctx->lastDstY) firstDstY = ctx->lastDstY + 1;
if (firstDstY >= endDstY || firstDstY >= ctx->dstHeight) return 1; if (firstDstY >= endDstY || firstDstY >= ctx->dstHeight) return 1;
if (endDstY > ctx->dstHeight) endDstY = ctx->dstHeight; if (endDstY > ctx->dstHeight) endDstY = ctx->dstHeight;
// PNGdec parses tRNS while decoding, after open() returns, so query the // Convert entire source line to grayscale (improves cache locality)
// transparent color here rather than caching its pre-decode value.
const uint32_t transparentColor = ctx->decoder ? ctx->decoder->getTransparentColor() : 0;
convertLineToGray(pDraw->pPixels, ctx->grayLineBuffer, srcWidth, pDraw->iPixelType, pDraw->iBpp, pDraw->pPalette, convertLineToGray(pDraw->pPixels, ctx->grayLineBuffer, srcWidth, pDraw->iPixelType, pDraw->iBpp, pDraw->pPalette,
pDraw->iHasAlpha, transparentColor, ctx->alphaLineBuffer); pDraw->iHasAlpha);
// Render scaled rows using Bresenham-style integer stepping (no floating-point division) // Render scaled rows using Bresenham-style integer stepping (no floating-point division)
int dstWidth = ctx->dstWidth; int dstWidth = ctx->dstWidth;
@@ -274,30 +265,27 @@ int pngDrawCallback(PNGDRAW* pDraw) {
} }
} }
int srcX = ctx->cropLeft; int srcX = 0;
int error = 0; int error = 0;
for (int dstX = 0; dstX < dstWidth; dstX++) { for (int dstX = 0; dstX < dstWidth; dstX++) {
int outX = outXBase + dstX; int outX = outXBase + dstX;
if (outX < screenWidth) { if (outX < screenWidth) {
const uint8_t alpha = ctx->alphaLineBuffer ? ctx->alphaLineBuffer[srcX] : 255; uint8_t gray = ctx->grayLineBuffer[srcX];
if (alpha >= 8 && alpha > alphaThreshold4x4(outX, outY)) {
uint8_t gray = ctx->grayLineBuffer[srcX];
uint8_t ditheredGray; uint8_t ditheredGray;
if (useDithering) { if (useDithering) {
ditheredGray = applyBayerDither4Level(gray, outX, outY); ditheredGray = applyBayerDither4Level(gray, outX, outY);
} else { } else {
ditheredGray = gray / 85; ditheredGray = gray / 85;
if (ditheredGray > 3) ditheredGray = 3; if (ditheredGray > 3) ditheredGray = 3;
}
pw.writePixel(outX, ditheredGray, ctx->alphaLineBuffer != nullptr);
if (caching) cw.writePixel(outX, ditheredGray);
} }
pw.writePixel(outX, ditheredGray);
if (caching) cw.writePixel(outX, ditheredGray);
} }
// Bresenham-style stepping: advance srcX based on ratio visibleWidth/dstWidth // Bresenham-style stepping: advance srcX based on ratio srcWidth/dstWidth
error += ctx->visibleWidth; error += srcWidth;
while (error >= dstWidth) { while (error >= dstWidth) {
error -= dstWidth; error -= dstWidth;
srcX++; srcX++;
@@ -356,7 +344,6 @@ bool PngToFramebufferConverter::decodeToFramebuffer(const std::string& imagePath
} }
PngContext ctx; PngContext ctx;
ctx.decoder = png.get();
ctx.renderer = &renderer; ctx.renderer = &renderer;
ctx.config = &config; ctx.config = &config;
ctx.screenWidth = renderer.getScreenWidth(); ctx.screenWidth = renderer.getScreenWidth();
@@ -377,36 +364,28 @@ bool PngToFramebufferConverter::decodeToFramebuffer(const std::string& imagePath
// Calculate output dimensions // Calculate output dimensions
ctx.srcWidth = png->getWidth(); ctx.srcWidth = png->getWidth();
ctx.srcHeight = png->getHeight(); ctx.srcHeight = png->getHeight();
ctx.cropLeft = static_cast<int>(ctx.srcWidth * std::clamp(config.sourceCropX, 0.0f, 0.99f) / 2.0f);
ctx.cropTop = static_cast<int>(ctx.srcHeight * std::clamp(config.sourceCropY, 0.0f, 0.99f) / 2.0f);
ctx.visibleWidth = ctx.srcWidth - 2 * ctx.cropLeft;
ctx.visibleHeight = ctx.srcHeight - 2 * ctx.cropTop;
if (ctx.visibleWidth <= 0 || ctx.visibleHeight <= 0) {
LOG_ERR("PNG", "PNG crop leaves no visible pixels");
return false;
}
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) // Use exact dimensions as specified (avoids rounding mismatches with pre-calculated sizes)
ctx.dstWidth = config.maxWidth; ctx.dstWidth = config.maxWidth;
ctx.dstHeight = config.maxHeight; ctx.dstHeight = config.maxHeight;
ctx.scale = (float)ctx.dstWidth / ctx.visibleWidth; ctx.scale = (float)ctx.dstWidth / ctx.srcWidth;
} else { } else {
// Calculate scale factor to fit within maxWidth/maxHeight // Calculate scale factor to fit within maxWidth/maxHeight
float scaleX = (float)config.maxWidth / ctx.visibleWidth; float scaleX = (float)config.maxWidth / ctx.srcWidth;
float scaleY = (float)config.maxHeight / ctx.visibleHeight; float scaleY = (float)config.maxHeight / ctx.srcHeight;
ctx.scale = (scaleX < scaleY) ? scaleX : scaleY; ctx.scale = (scaleX < scaleY) ? scaleX : scaleY;
if (ctx.scale > 1.0f) ctx.scale = 1.0f; // Don't upscale if (ctx.scale > 1.0f) ctx.scale = 1.0f; // Don't upscale
ctx.dstWidth = (int)(ctx.visibleWidth * ctx.scale); ctx.dstWidth = (int)(ctx.srcWidth * ctx.scale);
ctx.dstHeight = (int)(ctx.visibleHeight * ctx.scale); ctx.dstHeight = (int)(ctx.srcHeight * ctx.scale);
} }
ctx.lastDstY = -1; // Reset row tracking ctx.lastDstY = -1; // Reset row tracking
const int pixelType = png->getPixelType(); const int pixelType = png->getPixelType();
const int bitsPerSample = png->getBpp(); const int bitsPerSample = png->getBpp();
LOG_DBG("PNG", "PNG %dx%d (visible %dx%d) -> %dx%d (scale %.2f), type: %d, bpp: %d", ctx.srcWidth, ctx.srcHeight, LOG_DBG("PNG", "PNG %dx%d -> %dx%d (scale %.2f), type: %d, bpp: %d", ctx.srcWidth, ctx.srcHeight, ctx.dstWidth,
ctx.visibleWidth, ctx.visibleHeight, ctx.dstWidth, ctx.dstHeight, ctx.scale, pixelType, bitsPerSample); ctx.dstHeight, ctx.scale, pixelType, bitsPerSample);
const int requiredInternal = requiredPngInternalBufferBytes(ctx.srcWidth, pixelType, bitsPerSample); const int requiredInternal = requiredPngInternalBufferBytes(ctx.srcWidth, pixelType, bitsPerSample);
if (requiredInternal > PNG_MAX_BUFFERED_PIXELS) { if (requiredInternal > PNG_MAX_BUFFERED_PIXELS) {
@@ -435,18 +414,12 @@ bool PngToFramebufferConverter::decodeToFramebuffer(const std::string& imagePath
return false; return false;
} }
// tRNS chunks are parsed during decode(), so hasAlpha() is not reliable yet auto grayLineBuffer = makeUniqueNoThrow<uint8_t[]>(grayBufSize);
// for color-key and indexed transparency. Reserve the alpha line whenever if (!grayLineBuffer) {
// the caller requests preservation; opaque pixels simply store alpha 255. LOG_ERR("PNG", "Failed to allocate gray line buffer");
const bool retainAlpha = config.preserveAlpha;
const size_t lineBufferBytes = grayBufSize * (retainAlpha ? 2u : 1u);
auto lineBuffers = makeUniqueNoThrow<uint8_t[]>(lineBufferBytes);
if (!lineBuffers) {
LOG_ERR("PNG", "Failed to allocate PNG line buffers (%u bytes)", static_cast<unsigned>(lineBufferBytes));
return false; return false;
} }
ctx.grayLineBuffer = lineBuffers.get(); ctx.grayLineBuffer = grayLineBuffer.get();
ctx.alphaLineBuffer = retainAlpha ? ctx.grayLineBuffer + grayBufSize : nullptr;
// Stream the pixel cache to disk. PNGdec delivers source scanlines top to // Stream the pixel cache to disk. PNGdec delivers source scanlines top to
// bottom and we emit at most one (downscaled) output row per callback, so the // bottom and we emit at most one (downscaled) output row per callback, so the
@@ -454,7 +427,7 @@ bool PngToFramebufferConverter::decodeToFramebuffer(const std::string& imagePath
// unlike the old full-image buffer it neither competes with the ~44KB decoder // unlike the old full-image buffer it neither competes with the ~44KB decoder
// nor forces larger images to skip caching - which previously meant a full // nor forces larger images to skip caching - which previously meant a full
// re-decode on every one of an image page's ~14 render passes. // re-decode on every one of an image page's ~14 render passes.
ctx.caching = !config.preserveAlpha && !config.cachePath.empty(); ctx.caching = !config.cachePath.empty();
if (ctx.caching) { if (ctx.caching) {
if (!ctx.cache.begin(config.cachePath, ctx.dstWidth, ctx.dstHeight, config.x, config.y, 1)) { if (!ctx.cache.begin(config.cachePath, ctx.dstWidth, ctx.dstHeight, config.x, config.y, 1)) {
LOG_ERR("PNG", "Failed to start cache stream, continuing without caching"); LOG_ERR("PNG", "Failed to start cache stream, continuing without caching");
@@ -463,11 +436,11 @@ bool PngToFramebufferConverter::decodeToFramebuffer(const std::string& imagePath
} }
unsigned long decodeStart = millis(); unsigned long decodeStart = millis();
ctx.lastYieldMs = decodeStart;
rc = png->decode(&ctx, 0); rc = png->decode(&ctx, 0);
unsigned long decodeTime = millis() - decodeStart; unsigned long decodeTime = millis() - decodeStart;
ctx.grayLineBuffer = nullptr; ctx.grayLineBuffer = nullptr;
ctx.alphaLineBuffer = nullptr;
if (rc != PNG_SUCCESS) { if (rc != PNG_SUCCESS) {
LOG_ERR("PNG", "Decode failed: %d", rc); LOG_ERR("PNG", "Decode failed: %d", rc);
@@ -793,7 +793,18 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char*
self->currentPageVisibleOffsetSet = false; self->currentPageVisibleOffsetSet = false;
} }
// Apply top margin from container block // Apply top margin from container block. Clamp it so the image never
// overflows the page bottom: a full-viewport-height image leaves no room
// for the margin, and the break above only fires on non-empty pages, so a
// fresh page would otherwise place the image at y=marginTop and run
// marginTop pixels past viewportHeight. A large bottom reserve (status
// bar / big screen margin) absorbs that overflow silently, but with a
// thin reserve it crosses the physical screen edge and fails
// ImageBlock::render's bounds check, dropping the image entirely.
if (self->currentPageNextY + imageMarginTop + displayHeight > self->viewportHeight) {
const int room = self->viewportHeight - displayHeight - self->currentPageNextY;
imageMarginTop = static_cast<int16_t>(room > 0 ? room : 0);
}
self->currentPageNextY += imageMarginTop; self->currentPageNextY += imageMarginTop;
// Create ImageBlock and add to page // Create ImageBlock and add to page
+4 -7
View File
@@ -727,14 +727,11 @@ void GfxRenderer::drawRect(const int x, const int y, const int width, const int
// Border is inside the rectangle // Border is inside the rectangle
void GfxRenderer::drawRect(const int x, const int y, const int width, const int height, const int lineWidth, void GfxRenderer::drawRect(const int x, const int y, const int width, const int height, const int lineWidth,
const bool state) const { const bool state) const {
// Keep the border inside [x, x+width) like the thin overload: the previous
// right/bottom edges at x+width / y+height sat one pixel outside the rect,
// so stroked boxes looked shifted against fills computed from the rect.
for (int i = 0; i < lineWidth; i++) { for (int i = 0; i < lineWidth; i++) {
drawLine(x + i, y + i, x + width - 1 - i, y + i, state); drawLine(x + i, y + i, x + width - i, y + i, state);
drawLine(x + width - 1 - i, y + i, x + width - 1 - i, y + height - 1 - i, state); drawLine(x + width - i, y + i, x + width - i, y + height - i, state);
drawLine(x + width - 1 - i, y + height - 1 - i, x + i, y + height - 1 - i, state); drawLine(x + width - i, y + height - i, x + i, y + height - i, state);
drawLine(x + i, y + height - 1 - i, x + i, y + i, state); drawLine(x + i, y + height - i, x + i, y + i, state);
} }
} }
-1
View File
@@ -278,7 +278,6 @@ STR_BOOKMARK_REMOVED: "S'ha eliminat el punt de llibre."
STR_OPDS_BROWSER: "Navegador OPDS" STR_OPDS_BROWSER: "Navegador OPDS"
STR_COVER_CUSTOM: "Portada + Personalitzat" STR_COVER_CUSTOM: "Portada + Personalitzat"
STR_QUICK_RESUME: "Represa ràpida" STR_QUICK_RESUME: "Represa ràpida"
STR_TRANSPARENT: "Transparent"
STR_MENU_RECENT_BOOKS: "Llibres recents" STR_MENU_RECENT_BOOKS: "Llibres recents"
STR_REMOVE_FROM_RECENTS: "Vols suprimir-lo de Llibres recents?" STR_REMOVE_FROM_RECENTS: "Vols suprimir-lo de Llibres recents?"
STR_NO_RECENT_BOOKS: "No hi ha llibres recents" STR_NO_RECENT_BOOKS: "No hi ha llibres recents"
-2
View File
@@ -299,14 +299,12 @@ STR_THEME_LYRA_EXTENDED: "Lyra Extended"
STR_SUNLIGHT_FADING_FIX: "Sunlight Fading Fix" STR_SUNLIGHT_FADING_FIX: "Sunlight Fading Fix"
STR_REMAP_FRONT_BUTTONS: "Remap Front Buttons" STR_REMAP_FRONT_BUTTONS: "Remap Front Buttons"
STR_BOOKMARKS: "Bookmarks" STR_BOOKMARKS: "Bookmarks"
STR_NO_BOOKMARKS: "No bookmarks"
STR_BOOKMARK_ADDED: "Bookmark added." STR_BOOKMARK_ADDED: "Bookmark added."
STR_BOOKMARK_REMOVED: "Bookmark removed." STR_BOOKMARK_REMOVED: "Bookmark removed."
STR_OPDS_BROWSER: "OPDS Browser" STR_OPDS_BROWSER: "OPDS Browser"
STR_SEARCH: "Search" STR_SEARCH: "Search"
STR_COVER_CUSTOM: "Cover + Custom" STR_COVER_CUSTOM: "Cover + Custom"
STR_QUICK_RESUME: "Quick Resume" STR_QUICK_RESUME: "Quick Resume"
STR_TRANSPARENT: "Transparent"
STR_MENU_RECENT_BOOKS: "Recent Books" STR_MENU_RECENT_BOOKS: "Recent Books"
STR_REMOVE_FROM_RECENTS: "Remove from Recent Books?" STR_REMOVE_FROM_RECENTS: "Remove from Recent Books?"
STR_NO_RECENT_BOOKS: "No recent books" STR_NO_RECENT_BOOKS: "No recent books"
-328
View File
@@ -1,328 +0,0 @@
_language_name: "Orangutan"
_language_code: "OOK"
_order: "34"
STR_CROSSPOINT: "CrossPoint"
STR_BOOTING: "OOK"
STR_SLEEPING: "Oooook"
STR_ENTERING_SLEEP: "Ook..."
STR_BROWSE_FILES: "Ook? OOK"
STR_FILE_TRANSFER: "Oook!"
STR_SETTINGS_TITLE: "OOK!"
STR_CONTINUE_READING: "Oooook ook"
STR_RESUME: "OOk!"
STR_NO_OPEN_BOOK: "OOK?"
STR_START_READING: "Ook ook"
STR_NO_FILES_FOUND: "Oook?"
STR_SELECT_CHAPTER: "Ook? Oook OOK!"
STR_NO_CHAPTERS: "Ook..."
STR_END_OF_BOOK: "Oooook."
STR_EMPTY_CHAPTER: "OOk?"
STR_INDEXING: "Oook..."
STR_LOADING: "Ooook..."
STR_LOADING_POPUP: "Oook"
STR_LOOKUP: "Ook? Oook! OOK"
STR_WIFI_NETWORKS: "Wi-Fi OOK"
STR_NO_NETWORKS: "Oook?"
STR_NETWORKS_FOUND: "%zu OOK"
STR_SCANNING: "Oook..."
STR_FINDING_SAVED_WIFI: "Oook Wi-Fi..."
STR_CONNECTING: "Ooook..."
STR_CONNECTING_SAVED_WIFI: "OOk Wi-Fi..."
STR_SHOW_NETWORKS: "OOK"
STR_CONNECTED: "Oook!"
STR_CONNECTION_FAILED: "OOk?"
STR_FORGET_NETWORK: "Oook?"
STR_SAVE_PASSWORD: "OOK?"
STR_PRESS_OK_SCAN: "OOk OK"
STR_JOIN_NETWORK: "Oook!"
STR_CREATE_HOTSPOT: "Ooook"
STR_JOIN_DESC: "Oook Wi-Fi"
STR_HOTSPOT_DESC: "Oooook Wi-Fi"
STR_STARTING_HOTSPOT: "Ooook..."
STR_HOTSPOT_MODE: "OOk!"
STR_CONNECT_WIFI_HINT: "Oook Wi-Fi"
STR_OPEN_URL_HINT: "Oook URL"
STR_SCAN_QR_HINT: "Oook QR:"
STR_CALIBRE_WIRELESS: "Calibre OOK"
STR_NETWORK_LEGEND: "* = OOK | + = OOk"
STR_MAC_ADDRESS: "MAC!"
STR_CHECKING_WIFI: "OOk Wi-Fi..."
STR_ENTER_WIFI_PASSWORD: "Oook Wi-Fi"
STR_ADD_HIDDEN_NETWORK: "Oook..."
STR_ENTER_WIFI_SSID: "OOk (SSID)"
STR_TO_PREFIX: "Oo "
STR_CALIBRE_RECEIVING: "Oook: "
STR_CALIBRE_RECEIVED: "OOK!: "
STR_CAT_DISPLAY: "OOK"
STR_CAT_READER: "OOk"
STR_CAT_CONTROLS: "Ook!"
STR_CAT_SYSTEM: "Oook"
STR_SLEEP_SCREEN: "Oooook"
STR_NIGHT_MODE: "Ook!"
STR_QUICK_RESUME_TIMEOUT: "Oook ook"
STR_SLEEP_COVER_MODE: "Ook ook"
STR_HIDE_BATTERY: "OOk %"
STR_EXTRA_SPACING: "Oook ook"
STR_TEXT_AA: "Ook-ook"
STR_IMAGES: "OOk!"
STR_IMAGES_DISPLAY: "Oook"
STR_IMAGES_PLACEHOLDER: "Ooook"
STR_IMAGES_SUPPRESS: "OOK"
STR_EOB_HOME: "Oook"
STR_EOB_CONTINUE_WITH: "Oook ook"
STR_SHORT_PWR_BTN: "Oooook OOK"
STR_ORIENTATION: "Ook? Oooook OOK"
STR_SIDE_BTN_LAYOUT: "Oook ook"
STR_TOUCH_READER_CONTROLS: "OOk! OOK"
STR_FRONT_BTN_FOLLOW_ORIENTATION: "Oook OOK"
STR_LONG_PRESS_BEHAVIOR: "Oooook OOK"
STR_LONG_PRESS_BEHAVIOR_OFF: "OFF"
STR_LONG_PRESS_BEHAVIOR_SKIP: "Oook!"
STR_LONG_PRESS_BEHAVIOR_ORIENTATION: "Ook? OOK"
STR_LONG_PRESS_MENU: "Oooook!"
STR_FONT_PREVIEW_TEXT: "Ook ook ook."
STR_FONT_FAMILY: "Oook OOK"
STR_FONT_SIZE: "OOk OOK"
STR_TEXT_SETTINGS: "Oooook OOK: Ook!"
STR_FONT: "OOk!"
STR_SIZE: "OOK"
STR_LAYOUT: "Oook"
STR_ALIGNMENT: "Oooook"
STR_STYLE: "OOk!"
STR_NOT_IN_PREVIEW: "Oook?"
STR_LINE_SPACING: "Oook ook"
STR_SCREEN_MARGIN: "OOk ook"
STR_PARA_ALIGNMENT: "Ook OOK"
STR_HYPHENATION: "Ooook"
STR_TIME_TO_SLEEP: "Oook..."
STR_SHOW_HIDDEN_FILES: "OOk?"
STR_REMOVE_READ_FROM_RECENTS: "Oook OOK"
STR_MOVE_FINISHED_TO_READ: "Ook! OOK"
STR_REFRESH_FREQ: "Oook!"
STR_KOREADER_SYNC: "KOReader OOK"
STR_CHECK_UPDATES: "Oook!"
STR_LANGUAGE: "OOk?"
STR_CLEAR_READING_CACHE: "Oook OOK"
STR_USERNAME: "Oook"
STR_PASSWORD: "OOk!"
STR_SYNC_SERVER_URL: "Oook URL"
STR_DOCUMENT_MATCHING: "Oook OOK"
STR_SEND_METADATA: "Ook! OOK"
STR_SYNC_BEHAVIOR: "OOk ook"
STR_ASK_EVERY_TIME: "Oook?"
STR_SMART_SYNC: "Oook!"
STR_AUTHENTICATE: "Oooook"
STR_SIGN_UP: "OOk!"
STR_KOREADER_USERNAME: "KOReader OOK"
STR_KOREADER_PASSWORD: "KOReader OOk"
STR_FILENAME: "Oook"
STR_KOREADER_AUTH: "KOReader OOK"
STR_DONE: "Oook"
STR_DARK: "OOk!"
STR_LIGHT: "Oook"
STR_CUSTOM: "Ooook"
STR_COVER: "OOk?"
STR_NONE_OPT: "OOk!"
STR_FIT: "OOk"
STR_CROP: "OOK!"
STR_NEVER: "Oook!"
STR_IN_READER: "Ook? OOK"
STR_ALWAYS: "Oooook"
STR_IGNORE: "OOk?"
STR_SLEEP: "Oook!"
STR_PAGE_TURN: "Oook OOK"
STR_FORCE_REFRESH: "Ook! OOK"
STR_PORTRAIT: "Oook"
STR_LANDSCAPE_CW: "Oook OOK"
STR_INVERTED: "OOk!"
STR_ORIENTATION_INVERTED: "Ook 180°"
STR_LANDSCAPE_CCW: "OOk OOK"
STR_PREV_NEXT: "OOk/OOK"
STR_NEXT_PREV: "OOK/OOk"
STR_KOSYNC: "KOSync"
STR_BOOKMARK_OPTION: "Oook!"
STR_DICTIONARY: "Oooook"
STR_DISABLED: "OOk!"
STR_NOTO_SERIF: "Noto OOK"
STR_NOTO_SANS: "Noto OOk"
STR_TIGHT: "OOk!"
STR_NORMAL: "Oook"
STR_WIDE: "OOk!"
STR_JUSTIFY: "Ooook!"
STR_ALIGN_LEFT: "OOk"
STR_CENTER: "Oook"
STR_ALIGN_RIGHT: "OOK!"
STR_PAGES_1: "1 OOK"
STR_PAGES_5: "5 OOk"
STR_PAGES_10: "10 OOK"
STR_PAGES_15: "15 OOk"
STR_PAGES_30: "30 OooK"
STR_UPDATE: "Oook!"
STR_CURRENT_VERSION: "OOK: "
STR_NEW_VERSION: "OOk: "
STR_DOWNLOADING: "Oooook..."
STR_UNNAMED: "OOk?"
STR_HOLD_OPEN_TO_DELETE: "Oook OOK"
STR_NEXT_PAGE: "OOk »"
STR_PREV_PAGE: "« OOK"
STR_NETWORK_PREFIX: "OOk: "
STR_IP_ADDRESS_PREFIX: "IP: "
STR_SD_CARD: "SD OOK"
STR_BACK: "« OOK"
STR_EXIT: "« OOk"
STR_HOME: "« Oook"
STR_SELECT: "OOk!"
STR_SELECTED: "Oooook"
STR_TOGGLE: "Oook!"
STR_TOGGLE_BOOKMARK: "Ook? OOK! Oooook"
STR_CONFIRM: "Oooook"
STR_CANCEL: "OOk?"
STR_CONNECT: "Oooook"
STR_OPEN: "Oook"
STR_DOWNLOAD: "Oooook"
STR_RETRY: "Oook!"
STR_YES: "OOk"
STR_NO: "Oo"
STR_SHOW: "Oook"
STR_HIDE: "OOk!"
STR_STATE_ON: "OK"
STR_STATE_OFF: "OOk"
STR_NOT_SET: "Oook"
STR_DIR_LEFT: "OOk!"
STR_DIR_RIGHT: "OOK!"
STR_DIR_UP: "OK"
STR_DIR_DOWN: "OOk!"
STR_OK_BUTTON: "OK"
STR_KEY_SHIFT: "OOk!"
STR_KEY_MODE_SYMBOLS: "?123"
STR_KEY_MODE_ABC: "abc"
STR_SLEEP_COVER_FILTER: "Oook OOK"
STR_FILTER_CONTRAST: "Oooook"
STR_CUSTOMISE_STATUS_BAR: "Oook OOK"
STR_CHAPTER_PAGE_COUNT: "OOk OOK"
STR_BOOK_PROGRESS_PERCENTAGE: "Oook OOK"
STR_PROGRESS_BAR: "Oook OOK"
STR_PROGRESS_BAR_THICKNESS: "Oook OOK"
STR_PROGRESS_BAR_THIN: "OOk!"
STR_PROGRESS_BAR_MEDIUM: "Oook"
STR_PROGRESS_BAR_THICK: "Oook!"
STR_BOOK: "Oook"
STR_CHAPTER: "OOk!"
STR_EXAMPLE_CHAPTER: "OOk 21"
STR_EXAMPLE_BOOK: "Oook OOK"
STR_PREVIEW: "Oook!"
STR_TITLE: "OOk?"
STR_BATTERY: "Oook"
STR_XTC_STATUS_BAR: "XTC OOK"
STR_BOTTOM: "Oooook"
STR_TOP: "OOk"
STR_CLOCK: "Oook!"
STR_CLOCK_UTC_OFFSET: "Oook OOK"
STR_CLOCK_FORMAT: "OOk OOK"
STR_CLOCK_FORMAT_24H: "24-OOK"
STR_CLOCK_FORMAT_12H: "12-OOk"
STR_CURRENT_TIME: "OOk:"
STR_NEXT_FIELD: "Oook"
STR_CLOCK_SYNC: "OOk OOK"
STR_CLOCK_SYNC_NOW: "Oook OOK"
STR_CLOCK_SYNCING: "Oooook..."
STR_CLOCK_SYNC_OK: "Oook!"
STR_CLOCK_SYNC_FAIL: "OOk?"
STR_CLOCK_SYNC_NO_WIFI: "Wi-Fi OOK"
STR_CLOCK_SYNCED: "Oooook"
STR_UI_THEME: "OOk OOK"
STR_THEME_CLASSIC: "Oook!"
STR_THEME_LYRA: "OOk!"
STR_THEME_ROUNDEDRAFF: "Oook"
STR_THEME_LYRA_EXTENDED: "Oook OOK"
STR_SUNLIGHT_FADING_FIX: "Oook OOK"
STR_REMAP_FRONT_BUTTONS: "Oook OOK"
STR_BOOKMARKS: "Ook! OOK Oooook"
STR_NO_BOOKMARKS: "OOk?"
STR_BOOKMARK_ADDED: "Ook!"
STR_BOOKMARK_REMOVED: "Oook!"
STR_OPDS_BROWSER: "OPDS OOK"
STR_SEARCH: "Oook!"
STR_COVER_CUSTOM: "OOk + OOK"
STR_QUICK_RESUME: "Oook OOK"
STR_MENU_RECENT_BOOKS: "Oook OOK"
STR_REMOVE_FROM_RECENTS: "OOk?"
STR_NO_RECENT_BOOKS: "Oook?"
STR_CALIBRE_DESC: "Calibre OOK"
STR_FORGET_AND_REMOVE: "Oook?"
STR_FORGET_BUTTON: "Oook!"
STR_CALIBRE_STARTING: "Calibre OOK"
STR_CALIBRE_SETUP: "OOk!"
STR_CALIBRE_STATUS: "Oook"
STR_CLEAR_BUTTON: "Oook!"
STR_DEFAULT_VALUE: "OOk OOK"
STR_UNASSIGNED: "OOk?"
STR_ALREADY_ASSIGNED: "Oook OOK"
STR_GO_TO_PERCENT: "Oook! Ook? %"
STR_GO_HOME_BUTTON: "Oook OOK! Oooook"
STR_SYNC_PROGRESS: "Oooook OOK! Ook!"
STR_DELETE_CACHE: "Oook OOK? Oooook!"
STR_DELETE: "Oooook"
STR_CONFIRM_DELETE_BOOKMARK: "Oook?"
STR_DISPLAY_QR: "Oooook OOK QR!"
STR_CHAPTER_PREFIX: "OOk: "
STR_BOOK_PREFIX: "Oook: "
STR_SYNCING_TIME: "Oook..."
STR_FETCH_PROGRESS: "OOk..."
STR_UPLOAD_PROGRESS: "Oooook..."
STR_KOREADER_SETUP_HINT: "KOReader OOK"
STR_PROGRESS_FOUND: "Oook!"
STR_REMOTE_LABEL: "OOk:"
STR_LOCAL_LABEL: "Oook:"
STR_PAGE_OVERALL_FORMAT: "OOk %d, %.2f%%"
STR_PAGE_TOTAL_OVERALL_FORMAT: "Oook %d/%d, %.2f%%"
STR_DEVICE_FROM_FORMAT: "OOk: %s"
STR_APPLY_REMOTE: "Oook OOK"
STR_UPLOAD_LOCAL: "OOk OOK"
STR_UPLOAD_PROMPT: "Oook?"
STR_UPLOAD_SUCCESS: "Oooook!"
STR_ALREADY_SYNCED: "Oook!"
STR_SECTION_PREFIX: "OOk "
STR_UPLOAD: "Oooook"
STR_BOOK_S_STYLE: "Oook OOK"
STR_EMBEDDED_STYLE: "OOk OOK"
STR_FOCUS_READING: "Oook OOK"
STR_OPDS_SERVER_URL: "OPDS OOK"
STR_PWR_BTN_FOOTNOTE_BACK: "Oook OOK"
STR_BACK_SHORT_TO_FILE_BROWSER: "OOk OOK"
STR_SET_SLEEP_COVER: "Oook OOK"
STR_FOOTNOTES: "Ook! Oook OOK"
STR_NO_FOOTNOTES: "Oook?"
STR_LINK: "[Ook]"
STR_SCREENSHOT_BUTTON: "Oook! OOK? Ook!"
STR_SLEEP_TIMER_VALUE_FORMAT: "%u OOK"
STR_SLEEP_NEVER: "Oook!"
STR_ADD_SERVER: "Oook!"
STR_SERVER_NAME: "OOk OOK"
STR_DELETE_SERVER: "Oook OOK"
STR_OPDS_SERVERS: "OPDS OOK"
STR_OPDS_DOWNLOAD_FOLDER: "Oook OOK"
STR_OPDS_FILENAME_FORMAT: "OOk OOK"
STR_FMT_AUTHOR_TITLE: "OOk - OOK"
STR_FMT_TITLE_AUTHOR: "OOK - OOk"
STR_FMT_TITLE: "Oook"
STR_OPDS_SD_ROOT: "SD OOK"
STR_AUTO_TURN_ENABLED: "OOk: "
STR_AUTO_TURN_PAGES_PER_MIN: "Oooook OOK (Ook!)"
STR_MANAGE_FONTS: "Oook OOK"
STR_FONT_BROWSER: "OOk OOK"
STR_LOADING_FONT_LIST: "Oooook..."
STR_NO_FONTS_AVAILABLE: "Oook?"
STR_FONT_INSTALLED: "Oook!"
STR_INSTALLED: "OOk!"
STR_DOWNLOAD_ALL: "Oook OOK"
STR_UPDATE_ALL: "OOk OOK"
STR_UPDATE_AVAILABLE: "Oook!"
STR_TILT_PAGE_TURN: "Oook OOK"
STR_SD_FIRMWARE_UPDATE: "SD Oooook Ooook Ooooook"
STR_STEP_HINT_FRONT: "Ooook Ooooook:"
STR_STEP_HINT_SIDE: "OOK Oook:"
-1
View File
@@ -303,7 +303,6 @@ STR_OPDS_BROWSER: "Navegador OPDS"
STR_SEARCH: "Buscar" STR_SEARCH: "Buscar"
STR_COVER_CUSTOM: "Portada + Pers." STR_COVER_CUSTOM: "Portada + Pers."
STR_QUICK_RESUME: "Reanudación rápida" STR_QUICK_RESUME: "Reanudación rápida"
STR_TRANSPARENT: "Transparente"
STR_MENU_RECENT_BOOKS: "Libros recientes" STR_MENU_RECENT_BOOKS: "Libros recientes"
STR_REMOVE_FROM_RECENTS: "¿Eliminar de Libros recientes?" STR_REMOVE_FROM_RECENTS: "¿Eliminar de Libros recientes?"
STR_NO_RECENT_BOOKS: "No hay libros recientes" STR_NO_RECENT_BOOKS: "No hay libros recientes"
-1
View File
@@ -283,7 +283,6 @@ STR_BOOKMARK_REMOVED: "S'ha eliminat el punt de llibre."
STR_OPDS_BROWSER: "Navegador OPDS" STR_OPDS_BROWSER: "Navegador OPDS"
STR_COVER_CUSTOM: "Portada + Personalitzat" STR_COVER_CUSTOM: "Portada + Personalitzat"
STR_QUICK_RESUME: "Represa ràpida" STR_QUICK_RESUME: "Represa ràpida"
STR_TRANSPARENT: "Transparent"
STR_MENU_RECENT_BOOKS: "Llibres recents" STR_MENU_RECENT_BOOKS: "Llibres recents"
STR_REMOVE_FROM_RECENTS: "Vols eliminar-lo de Llibres recents?" STR_REMOVE_FROM_RECENTS: "Vols eliminar-lo de Llibres recents?"
STR_NO_RECENT_BOOKS: "No hi ha llibres recents" STR_NO_RECENT_BOOKS: "No hi ha llibres recents"
Loaded 20 of 132 files, more files were not shown because too many files have changed in this diff. Show more