Compare commits
59
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fd00c185f2 | ||
|
|
f180069643 | ||
|
|
a0ec5812b1 | ||
|
|
e84840b473 | ||
|
|
94ae750e89 | ||
|
|
8c5cd67f38 | ||
|
|
c5787d1cc1 | ||
|
|
2a19eb9e0d | ||
|
|
fae3423dec | ||
|
|
932a472835 | ||
|
|
552b2683e6 | ||
|
|
bfa1d706b1 | ||
|
|
73ab9ab20d | ||
|
|
3a97f3e63e | ||
|
|
39ea4b045f | ||
|
|
444d87de82 | ||
|
|
859f6cb0d5 | ||
|
|
d8139e9605 | ||
|
|
b596f21e61 | ||
|
|
5fbc657aeb | ||
|
|
bb06219c9c | ||
|
|
5202bbf911 | ||
|
|
e0253a8664 | ||
|
|
e142e88ce7 | ||
|
|
6add0b91bc | ||
|
|
287457f7dd | ||
|
|
3e627112f6 | ||
|
|
1f5669a08a | ||
|
|
0dc92c142c | ||
|
|
4b34a576eb | ||
|
|
4fb843ef92 | ||
|
|
a43061305a | ||
|
|
7ec07b4d9d | ||
|
|
c2c1badc7e | ||
|
|
5776911af0 | ||
|
|
3c5c5e8aa7 | ||
|
|
6f5c5a0900 | ||
|
|
f2b00f6b93 | ||
|
|
44ff313740 | ||
|
|
ca1b833126 | ||
|
|
3e1dd31e53 | ||
|
|
685d4e88f9 | ||
|
|
79b4d63ee2 | ||
|
|
57447a56a6 | ||
|
|
5c86bfe1fd | ||
|
|
4cdefb137c | ||
|
|
e7f3406704 | ||
|
|
6f08a88b12 | ||
|
|
e4d3981469 | ||
|
|
c9d27826f9 | ||
|
|
13bd00bf9d | ||
|
|
fef39da23c | ||
|
|
fd43ca2fe1 | ||
|
|
28255061fb | ||
|
|
43ff1b6644 | ||
|
|
2754a5ff01 | ||
|
|
6edeed73c5 | ||
|
|
8ac302018c | ||
|
|
a11517a62d |
@@ -2,6 +2,8 @@
|
|||||||
.idea
|
.idea
|
||||||
.DS_Store
|
.DS_Store
|
||||||
.vscode
|
.vscode
|
||||||
|
open-x4-sdk
|
||||||
|
fs_
|
||||||
lib/EpdFont/fontsrc
|
lib/EpdFont/fontsrc
|
||||||
lib/I18n/I18nKeys.h
|
lib/I18n/I18nKeys.h
|
||||||
lib/I18n/I18nStrings.h
|
lib/I18n/I18nStrings.h
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ CrossPoint is open-source e-reader firmware - community-built, fully hackable, f
|
|||||||
|
|
||||||

|

|
||||||
|
|
||||||
|
> If you're planning to buy an Xteink device, consider purchasing an **X3/X4 Developer Edition** through https://crosspointreader.com. CrossPoint receives a small share of each sale, helping fund development costs.
|
||||||
|
|
||||||
## What can CrossPoint do?
|
## What can CrossPoint do?
|
||||||
|
|
||||||
- **Reader engine**: EPUB 2/3 rendering with embedded-style option, image handling, hyphenation, kerning, chapter navigation, footnotes, bookmarks, go-to-percent, auto page turn, orientation control, focus reading, KOReader progress sync and more.
|
- **Reader engine**: EPUB 2/3 rendering with embedded-style option, image handling, hyphenation, kerning, chapter navigation, footnotes, bookmarks, go-to-percent, auto page turn, orientation control, focus reading, KOReader progress sync and more.
|
||||||
@@ -67,10 +69,6 @@ USB port or browser before assuming the device is locked. Only reach for the unl
|
|||||||
> Flashing any other firmware on a USB-locked device may **permanently brick the device** or leave it **permanently
|
> Flashing any other firmware on a USB-locked device may **permanently brick the device** or leave it **permanently
|
||||||
> stuck on that firmware with no recovery path**. Once USB flashing is re-locked, your only way back is via OTA, and if
|
> stuck on that firmware with no recovery path**. Once USB flashing is re-locked, your only way back is via OTA, and if
|
||||||
> the firmware you flashed doesn't support OTA, **there is no way out**.
|
> the firmware you flashed doesn't support OTA, **there is no way out**.
|
||||||
>
|
|
||||||
> **The Papyrix fork has removed OTA update support from its code.** If you flash Papyrix onto a
|
|
||||||
> USB-locked unit, you will have **zero update or recovery path** and will be stuck on it forever. **Do not flash
|
|
||||||
> Papyrix (or any other unsupported firmware) on a locked device.**
|
|
||||||
|
|
||||||
## Install firmware
|
## Install firmware
|
||||||
|
|
||||||
@@ -249,7 +247,7 @@ One of the best things about open source is that anyone can take the code in a d
|
|||||||
|
|
||||||
- [papyrix-reader](https://github.com/bigbag/papyrix-reader) — Adds FB2 and MD format support. Actively maintained with Arabic script support. Custom themes via SD card.
|
- [papyrix-reader](https://github.com/bigbag/papyrix-reader) — Adds FB2 and MD format support. Actively maintained with Arabic script support. Custom themes via SD card.
|
||||||
|
|
||||||
- [crosspet](https://github.com/trilwu/crosspet) — A Vietnamese fork that adds a Tamagotchi-style virtual chicken that grows based on your reading milestones (pages read, streaks, care). Also: Flashcards, Weather, Pomodoro timer, and mini-games.
|
- ~~[crosspet](https://github.com/trilwu/crosspet) — A Vietnamese fork that adds a Tamagotchi-style virtual chicken that grows based on your reading milestones (pages read, streaks, care). Also: Flashcards, Weather, Pomodoro timer, and mini-games.~~ (Unmaintained)
|
||||||
|
|
||||||
- [crosspoint-reader-cjk](https://github.com/aBER0724/crosspoint-reader-cjk) — Purpose-built for Chinese, Japanese, and Korean reading.
|
- [crosspoint-reader-cjk](https://github.com/aBER0724/crosspoint-reader-cjk) — Purpose-built for Chinese, Japanese, and Korean reading.
|
||||||
|
|
||||||
|
|||||||
+21
-12
@@ -5,6 +5,7 @@ Welcome to the **CrossPoint** firmware. This guide outlines the hardware control
|
|||||||
- [CrossPoint User Guide](#crosspoint-user-guide)
|
- [CrossPoint User Guide](#crosspoint-user-guide)
|
||||||
- [1. Hardware Overview](#1-hardware-overview)
|
- [1. Hardware Overview](#1-hardware-overview)
|
||||||
- [Button Layout](#button-layout)
|
- [Button Layout](#button-layout)
|
||||||
|
- [Taking a Screenshot](#taking-a-screenshot)
|
||||||
- [2. Power \& Startup](#2-power--startup)
|
- [2. Power \& Startup](#2-power--startup)
|
||||||
- [Power On / Off](#power-on--off)
|
- [Power On / Off](#power-on--off)
|
||||||
- [First Launch](#first-launch)
|
- [First Launch](#first-launch)
|
||||||
@@ -14,7 +15,11 @@ Welcome to the **CrossPoint** firmware. This guide outlines the hardware control
|
|||||||
- [3.3 Browse Files Screen](#33-browse-files-screen)
|
- [3.3 Browse Files Screen](#33-browse-files-screen)
|
||||||
- [3.4 Recent Books Screen](#34-recent-books-screen)
|
- [3.4 Recent Books Screen](#34-recent-books-screen)
|
||||||
- [3.5 File Transfer Screen](#35-file-transfer-screen)
|
- [3.5 File Transfer Screen](#35-file-transfer-screen)
|
||||||
- [3.5.1 Calibre Wireless Transfers](#351-calibre-wireless-transfers)
|
- [3.5.1 Calibre Wireless Transfers](#351-calibre-wireless-transfers)
|
||||||
|
- [Installing the Plugin in Calibre](#installing-the-plugin-in-calibre)
|
||||||
|
- [Configuring the CrossPoint Plugin in Calibre](#configuring-the-crosspoint-plugin-in-calibre)
|
||||||
|
- [Uploading Books](#uploading-books)
|
||||||
|
- [Removing a Book](#removing-a-book)
|
||||||
- [3.6 Settings](#36-settings)
|
- [3.6 Settings](#36-settings)
|
||||||
- [3.6.1 Display](#361-display)
|
- [3.6.1 Display](#361-display)
|
||||||
- [3.6.2 Reader](#362-reader)
|
- [3.6.2 Reader](#362-reader)
|
||||||
@@ -23,21 +28,25 @@ Welcome to the **CrossPoint** firmware. This guide outlines the hardware control
|
|||||||
- [3.6.5 OPDS Servers (Multiple Libraries)](#365-opds-servers-multiple-libraries)
|
- [3.6.5 OPDS Servers (Multiple Libraries)](#365-opds-servers-multiple-libraries)
|
||||||
- [3.6.6 Web Settings (Wi-Fi + OPDS)](#366-web-settings-wi-fi--opds)
|
- [3.6.6 Web Settings (Wi-Fi + OPDS)](#366-web-settings-wi-fi--opds)
|
||||||
- [3.6.7 KOReader Sync Quick Setup](#367-koreader-sync-quick-setup)
|
- [3.6.7 KOReader Sync Quick Setup](#367-koreader-sync-quick-setup)
|
||||||
|
- [Option A: Free Public Server (`sync.koreader.rocks`)](#option-a-free-public-server-synckoreaderrocks)
|
||||||
|
- [Option B: Self-Hosted Server (Docker Compose)](#option-b-self-hosted-server-docker-compose)
|
||||||
- [3.7 Sleep Screen](#37-sleep-screen)
|
- [3.7 Sleep Screen](#37-sleep-screen)
|
||||||
|
- [Cover settings](#cover-settings)
|
||||||
|
- [Custom images](#custom-images)
|
||||||
- [3.8 Custom Fonts (SD Card)](#38-custom-fonts-sd-card)
|
- [3.8 Custom Fonts (SD Card)](#38-custom-fonts-sd-card)
|
||||||
- [4. Reading Mode](#4-reading-mode)
|
- [4. Reading Mode](#4-reading-mode)
|
||||||
- [Page Turning](#page-turning)
|
- [Page Turning](#page-turning)
|
||||||
- [Chapter Navigation](#chapter-navigation)
|
- [Chapter Navigation](#chapter-navigation)
|
||||||
- [Auto Page Turn](#auto-page-turn)
|
- [Auto Page Turn](#auto-page-turn)
|
||||||
- [Tilt Page Turn (X3 only)](#tilt-page-turn-x3-only)
|
- [Tilt Page Turn (X3 only)](#tilt-page-turn-x3-only)
|
||||||
- [Footnote Navigation](#footnote-navigation)
|
- [Footnote Navigation](#footnote-navigation)
|
||||||
- [System Navigation](#system-navigation)
|
- [System Navigation](#system-navigation)
|
||||||
- [Supported Languages](#supported-languages)
|
- [Supported Languages](#supported-languages)
|
||||||
- [5. Reader Menu](#5-reader-menu)
|
- [5. Reader Menu](#5-reader-menu)
|
||||||
- [5.1 Chapter Selection](#51-chapter-selection)
|
- [5.1 Chapter Selection](#51-chapter-selection)
|
||||||
- [5.2 Bookmarks](#52-bookmarks)
|
- [5.2 Bookmarks](#52-bookmarks)
|
||||||
- [6. Current Limitations & Roadmap](#6-current-limitations--roadmap)
|
- [6. Current Limitations \& Roadmap](#6-current-limitations--roadmap)
|
||||||
- [7. Troubleshooting Issues & Escaping Bootloop](#7-troubleshooting-issues--escaping-bootloop)
|
- [7. Troubleshooting Issues \& Escaping Bootloop](#7-troubleshooting-issues--escaping-bootloop)
|
||||||
|
|
||||||
## 1. Hardware Overview
|
## 1. Hardware Overview
|
||||||
|
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ git ls-files --exclude-standard ${GIT_LS_FILES_FLAGS} \
|
|||||||
| grep -v -E '^lib/EpdFont/builtinFonts/' \
|
| grep -v -E '^lib/EpdFont/builtinFonts/' \
|
||||||
| grep -v -E '^lib/Epub/Epub/hyphenation/generated/' \
|
| grep -v -E '^lib/Epub/Epub/hyphenation/generated/' \
|
||||||
| grep -v -E '^lib/uzlib/' \
|
| grep -v -E '^lib/uzlib/' \
|
||||||
|
| grep -v -E '^lib/miniz/third_party/' \
|
||||||
| xargs -r "${CLANG_FORMAT_BIN}" -style=file -i
|
| xargs -r "${CLANG_FORMAT_BIN}" -style=file -i
|
||||||
# Restore strict pipeline failure handling for the rest of the script.
|
# Restore strict pipeline failure handling for the rest of the script.
|
||||||
set -o pipefail
|
set -o pipefail
|
||||||
|
|||||||
+28
-10
@@ -90,13 +90,19 @@ if (parsedSize != fileSize) {
|
|||||||
|
|
||||||
## `section.bin`
|
## `section.bin`
|
||||||
|
|
||||||
### Version 25
|
### Version 30
|
||||||
|
|
||||||
Each file in `sections/*.bin` stores one laid-out spine section. The header is
|
Each file in `sections/*.bin` stores one laid-out spine section. The header is
|
||||||
also the cache-busting key: if any layout-affecting setting differs from the
|
also the cache-busting key: if any layout-affecting setting differs from the
|
||||||
current reader settings, the section is discarded and rebuilt.
|
current reader settings, the section is discarded and rebuilt.
|
||||||
|
|
||||||
Version 25 includes:
|
Version 30 is binary-identical to version 29. The version was bumped because
|
||||||
|
Arabic contextual shaping changed text measurement (`getTextAdvanceX` now
|
||||||
|
measures the shaped visual text), so word positions cached by v29 no longer
|
||||||
|
match what `drawText` renders.
|
||||||
|
|
||||||
|
Version 28 introduced serialized word style bits for underline, strikethrough,
|
||||||
|
superscript, and subscript. The format also includes:
|
||||||
|
|
||||||
- cache-busting fields for paragraph alignment, hyphenation, embedded CSS,
|
- cache-busting fields for paragraph alignment, hyphenation, embedded CSS,
|
||||||
image rendering mode, and Focus Reading
|
image rendering mode, and Focus Reading
|
||||||
@@ -105,6 +111,12 @@ Version 25 includes:
|
|||||||
- paragraph and list-item LUTs used by KOReader sync page refinement
|
- paragraph and list-item LUTs used by KOReader sync page refinement
|
||||||
- optional per-word Focus Reading split metadata
|
- optional per-word Focus Reading split metadata
|
||||||
- per-page footnote entries
|
- per-page footnote entries
|
||||||
|
- serialized word style bits for underline, strikethrough, superscript, and
|
||||||
|
subscript
|
||||||
|
- flat TextBlock word storage (v29): per-word arrays plus one shared
|
||||||
|
NUL-terminated text blob, replacing v28's length-prefixed word strings. The
|
||||||
|
on-disk order mirrors the in-RAM arena so the firmware reads a whole block
|
||||||
|
payload with a single allocation and a single SD read
|
||||||
|
|
||||||
ImHex pattern:
|
ImHex pattern:
|
||||||
|
|
||||||
@@ -113,7 +125,7 @@ import std.mem;
|
|||||||
import std.string;
|
import std.string;
|
||||||
import std.core;
|
import std.core;
|
||||||
|
|
||||||
#define EXPECTED_VERSION 25
|
#define EXPECTED_VERSION 30
|
||||||
#define MAX_STRING_LENGTH 65535
|
#define MAX_STRING_LENGTH 65535
|
||||||
#define FOOTNOTE_NUMBER_LEN 32
|
#define FOOTNOTE_NUMBER_LEN 32
|
||||||
#define FOOTNOTE_HREF_LEN 96
|
#define FOOTNOTE_HREF_LEN 96
|
||||||
@@ -174,14 +186,20 @@ struct BlockStyle {
|
|||||||
|
|
||||||
struct TextBlock {
|
struct TextBlock {
|
||||||
u16 wordCount;
|
u16 wordCount;
|
||||||
String words[wordCount];
|
|
||||||
s16 wordXPos[wordCount];
|
|
||||||
WordStyle wordStyle[wordCount];
|
|
||||||
|
|
||||||
u8 hasFocus;
|
u8 hasFocus;
|
||||||
if (hasFocus != 0) {
|
u16 textBytes [[comment("Total size of text[], including one NUL per word")]];
|
||||||
u8 wordFocusBoundary[wordCount] [[comment("UTF-8 byte boundary between bold prefix and suffix")]];
|
|
||||||
u16 wordFocusSuffixX[wordCount] [[comment("Suffix x offset from word start")]];
|
if (wordCount > 0) {
|
||||||
|
u16 textOff[wordCount] [[comment("Byte offset of word i's text within text[]")]];
|
||||||
|
s16 wordXPos[wordCount];
|
||||||
|
if (hasFocus != 0) {
|
||||||
|
u16 wordFocusSuffixX[wordCount] [[comment("Suffix x offset from word start")]];
|
||||||
|
}
|
||||||
|
WordStyle wordStyle[wordCount];
|
||||||
|
if (hasFocus != 0) {
|
||||||
|
u8 wordFocusBoundary[wordCount] [[comment("UTF-8 byte boundary between bold prefix and suffix")]];
|
||||||
|
}
|
||||||
|
char text[textBytes] [[comment("All words back to back, each NUL-terminated")]];
|
||||||
}
|
}
|
||||||
|
|
||||||
BlockStyle blockStyle;
|
BlockStyle blockStyle;
|
||||||
|
|||||||
+8
-3
@@ -32,9 +32,14 @@ networks or in hotspot mode when you control who is connected.
|
|||||||
## Join Network Mode
|
## Join Network Mode
|
||||||
|
|
||||||
1. Select **Join Network**.
|
1. Select **Join Network**.
|
||||||
2. Pick a 2.4 GHz Wi-Fi network from the scan results.
|
2. If you have saved Wi-Fi credentials, CrossPoint first tries the last
|
||||||
3. Enter the password if prompted.
|
connected network, then other visible saved networks in signal-strength
|
||||||
4. Save credentials if you want the reader to reconnect automatically next time.
|
order. Press **Back** to cancel or **Confirm** to stop auto-connect and show
|
||||||
|
the network list.
|
||||||
|
3. If the network list is shown, pick a 2.4 GHz Wi-Fi network from the scan
|
||||||
|
results.
|
||||||
|
4. Enter the password if prompted.
|
||||||
|
5. Save credentials if you want the reader to reconnect automatically next time.
|
||||||
|
|
||||||
After connection, the reader shows:
|
After connection, the reader shows:
|
||||||
|
|
||||||
|
|||||||
+1
-1
Submodule freeink-sdk updated: 8be3236609...e7d336191b
@@ -44,16 +44,17 @@ void EpdFont::getTextBounds(const char* string, const int startX, const int star
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const int raiseBy = isCombining ? combiningMark::raiseAboveBase(glyph->top, glyph->height, lastBaseTop) : 0;
|
const combiningMark::Anchor anchor = combiningMark::anchorFor(cp);
|
||||||
|
const int raiseBy = isCombining ? combiningMark::raiseAboveBase(anchor, glyph->top, glyph->height, lastBaseTop) : 0;
|
||||||
|
|
||||||
if (!isCombining && prevCp != 0) {
|
if (!isCombining && prevCp != 0) {
|
||||||
const auto kernFP = getKerning(prevCp, cp); // 4.4 fixed-point kern
|
const auto kernFP = getKerning(prevCp, cp); // 4.4 fixed-point kern
|
||||||
lastBaseX += fp4::toPixel(prevAdvanceFP + kernFP);
|
lastBaseX += fp4::toPixel(prevAdvanceFP + kernFP);
|
||||||
}
|
}
|
||||||
|
|
||||||
const int glyphBaseX =
|
const int glyphBaseX = isCombining ? combiningMark::anchorOver(anchor, lastBaseX, lastBaseLeft, lastBaseWidth,
|
||||||
isCombining ? combiningMark::centerOver(lastBaseX, lastBaseLeft, lastBaseWidth, glyph->left, glyph->width)
|
glyph->left, glyph->width)
|
||||||
: lastBaseX;
|
: lastBaseX;
|
||||||
const int glyphBaseY = startY - raiseBy;
|
const int glyphBaseY = startY - raiseBy;
|
||||||
|
|
||||||
*minX = std::min(*minX, glyphBaseX + glyph->left);
|
*minX = std::min(*minX, glyphBaseX + glyph->left);
|
||||||
|
|||||||
+76
-11
@@ -36,24 +36,71 @@ namespace combiningMark {
|
|||||||
|
|
||||||
constexpr int MIN_GAP_PX = 1;
|
constexpr int MIN_GAP_PX = 1;
|
||||||
|
|
||||||
/// Compute the cursor-X at which to render a combining mark so its bitmap
|
/// Placement of a mark relative to its base glyph. The default heuristic —
|
||||||
/// is visually centered over the base glyph's bitmap.
|
/// centered over the base, raised clear of its top — suits Latin diacritics
|
||||||
constexpr int centerOver(int baseCursorPos, int baseLeft, int baseWidth, int markLeft, int markWidth) {
|
/// and Arabic harakat, but misplaces the Hebrew niqqud whose identity depends
|
||||||
return baseCursorPos + baseLeft + baseWidth / 2 - markWidth / 2 - markLeft;
|
/// on position: dagesh sits inside the letter body, the shin/sin dots
|
||||||
|
/// distinguish the letter by sitting over its right/left arm, and holam hangs
|
||||||
|
/// over the left corner. "Native" anchors keep the glyph's font-designed
|
||||||
|
/// height (which may overlap the base) instead of raising it.
|
||||||
|
enum class Anchor : uint8_t {
|
||||||
|
CenterRaised, ///< centered over the base, lifted above its top (default)
|
||||||
|
CenterNative, ///< centered over the base at font-native height
|
||||||
|
RightNative, ///< right edges aligned, font-native height
|
||||||
|
LeftNative, ///< left edges aligned, font-native height
|
||||||
|
};
|
||||||
|
|
||||||
|
constexpr Anchor anchorFor(const uint32_t cp) {
|
||||||
|
switch (cp) {
|
||||||
|
case 0x05BC: // dagesh / mapiq / shuruk dot: inside the letter body
|
||||||
|
case 0x05BA: // holam haser for vav: straight above the vav stem
|
||||||
|
return Anchor::CenterNative;
|
||||||
|
case 0x05C1: // shin dot: over the letter's right arm
|
||||||
|
return Anchor::RightNative;
|
||||||
|
case 0x05B9: // holam: above the letter's left corner
|
||||||
|
case 0x05C2: // sin dot: over the letter's left arm
|
||||||
|
return Anchor::LeftNative;
|
||||||
|
default:
|
||||||
|
return Anchor::CenterRaised;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Rotated-90CW variant of centerOver. In the rotated coordinate system
|
/// Horizontal offset from the base bitmap's left edge to the mark bitmap's
|
||||||
|
/// left edge for a given anchor.
|
||||||
|
constexpr int anchorShift(const Anchor anchor, const int baseWidth, const int markWidth) {
|
||||||
|
switch (anchor) {
|
||||||
|
case Anchor::LeftNative:
|
||||||
|
return 0;
|
||||||
|
case Anchor::RightNative:
|
||||||
|
return baseWidth - markWidth;
|
||||||
|
default:
|
||||||
|
return baseWidth / 2 - markWidth / 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Compute the cursor-X at which to render a combining mark so its bitmap
|
||||||
|
/// lands at its anchor position over the base glyph's bitmap.
|
||||||
|
constexpr int anchorOver(const Anchor anchor, const int baseCursorPos, const int baseLeft, const int baseWidth,
|
||||||
|
const int markLeft, const int markWidth) {
|
||||||
|
return baseCursorPos + baseLeft + anchorShift(anchor, baseWidth, markWidth) - markLeft;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Rotated-90CW variant of anchorOver. In the rotated coordinate system
|
||||||
/// renderCharImpl uses (cursorY - left) instead of (cursorX + left), so
|
/// renderCharImpl uses (cursorY - left) instead of (cursorX + left), so
|
||||||
/// every left/width term inverts sign.
|
/// every left/width term inverts sign.
|
||||||
constexpr int centerOverRotated90CW(int baseCursorPos, int baseLeft, int baseWidth, int markLeft, int markWidth) {
|
constexpr int anchorOverRotated90CW(const Anchor anchor, const int baseCursorPos, const int baseLeft,
|
||||||
return baseCursorPos - baseLeft - baseWidth / 2 + markWidth / 2 + markLeft;
|
const int baseWidth, const int markLeft, const int markWidth) {
|
||||||
|
return baseCursorPos - baseLeft - anchorShift(anchor, baseWidth, markWidth) + markLeft;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// For combining marks that sit entirely above the baseline, compute how many
|
/// For combining marks that sit entirely above the baseline, compute how many
|
||||||
/// pixels to raise the mark so there is at least MIN_GAP_PX between its bottom
|
/// pixels to raise the mark so there is at least MIN_GAP_PX between its bottom
|
||||||
/// edge and the top of the base glyph. Returns 0 for marks that extend to or
|
/// edge and the top of the base glyph. Returns 0 for marks that extend to or
|
||||||
/// below the baseline (e.g. cedilla, dot-below, ogonek).
|
/// below the baseline (e.g. cedilla, dot-below, ogonek) and for anchors that
|
||||||
constexpr int raiseAboveBase(int markTop, int markHeight, int baseTop) {
|
/// keep the font-native height (dagesh must stay inside the letter, the
|
||||||
|
/// shin/sin dots touch its arms).
|
||||||
|
constexpr int raiseAboveBase(const Anchor anchor, const int markTop, const int markHeight, const int baseTop) {
|
||||||
|
if (anchor != Anchor::CenterRaised) return 0;
|
||||||
if (markTop - markHeight <= 0) return 0;
|
if (markTop - markHeight <= 0) return 0;
|
||||||
const int gap = markTop - markHeight - baseTop;
|
const int gap = markTop - markHeight - baseTop;
|
||||||
return (gap < MIN_GAP_PX) ? (MIN_GAP_PX - gap) : 0;
|
return (gap < MIN_GAP_PX) ? (MIN_GAP_PX - gap) : 0;
|
||||||
@@ -61,6 +108,20 @@ constexpr int raiseAboveBase(int markTop, int markHeight, int baseTop) {
|
|||||||
|
|
||||||
} // namespace combiningMark
|
} // namespace combiningMark
|
||||||
|
|
||||||
|
/// GCC/Clang (the ESP32 firmware toolchain) pack structs with __attribute__((packed)).
|
||||||
|
/// MSVC (host unit tests) has no equivalent attribute and instead needs a #pragma pack
|
||||||
|
/// region achieving the same 1-byte alignment. These macros keep the on-disk font layout
|
||||||
|
/// identical across both toolchains.
|
||||||
|
#if defined(_MSC_VER)
|
||||||
|
#define EPD_PACKED_BEGIN __pragma(pack(push, 1))
|
||||||
|
#define EPD_PACKED_END __pragma(pack(pop))
|
||||||
|
#define EPD_PACKED_ATTR
|
||||||
|
#else
|
||||||
|
#define EPD_PACKED_BEGIN
|
||||||
|
#define EPD_PACKED_END
|
||||||
|
#define EPD_PACKED_ATTR __attribute__((packed))
|
||||||
|
#endif
|
||||||
|
|
||||||
/// Fixed-point conventions used by EpdGlyph and EpdFontData:
|
/// Fixed-point conventions used by EpdGlyph and EpdFontData:
|
||||||
/// advanceX: 12.4 unsigned fixed-point in uint16_t (use fp4::toPixel)
|
/// advanceX: 12.4 unsigned fixed-point in uint16_t (use fp4::toPixel)
|
||||||
/// kernMatrix: 4.4 signed fixed-point in int8_t (use fp4::toPixel)
|
/// kernMatrix: 4.4 signed fixed-point in int8_t (use fp4::toPixel)
|
||||||
@@ -95,17 +156,21 @@ typedef struct {
|
|||||||
|
|
||||||
/// Maps a codepoint to a kerning class ID, sorted by codepoint for binary search.
|
/// Maps a codepoint to a kerning class ID, sorted by codepoint for binary search.
|
||||||
/// Class IDs are 1-based; codepoints not in the table have implicit class 0 (no kerning).
|
/// Class IDs are 1-based; codepoints not in the table have implicit class 0 (no kerning).
|
||||||
|
EPD_PACKED_BEGIN
|
||||||
typedef struct {
|
typedef struct {
|
||||||
uint16_t codepoint; ///< Unicode codepoint
|
uint16_t codepoint; ///< Unicode codepoint
|
||||||
uint8_t classId; ///< 1-based kerning class ID
|
uint8_t classId; ///< 1-based kerning class ID
|
||||||
} __attribute__((packed)) EpdKernClassEntry;
|
} EPD_PACKED_ATTR EpdKernClassEntry;
|
||||||
|
EPD_PACKED_END
|
||||||
|
|
||||||
/// Ligature substitution for a specific glyph pair, sorted by `pair` for binary search.
|
/// Ligature substitution for a specific glyph pair, sorted by `pair` for binary search.
|
||||||
/// `pair` encodes (leftCodepoint << 16 | rightCodepoint) for single-key lookup.
|
/// `pair` encodes (leftCodepoint << 16 | rightCodepoint) for single-key lookup.
|
||||||
|
EPD_PACKED_BEGIN
|
||||||
typedef struct {
|
typedef struct {
|
||||||
uint32_t pair; ///< Packed codepoint pair (left << 16 | right)
|
uint32_t pair; ///< Packed codepoint pair (left << 16 | right)
|
||||||
uint32_t ligatureCp; ///< Codepoint of the replacement ligature glyph
|
uint32_t ligatureCp; ///< Codepoint of the replacement ligature glyph
|
||||||
} __attribute__((packed)) EpdLigaturePair;
|
} EPD_PACKED_ATTR EpdLigaturePair;
|
||||||
|
EPD_PACKED_END
|
||||||
|
|
||||||
/// Data stored for FONT AS A WHOLE
|
/// Data stored for FONT AS A WHOLE
|
||||||
typedef struct {
|
typedef struct {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
#include "EpdFontFamily.h"
|
#include "EpdFontFamily.h"
|
||||||
|
|
||||||
const EpdFont* EpdFontFamily::getFont(const Style style) const {
|
const EpdFont* EpdFontFamily::getFont(const Style style) const {
|
||||||
// Extract font style bits (ignore UNDERLINE bit for font selection)
|
// Extract font style bits; render-time overlay bits do not affect font selection.
|
||||||
const bool hasBold = (style & BOLD) != 0;
|
const bool hasBold = (style & BOLD) != 0;
|
||||||
const bool hasItalic = (style & ITALIC) != 0;
|
const bool hasItalic = (style & ITALIC) != 0;
|
||||||
|
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ class EpdFontFamily {
|
|||||||
SUP = 16, // superscript: glyph scaled 50%, raised ~40% of ascender
|
SUP = 16, // superscript: glyph scaled 50%, raised ~40% of ascender
|
||||||
SUB = 32, // subscript: glyph scaled 50%, lowered ~25% of ascender
|
SUB = 32, // subscript: glyph scaled 50%, lowered ~25% of ascender
|
||||||
};
|
};
|
||||||
|
static constexpr uint8_t TEXT_DECORATION_MASK = static_cast<uint8_t>(UNDERLINE | STRIKETHROUGH);
|
||||||
|
|
||||||
explicit EpdFontFamily(const EpdFont* regular, const EpdFont* bold = nullptr, const EpdFont* italic = nullptr,
|
explicit EpdFontFamily(const EpdFont* regular, const EpdFont* bold = nullptr, const EpdFont* italic = nullptr,
|
||||||
const EpdFont* boldItalic = nullptr)
|
const EpdFont* boldItalic = nullptr)
|
||||||
@@ -27,6 +28,9 @@ class EpdFontFamily {
|
|||||||
const EpdGlyph* getGlyph(uint32_t cp, Style style = REGULAR) const;
|
const EpdGlyph* getGlyph(uint32_t cp, Style style = REGULAR) const;
|
||||||
int8_t getKerning(uint32_t leftCp, uint32_t rightCp, Style style = REGULAR) const;
|
int8_t getKerning(uint32_t leftCp, uint32_t rightCp, Style style = REGULAR) const;
|
||||||
uint32_t applyLigatures(uint32_t cp, const char*& text, Style style = REGULAR) const;
|
uint32_t applyLigatures(uint32_t cp, const char*& text, Style style = REGULAR) const;
|
||||||
|
static constexpr bool hasTextDecoration(const Style style) {
|
||||||
|
return (static_cast<uint8_t>(style) & TEXT_DECORATION_MASK) != 0;
|
||||||
|
}
|
||||||
|
|
||||||
private:
|
private:
|
||||||
const EpdFont* regular;
|
const EpdFont* regular;
|
||||||
|
|||||||
@@ -33,12 +33,24 @@ void FontDecompressor::freePageBuffer() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void FontDecompressor::freeHotGroup() {
|
void FontDecompressor::freeHotGroup() {
|
||||||
hotGroup.clear();
|
free(hotGroup);
|
||||||
hotGroup.shrink_to_fit();
|
hotGroup = nullptr;
|
||||||
|
hotGroupCapacity = 0;
|
||||||
hotGroupFont = nullptr;
|
hotGroupFont = nullptr;
|
||||||
hotGroupIndex = UINT16_MAX;
|
hotGroupIndex = UINT16_MAX;
|
||||||
hotGlyphBuf.clear();
|
free(hotGlyphBuf);
|
||||||
hotGlyphBuf.shrink_to_fit();
|
hotGlyphBuf = nullptr;
|
||||||
|
hotGlyphBufCapacity = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool FontDecompressor::ensureCapacity(uint8_t*& buf, uint32_t& capacity, uint32_t needed) {
|
||||||
|
if (capacity >= needed) return true;
|
||||||
|
// Grow-only, free-then-malloc: every caller fully rewrites the buffer after a grow, so the
|
||||||
|
// old contents are dead -- freeing first gives the allocator its best shot on a tight heap.
|
||||||
|
free(buf);
|
||||||
|
buf = static_cast<uint8_t*>(malloc(needed)); // owned by FontDecompressor, freed in freeHotGroup()
|
||||||
|
capacity = buf ? needed : 0;
|
||||||
|
return buf != nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
uint16_t FontDecompressor::getGroupIndex(const EpdFontData* fontData, uint32_t glyphIndex) {
|
uint16_t FontDecompressor::getGroupIndex(const EpdFontData* fontData, uint32_t glyphIndex) {
|
||||||
@@ -170,24 +182,20 @@ const uint8_t* FontDecompressor::getBitmap(const EpdFontData* fontData, const Ep
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Check if hot group already has this group decompressed — if not, decompress it
|
// Check if hot group already has this group decompressed — if not, decompress it
|
||||||
if (!(!hotGroup.empty() && hotGroupFont == fontData && hotGroupIndex == groupIndex)) {
|
if (!(hotGroup != nullptr && hotGroupFont == fontData && hotGroupIndex == groupIndex)) {
|
||||||
stats.cacheMisses++;
|
stats.cacheMisses++;
|
||||||
const EpdFontGroup& group = fontData->groups[groupIndex];
|
const EpdFontGroup& group = fontData->groups[groupIndex];
|
||||||
|
|
||||||
hotGroup.resize(group.uncompressedSize);
|
// ensureCapacity may free the buffer, so the cached-group identity dies with it either way.
|
||||||
if (hotGroup.empty()) {
|
hotGroupFont = nullptr;
|
||||||
|
hotGroupIndex = UINT16_MAX;
|
||||||
|
if (!ensureCapacity(hotGroup, hotGroupCapacity, group.uncompressedSize)) {
|
||||||
LOG_ERR("FDC", "Failed to allocate %u bytes for hot group %u", group.uncompressedSize, groupIndex);
|
LOG_ERR("FDC", "Failed to allocate %u bytes for hot group %u", group.uncompressedSize, groupIndex);
|
||||||
hotGroupFont = nullptr;
|
|
||||||
hotGroupIndex = UINT16_MAX;
|
|
||||||
stats.getBitmapTimeUs += micros() - tStart;
|
stats.getBitmapTimeUs += micros() - tStart;
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!decompressGroup(fontData, groupIndex, hotGroup.data(), group.uncompressedSize)) {
|
if (!decompressGroup(fontData, groupIndex, hotGroup, group.uncompressedSize)) {
|
||||||
hotGroup.clear();
|
|
||||||
hotGroup.shrink_to_fit();
|
|
||||||
hotGroupFont = nullptr;
|
|
||||||
hotGroupIndex = UINT16_MAX;
|
|
||||||
stats.getBitmapTimeUs += micros() - tStart;
|
stats.getBitmapTimeUs += micros() - tStart;
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
@@ -200,18 +208,16 @@ const uint8_t* FontDecompressor::getBitmap(const EpdFontData* fontData, const Ep
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Compact just the requested glyph from byte-aligned data into scratch buffer
|
// Compact just the requested glyph from byte-aligned data into scratch buffer
|
||||||
if (glyph->dataLength > hotGlyphBuf.size()) {
|
if (!ensureCapacity(hotGlyphBuf, hotGlyphBufCapacity, glyph->dataLength)) {
|
||||||
hotGlyphBuf.resize(glyph->dataLength);
|
LOG_ERR("FDC", "Failed to allocate %u bytes for glyph scratch", (unsigned)glyph->dataLength);
|
||||||
}
|
|
||||||
if (hotGlyphBuf.empty()) {
|
|
||||||
stats.getBitmapTimeUs += micros() - tStart;
|
stats.getBitmapTimeUs += micros() - tStart;
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
uint32_t alignedOff = getAlignedOffset(fontData, groupIndex, glyphIndex);
|
uint32_t alignedOff = getAlignedOffset(fontData, groupIndex, glyphIndex);
|
||||||
compactSingleGlyph(&hotGroup[alignedOff], hotGlyphBuf.data(), glyph->width, glyph->height);
|
compactSingleGlyph(&hotGroup[alignedOff], hotGlyphBuf, glyph->width, glyph->height);
|
||||||
stats.getBitmapTimeUs += micros() - tStart;
|
stats.getBitmapTimeUs += micros() - tStart;
|
||||||
return hotGlyphBuf.data();
|
return hotGlyphBuf;
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Prewarm: pre-decompress glyph bitmaps for a page of text ---
|
// --- Prewarm: pre-decompress glyph bitmaps for a page of text ---
|
||||||
|
|||||||
@@ -2,8 +2,6 @@
|
|||||||
|
|
||||||
#include <InflateReader.h>
|
#include <InflateReader.h>
|
||||||
|
|
||||||
#include <vector>
|
|
||||||
|
|
||||||
#include "EpdFontData.h"
|
#include "EpdFontData.h"
|
||||||
|
|
||||||
class FontDecompressor {
|
class FontDecompressor {
|
||||||
@@ -67,13 +65,22 @@ class FontDecompressor {
|
|||||||
|
|
||||||
// Hot group: last decompressed group (byte-aligned) for non-prewarmed fallback path.
|
// 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.
|
// Kept in byte-aligned format; individual glyphs are compacted on demand into hotGlyphBuf.
|
||||||
|
// Nothrow high-water malloc buffers, NOT std::vector: getBitmap() runs on the render path,
|
||||||
|
// and under -fno-exceptions a vector resize that hits OOM abort()s the firmware instead of
|
||||||
|
// failing (field crash: hotGroup.resize() -> std::bad_alloc -> abort with ~11 KB free).
|
||||||
|
// ensureCapacity() returns false on OOM so the caller can skip the glyph gracefully.
|
||||||
const EpdFontData* hotGroupFont = nullptr;
|
const EpdFontData* hotGroupFont = nullptr;
|
||||||
uint16_t hotGroupIndex = UINT16_MAX;
|
uint16_t hotGroupIndex = UINT16_MAX;
|
||||||
std::vector<uint8_t> hotGroup;
|
uint8_t* hotGroup = nullptr; // owned; freed in freeHotGroup()/dtor
|
||||||
|
uint32_t hotGroupCapacity = 0;
|
||||||
|
|
||||||
// Scratch buffer for compacting a single glyph from the hot group.
|
// Scratch buffer for compacting a single glyph from the hot group.
|
||||||
// Valid until the next getBitmap() call.
|
// Valid until the next getBitmap() call. Same ownership/OOM contract as hotGroup.
|
||||||
std::vector<uint8_t> hotGlyphBuf;
|
uint8_t* hotGlyphBuf = nullptr;
|
||||||
|
uint32_t hotGlyphBufCapacity = 0;
|
||||||
|
|
||||||
|
// Grow (never shrink) an owned buffer to at least `needed` bytes; false on OOM, buffer freed.
|
||||||
|
static bool ensureCapacity(uint8_t*& buf, uint32_t& capacity, uint32_t needed);
|
||||||
|
|
||||||
void freePageBuffer();
|
void freePageBuffer();
|
||||||
void freeHotGroup();
|
void freeHotGroup();
|
||||||
|
|||||||
@@ -1195,7 +1195,8 @@ int SdCardFont::fetchAdvancesForCodepoints(uint32_t* codepoints, uint32_t cpCoun
|
|||||||
}
|
}
|
||||||
|
|
||||||
template <typename Iter>
|
template <typename Iter>
|
||||||
int SdCardFont::buildAdvanceTableRange(Iter begin, Iter end, bool includeSpace, bool includeHyphen, uint8_t styleMask) {
|
int SdCardFont::buildAdvanceTableRange(Iter begin, Iter end, bool includeSpace, bool includeHyphen, uint8_t styleMask,
|
||||||
|
const char* extraText) {
|
||||||
if (!loaded_) return -1;
|
if (!loaded_) return -1;
|
||||||
styleMask = resolveStyleMask(styleMask);
|
styleMask = resolveStyleMask(styleMask);
|
||||||
if (styleMask == 0) return 0;
|
if (styleMask == 0) return 0;
|
||||||
@@ -1215,6 +1216,9 @@ int SdCardFont::buildAdvanceTableRange(Iter begin, Iter end, bool includeSpace,
|
|||||||
for (auto it = begin; it != end && !hitCap; ++it) {
|
for (auto it = begin; it != end && !hitCap; ++it) {
|
||||||
hitCap = collectUniqueCodepoints(asCStr(*it), codepoints, cpCount, MAX_UNIQUE_CODEPOINTS);
|
hitCap = collectUniqueCodepoints(asCStr(*it), codepoints, cpCount, MAX_UNIQUE_CODEPOINTS);
|
||||||
}
|
}
|
||||||
|
if (extraText && !hitCap) {
|
||||||
|
hitCap = collectUniqueCodepoints(extraText, codepoints, cpCount, MAX_UNIQUE_CODEPOINTS);
|
||||||
|
}
|
||||||
|
|
||||||
if (includeSpace && std::none_of(codepoints, codepoints + cpCount, [](uint32_t c) { return c == ' '; }))
|
if (includeSpace && std::none_of(codepoints, codepoints + cpCount, [](uint32_t c) { return c == ' '; }))
|
||||||
codepoints[cpCount++] = ' ';
|
codepoints[cpCount++] = ' ';
|
||||||
@@ -1232,12 +1236,13 @@ int SdCardFont::buildAdvanceTableRange(Iter begin, Iter end, bool includeSpace,
|
|||||||
return totalMissed;
|
return totalMissed;
|
||||||
}
|
}
|
||||||
|
|
||||||
int SdCardFont::buildAdvanceTable(const char* utf8Text, uint8_t styleMask) {
|
int SdCardFont::buildAdvanceTable(const char* utf8Text, uint8_t styleMask, const char* extraText) {
|
||||||
return buildAdvanceTableRange(&utf8Text, &utf8Text + 1, false, false, styleMask);
|
return buildAdvanceTableRange(&utf8Text, &utf8Text + 1, false, false, styleMask, extraText);
|
||||||
}
|
}
|
||||||
|
|
||||||
int SdCardFont::buildAdvanceTable(const std::vector<std::string>& words, bool includeHyphen, uint8_t styleMask) {
|
int SdCardFont::buildAdvanceTable(const std::vector<std::string>& words, bool includeHyphen, uint8_t styleMask,
|
||||||
return buildAdvanceTableRange(words.begin(), words.end(), words.size() > 1, includeHyphen, styleMask);
|
const char* extraText) {
|
||||||
|
return buildAdvanceTableRange(words.begin(), words.end(), words.size() > 1, includeHyphen, styleMask, extraText);
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Stats ---
|
// --- Stats ---
|
||||||
|
|||||||
@@ -47,9 +47,12 @@ class SdCardFont {
|
|||||||
// Build a compact advance-only table for layout measurement.
|
// Build a compact advance-only table for layout measurement.
|
||||||
// Extracts ALL unique codepoints from words (no MAX_PAGE_GLYPHS cap),
|
// Extracts ALL unique codepoints from words (no MAX_PAGE_GLYPHS cap),
|
||||||
// batch-reads advanceX from SD, stores in a sorted per-style table.
|
// batch-reads advanceX from SD, stores in a sorted per-style table.
|
||||||
|
// extraText: optional additional codepoints to warm in the same SD pass
|
||||||
|
// (e.g. shaped Arabic presentation forms the measurement path will look up).
|
||||||
// Returns number of codepoints not found in font coverage.
|
// Returns number of codepoints not found in font coverage.
|
||||||
int buildAdvanceTable(const char* utf8Text, uint8_t styleMask = 0x0F);
|
int buildAdvanceTable(const char* utf8Text, uint8_t styleMask = 0x0F, const char* extraText = nullptr);
|
||||||
int buildAdvanceTable(const std::vector<std::string>& words, bool includeHyphen, uint8_t styleMask = 0x0F);
|
int buildAdvanceTable(const std::vector<std::string>& words, bool includeHyphen, uint8_t styleMask = 0x0F,
|
||||||
|
const char* extraText = nullptr);
|
||||||
|
|
||||||
// Look up advanceX for a codepoint from the advance table.
|
// Look up advanceX for a codepoint from the advance table.
|
||||||
// Returns the 12.4 fixed-point advance, or 0 if not found.
|
// Returns the 12.4 fixed-point advance, or 0 if not found.
|
||||||
@@ -140,11 +143,13 @@ class SdCardFont {
|
|||||||
|
|
||||||
// Full intervals loaded from file (kept in RAM for codepoint lookup)
|
// Full intervals loaded from file (kept in RAM for codepoint lookup)
|
||||||
EpdUnicodeInterval* fullIntervals = nullptr;
|
EpdUnicodeInterval* fullIntervals = nullptr;
|
||||||
|
EPD_PACKED_BEGIN
|
||||||
struct BmpInterval16 {
|
struct BmpInterval16 {
|
||||||
uint16_t first;
|
uint16_t first;
|
||||||
uint16_t last;
|
uint16_t last;
|
||||||
uint16_t offset;
|
uint16_t offset;
|
||||||
} __attribute__((packed));
|
} EPD_PACKED_ATTR;
|
||||||
|
EPD_PACKED_END
|
||||||
static_assert(sizeof(BmpInterval16) == 6, "BmpInterval16 must remain compact");
|
static_assert(sizeof(BmpInterval16) == 6, "BmpInterval16 must remain compact");
|
||||||
BmpInterval16* bmpIntervals = nullptr;
|
BmpInterval16* bmpIntervals = nullptr;
|
||||||
bool intervalsAreBmp16 = false;
|
bool intervalsAreBmp16 = false;
|
||||||
@@ -249,7 +254,8 @@ class SdCardFont {
|
|||||||
int32_t findGlobalGlyphIndex(const PerStyle& s, uint32_t codepoint) const;
|
int32_t findGlobalGlyphIndex(const PerStyle& s, uint32_t codepoint) const;
|
||||||
int fetchAdvancesForCodepoints(uint32_t* codepoints, uint32_t cpCount, uint8_t styleMask);
|
int fetchAdvancesForCodepoints(uint32_t* codepoints, uint32_t cpCount, uint8_t styleMask);
|
||||||
template <typename Iter>
|
template <typename Iter>
|
||||||
int buildAdvanceTableRange(Iter begin, Iter end, bool includeSpace, bool includeHyphen, uint8_t styleMask);
|
int buildAdvanceTableRange(Iter begin, Iter end, bool includeSpace, bool includeHyphen, uint8_t styleMask,
|
||||||
|
const char* extraText = nullptr);
|
||||||
int prewarmStyle(uint8_t styleIdx, const uint32_t* codepoints, uint32_t cpCount, bool metadataOnly);
|
int prewarmStyle(uint8_t styleIdx, const uint32_t* codepoints, uint32_t cpCount, bool metadataOnly);
|
||||||
|
|
||||||
// Global helpers
|
// Global helpers
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -6,6 +6,8 @@
|
|||||||
!NotoSerif/**
|
!NotoSerif/**
|
||||||
!NotoSans/
|
!NotoSans/
|
||||||
!NotoSans/**
|
!NotoSans/**
|
||||||
|
!NotoSansArabic/
|
||||||
|
!NotoSansArabic/**
|
||||||
!NotoSansHebrew/
|
!NotoSansHebrew/
|
||||||
!NotoSansHebrew/**
|
!NotoSansHebrew/**
|
||||||
!OpenDyslexic/
|
!OpenDyslexic/
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,93 @@
|
|||||||
|
Copyright 2022 The Noto Project Authors (https://github.com/notofonts/arabic)
|
||||||
|
|
||||||
|
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||||
|
This license is copied below, and is also available with a FAQ at:
|
||||||
|
https://openfontlicense.org
|
||||||
|
|
||||||
|
|
||||||
|
-----------------------------------------------------------
|
||||||
|
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||||
|
-----------------------------------------------------------
|
||||||
|
|
||||||
|
PREAMBLE
|
||||||
|
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||||
|
development of collaborative font projects, to support the font creation
|
||||||
|
efforts of academic and linguistic communities, and to provide a free and
|
||||||
|
open framework in which fonts may be shared and improved in partnership
|
||||||
|
with others.
|
||||||
|
|
||||||
|
The OFL allows the licensed fonts to be used, studied, modified and
|
||||||
|
redistributed freely as long as they are not sold by themselves. The
|
||||||
|
fonts, including any derivative works, can be bundled, embedded,
|
||||||
|
redistributed and/or sold with any software provided that any reserved
|
||||||
|
names are not used by derivative works. The fonts and derivatives,
|
||||||
|
however, cannot be released under any other type of license. The
|
||||||
|
requirement for fonts to remain under this license does not apply
|
||||||
|
to any document created using the fonts or their derivatives.
|
||||||
|
|
||||||
|
DEFINITIONS
|
||||||
|
"Font Software" refers to the set of files released by the Copyright
|
||||||
|
Holder(s) under this license and clearly marked as such. This may
|
||||||
|
include source files, build scripts and documentation.
|
||||||
|
|
||||||
|
"Reserved Font Name" refers to any names specified as such after the
|
||||||
|
copyright statement(s).
|
||||||
|
|
||||||
|
"Original Version" refers to the collection of Font Software components as
|
||||||
|
distributed by the Copyright Holder(s).
|
||||||
|
|
||||||
|
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||||
|
or substituting -- in part or in whole -- any of the components of the
|
||||||
|
Original Version, by changing formats or by porting the Font Software to a
|
||||||
|
new environment.
|
||||||
|
|
||||||
|
"Author" refers to any designer, engineer, programmer, technical
|
||||||
|
writer or other person who contributed to the Font Software.
|
||||||
|
|
||||||
|
PERMISSION & CONDITIONS
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining
|
||||||
|
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||||
|
redistribute, and sell modified and unmodified copies of the Font
|
||||||
|
Software, subject to the following conditions:
|
||||||
|
|
||||||
|
1) Neither the Font Software nor any of its individual components,
|
||||||
|
in Original or Modified Versions, may be sold by itself.
|
||||||
|
|
||||||
|
2) Original or Modified Versions of the Font Software may be bundled,
|
||||||
|
redistributed and/or sold with any software, provided that each copy
|
||||||
|
contains the above copyright notice and this license. These can be
|
||||||
|
included either as stand-alone text files, human-readable headers or
|
||||||
|
in the appropriate machine-readable metadata fields within text or
|
||||||
|
binary files as long as those fields can be easily viewed by the user.
|
||||||
|
|
||||||
|
3) No Modified Version of the Font Software may use the Reserved Font
|
||||||
|
Name(s) unless explicit written permission is granted by the corresponding
|
||||||
|
Copyright Holder. This restriction only applies to the primary font name as
|
||||||
|
presented to the users.
|
||||||
|
|
||||||
|
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||||
|
Software shall not be used to promote, endorse or advertise any
|
||||||
|
Modified Version, except to acknowledge the contribution(s) of the
|
||||||
|
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||||
|
permission.
|
||||||
|
|
||||||
|
5) The Font Software, modified or unmodified, in part or in whole,
|
||||||
|
must be distributed entirely under this license, and must not be
|
||||||
|
distributed under any other license. The requirement for fonts to
|
||||||
|
remain under this license does not apply to any document created
|
||||||
|
using the Font Software.
|
||||||
|
|
||||||
|
TERMINATION
|
||||||
|
This license becomes null and void if any of the above conditions are
|
||||||
|
not met.
|
||||||
|
|
||||||
|
DISCLAIMER
|
||||||
|
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||||
|
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||||
|
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||||
|
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||||
|
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||||
|
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||||
|
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||||
|
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||||
+1872
-1256
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+2244
-1483
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -33,6 +33,7 @@ import sys
|
|||||||
import tempfile
|
import tempfile
|
||||||
import threading
|
import threading
|
||||||
import time
|
import time
|
||||||
|
import socket
|
||||||
import urllib.request
|
import urllib.request
|
||||||
from concurrent.futures import ProcessPoolExecutor, as_completed
|
from concurrent.futures import ProcessPoolExecutor, as_completed
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
@@ -49,17 +50,44 @@ INSTANCE_DIR = SCRIPT_DIR / "instanced_fonts"
|
|||||||
DEFAULT_FALLBACK_FONT = EPDFONTS_DIR / "builtinFonts/source/NotoSans/NotoSans-Regular.ttf"
|
DEFAULT_FALLBACK_FONT = EPDFONTS_DIR / "builtinFonts/source/NotoSans/NotoSans-Regular.ttf"
|
||||||
|
|
||||||
|
|
||||||
def download_font(url: str, dest: Path) -> Path:
|
_orig_getaddrinfo = socket.getaddrinfo
|
||||||
"""Download a font file if not already cached. Returns the local path."""
|
|
||||||
|
|
||||||
|
def _ipv4_only_getaddrinfo(*args, **kwargs):
|
||||||
|
"""getaddrinfo variant that drops AAAA records (IPv4 only)."""
|
||||||
|
return [ai for ai in _orig_getaddrinfo(*args, **kwargs) if ai[0] == socket.AF_INET]
|
||||||
|
|
||||||
|
|
||||||
|
def download_font(url: str, dest: Path, retries: int = 3) -> Path:
|
||||||
|
"""Download a font file if not already cached. Returns the local path.
|
||||||
|
|
||||||
|
Some sources (e.g. mirrors.ctan.org) are round-robin redirectors that land
|
||||||
|
on a different mirror each request; a mirror may advertise an IPv6 address a
|
||||||
|
host without an IPv6 route cannot reach ([Errno 101] Network is unreachable).
|
||||||
|
Retry on failure, forcing IPv4 resolution after the first attempt.
|
||||||
|
"""
|
||||||
if dest.exists():
|
if dest.exists():
|
||||||
return dest
|
return dest
|
||||||
dest.parent.mkdir(parents=True, exist_ok=True)
|
dest.parent.mkdir(parents=True, exist_ok=True)
|
||||||
print(f" Downloading {dest.name}...")
|
print(f" Downloading {dest.name}...")
|
||||||
try:
|
last_err = None
|
||||||
urllib.request.urlretrieve(url, dest)
|
for attempt in range(1, retries + 1):
|
||||||
except Exception as e:
|
force_ipv4 = attempt > 1
|
||||||
dest.unlink(missing_ok=True)
|
if force_ipv4:
|
||||||
raise RuntimeError(f"Failed to download {url}: {e}") from e
|
socket.getaddrinfo = _ipv4_only_getaddrinfo
|
||||||
|
try:
|
||||||
|
urllib.request.urlretrieve(url, dest)
|
||||||
|
break
|
||||||
|
except Exception as e: # noqa: BLE001 - reported via RuntimeError below
|
||||||
|
last_err = e
|
||||||
|
dest.unlink(missing_ok=True)
|
||||||
|
if attempt < retries:
|
||||||
|
print(f" Attempt {attempt} failed ({e}); retrying (IPv4-only)...")
|
||||||
|
finally:
|
||||||
|
if force_ipv4:
|
||||||
|
socket.getaddrinfo = _orig_getaddrinfo
|
||||||
|
else:
|
||||||
|
raise RuntimeError(f"Failed to download {url}: {last_err}") from last_err
|
||||||
size_kb = dest.stat().st_size / 1024
|
size_kb = dest.stat().st_size / 1024
|
||||||
print(f" Downloaded {dest.name} ({size_kb:.0f} KB)")
|
print(f" Downloaded {dest.name} ({size_kb:.0f} KB)")
|
||||||
return dest
|
return dest
|
||||||
|
|||||||
@@ -31,19 +31,48 @@ done
|
|||||||
UI_FONT_SIZES=(10 12)
|
UI_FONT_SIZES=(10 12)
|
||||||
UI_FONT_STYLES=("Regular" "Bold")
|
UI_FONT_STYLES=("Regular" "Bold")
|
||||||
|
|
||||||
|
# Arabic glyphs for UI text (menus, file browser titles). The built-in fonts
|
||||||
|
# must cover the *output* of MiniBidi's do_shape() — contextual presentation
|
||||||
|
# forms — not base letters, or shaped UI text silently drops glyphs.
|
||||||
|
# Curated for firmware-size budget: core Arabic (Presentation Forms-B,
|
||||||
|
# incl. the Lam-Alef ligature forms) plus the Farsi/Urdu extra letters'
|
||||||
|
# Presentation Forms-A blocks, the few characters shaping leaves at their
|
||||||
|
# base codepoint, Arabic punctuation, and both digit sets. No harakat and
|
||||||
|
# no Sindhi/Pashto/Kurdish forms — book text gets those from SD-card fonts.
|
||||||
|
ARABIC_INTERVALS=(
|
||||||
|
--additional-intervals 0x060C,0x060C # Arabic comma
|
||||||
|
--additional-intervals 0x061B,0x061B # Arabic semicolon
|
||||||
|
--additional-intervals 0x061F,0x061F # Arabic question mark
|
||||||
|
--additional-intervals 0x0621,0x0621 # hamza (non-joining, never shaped)
|
||||||
|
--additional-intervals 0x0640,0x0640 # tatweel
|
||||||
|
--additional-intervals 0x0660,0x0669 # Arabic-Indic digits
|
||||||
|
--additional-intervals 0x06BA,0x06BA # noon ghunna base (initial/medial keep base cp)
|
||||||
|
--additional-intervals 0x06D4,0x06D4 # Urdu full stop
|
||||||
|
--additional-intervals 0x06F0,0x06F9 # extended Arabic-Indic digits (Farsi/Urdu)
|
||||||
|
--additional-intervals 0xFB56,0xFB59 # peh (Farsi)
|
||||||
|
--additional-intervals 0xFB66,0xFB69 # tteh (Urdu)
|
||||||
|
--additional-intervals 0xFB7A,0xFB7D # tcheh (Farsi)
|
||||||
|
--additional-intervals 0xFB88,0xFB95 # ddal, jeh, rreh (Urdu), keheh, gaf (Farsi/Urdu)
|
||||||
|
--additional-intervals 0xFB9E,0xFB9F # noon ghunna isolated/final (Urdu)
|
||||||
|
--additional-intervals 0xFBA6,0xFBB1 # heh goal, heh doachashmee, yeh barree(+hamza) (Urdu)
|
||||||
|
--additional-intervals 0xFBFC,0xFBFF # farsi yeh (Farsi/Urdu)
|
||||||
|
--additional-intervals 0xFE80,0xFEFC # Presentation Forms-B: core Arabic + Lam-Alef
|
||||||
|
)
|
||||||
|
|
||||||
for size in ${UI_FONT_SIZES[@]}; do
|
for size in ${UI_FONT_SIZES[@]}; do
|
||||||
for style in ${UI_FONT_STYLES[@]}; do
|
for style in ${UI_FONT_STYLES[@]}; do
|
||||||
font_name="ubuntu_${size}_$(echo $style | tr '[:upper:]' '[:lower:]')"
|
font_name="ubuntu_${size}_$(echo $style | tr '[:upper:]' '[:lower:]')"
|
||||||
font_path="../builtinFonts/source/Ubuntu/Ubuntu-${style}.ttf"
|
font_path="../builtinFonts/source/Ubuntu/Ubuntu-${style}.ttf"
|
||||||
hebrew_path="../builtinFonts/source/NotoSansHebrew/NotoSansHebrew-${style}.ttf"
|
hebrew_path="../builtinFonts/source/NotoSansHebrew/NotoSansHebrew-${style}.ttf"
|
||||||
|
arabic_path="../builtinFonts/source/NotoSansArabic/NotoSansArabic-${style}.ttf"
|
||||||
# Ubuntu lacks the Latin Extended Additional block (U+1EA0-U+1EF9) used for
|
# Ubuntu lacks the Latin Extended Additional block (U+1EA0-U+1EF9) used for
|
||||||
# Vietnamese tone marks. Append a Vietnamese-only Ubuntu cut so those glyphs
|
# Vietnamese tone marks. Append a Vietnamese-only Ubuntu cut so those glyphs
|
||||||
# are filled from it while every glyph Ubuntu already has stays unchanged
|
# are filled from it while every glyph Ubuntu already has stays unchanged
|
||||||
# (fontstack is ordered by descending priority).
|
# (fontstack is ordered by descending priority).
|
||||||
viet_path="../builtinFonts/source/Ubuntu/Ubuntu-Vietnamese-${style}.ttf"
|
viet_path="../builtinFonts/source/Ubuntu/Ubuntu-Vietnamese-${style}.ttf"
|
||||||
output_path="../builtinFonts/${font_name}.h"
|
output_path="../builtinFonts/${font_name}.h"
|
||||||
python fontconvert.py $font_name $size $font_path $hebrew_path $viet_path \
|
python fontconvert.py $font_name $size $font_path $hebrew_path $arabic_path $viet_path \
|
||||||
--additional-intervals 0x05D0,0x05EA > $output_path
|
--additional-intervals 0x05D0,0x05EA "${ARABIC_INTERVALS[@]}" > $output_path
|
||||||
echo "Generated $output_path"
|
echo "Generated $output_path"
|
||||||
done
|
done
|
||||||
done
|
done
|
||||||
@@ -51,7 +80,8 @@ done
|
|||||||
python fontconvert.py notosans_8_regular 8 \
|
python fontconvert.py notosans_8_regular 8 \
|
||||||
../builtinFonts/source/NotoSans/NotoSans-Regular.ttf \
|
../builtinFonts/source/NotoSans/NotoSans-Regular.ttf \
|
||||||
../builtinFonts/source/NotoSansHebrew/NotoSansHebrew-Regular.ttf \
|
../builtinFonts/source/NotoSansHebrew/NotoSansHebrew-Regular.ttf \
|
||||||
--additional-intervals 0x05D0,0x05EA > ../builtinFonts/notosans_8_regular.h
|
../builtinFonts/source/NotoSansArabic/NotoSansArabic-Regular.ttf \
|
||||||
|
--additional-intervals 0x05D0,0x05EA "${ARABIC_INTERVALS[@]}" > ../builtinFonts/notosans_8_regular.h
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
echo "Running compression verification..."
|
echo "Running compression verification..."
|
||||||
|
|||||||
@@ -132,7 +132,16 @@ families:
|
|||||||
bold: {url: "https://raw.githubusercontent.com/impallari/Libre-Baskerville/master/fonts/ttf/LibreBaskerville-Bold.ttf"}
|
bold: {url: "https://raw.githubusercontent.com/impallari/Libre-Baskerville/master/fonts/ttf/LibreBaskerville-Bold.ttf"}
|
||||||
italic: {url: "https://raw.githubusercontent.com/impallari/Libre-Baskerville/master/fonts/ttf/LibreBaskerville-Italic.ttf"}
|
italic: {url: "https://raw.githubusercontent.com/impallari/Libre-Baskerville/master/fonts/ttf/LibreBaskerville-Italic.ttf"}
|
||||||
bolditalic: {url: "https://raw.githubusercontent.com/impallari/Libre-Baskerville/master/fonts/ttf/LibreBaskerville-BoldItalic.ttf"}
|
bolditalic: {url: "https://raw.githubusercontent.com/impallari/Libre-Baskerville/master/fonts/ttf/LibreBaskerville-BoldItalic.ttf"}
|
||||||
|
|
||||||
|
- name: Vollkorn
|
||||||
|
description: "A serif for bread and butter use by Friedrich Althausen (Latin, Greek, Cyrillic)"
|
||||||
|
intervals: latin-ext,greek,cyrillic
|
||||||
|
sizes: [12, 14, 16, 18]
|
||||||
|
styles:
|
||||||
|
regular: {url: "https://raw.githubusercontent.com/google/fonts/main/ofl/vollkorn/Vollkorn%5Bwght%5D.ttf", variable: {wght: 400}}
|
||||||
|
bold: {url: "https://raw.githubusercontent.com/google/fonts/main/ofl/vollkorn/Vollkorn%5Bwght%5D.ttf", variable: {wght: 700}}
|
||||||
|
italic: {url: "https://raw.githubusercontent.com/google/fonts/main/ofl/vollkorn/Vollkorn-Italic%5Bwght%5D.ttf", variable: {wght: 400}}
|
||||||
|
bolditalic: {url: "https://raw.githubusercontent.com/google/fonts/main/ofl/vollkorn/Vollkorn-Italic%5Bwght%5D.ttf", variable: {wght: 700}}
|
||||||
|
|
||||||
# ── Sans-serif ─────────────────────────────────────────────────────────
|
# ── Sans-serif ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
|||||||
+19
-59
@@ -153,18 +153,11 @@ bool Epub::parseTocNcxFile() const {
|
|||||||
|
|
||||||
LOG_DBG("EBP", "Parsing toc ncx file: %s", tocNcxItem.c_str());
|
LOG_DBG("EBP", "Parsing toc ncx file: %s", tocNcxItem.c_str());
|
||||||
|
|
||||||
const auto tmpNcxPath = getCachePath() + "/toc.ncx";
|
size_t ncxSize;
|
||||||
HalFile tempNcxFile;
|
if (!getItemSize(tocNcxItem, &ncxSize)) {
|
||||||
if (!Storage.openFileForWrite("EBP", tmpNcxPath, tempNcxFile)) {
|
LOG_ERR("EBP", "Could not get size of toc ncx file");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
readItemContentsToStream(tocNcxItem, tempNcxFile, 1024);
|
|
||||||
// Explicitly close() file before reopening for reading
|
|
||||||
tempNcxFile.close();
|
|
||||||
if (!Storage.openFileForRead("EBP", tmpNcxPath, tempNcxFile)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
const auto ncxSize = tempNcxFile.size();
|
|
||||||
|
|
||||||
TocNcxParser ncxParser(contentBasePath, ncxSize, bookMetadataCache.get());
|
TocNcxParser ncxParser(contentBasePath, ncxSize, bookMetadataCache.get());
|
||||||
|
|
||||||
@@ -173,29 +166,13 @@ bool Epub::parseTocNcxFile() const {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const auto ncxBuffer = static_cast<uint8_t*>(malloc(1024));
|
// Stream the decompressed NCX straight into the parser instead of round-tripping
|
||||||
if (!ncxBuffer) {
|
// through a temp file on the SD card (decompress -> write -> reopen -> reread -> delete).
|
||||||
LOG_ERR("EBP", "Could not allocate memory for toc ncx parser");
|
if (!readItemContentsToStream(tocNcxItem, ncxParser, 1024)) {
|
||||||
|
LOG_ERR("EBP", "Could not read toc ncx file");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
while (tempNcxFile.available()) {
|
|
||||||
const auto readSize = tempNcxFile.read(ncxBuffer, 1024);
|
|
||||||
if (readSize == 0) break;
|
|
||||||
const auto processedSize = ncxParser.write(ncxBuffer, readSize);
|
|
||||||
|
|
||||||
if (processedSize != readSize) {
|
|
||||||
LOG_ERR("EBP", "Could not process all toc ncx data");
|
|
||||||
free(ncxBuffer);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
free(ncxBuffer);
|
|
||||||
// Explicitly close() file before calling Storage.remove()
|
|
||||||
tempNcxFile.close();
|
|
||||||
Storage.remove(tmpNcxPath.c_str());
|
|
||||||
|
|
||||||
LOG_DBG("EBP", "Parsed TOC items");
|
LOG_DBG("EBP", "Parsed TOC items");
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -209,18 +186,11 @@ bool Epub::parseTocNavFile() const {
|
|||||||
|
|
||||||
LOG_DBG("EBP", "Parsing toc nav file: %s", tocNavItem.c_str());
|
LOG_DBG("EBP", "Parsing toc nav file: %s", tocNavItem.c_str());
|
||||||
|
|
||||||
const auto tmpNavPath = getCachePath() + "/toc.nav";
|
size_t navSize;
|
||||||
HalFile tempNavFile;
|
if (!getItemSize(tocNavItem, &navSize)) {
|
||||||
if (!Storage.openFileForWrite("EBP", tmpNavPath, tempNavFile)) {
|
LOG_ERR("EBP", "Could not get size of toc nav file");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
readItemContentsToStream(tocNavItem, tempNavFile, 1024);
|
|
||||||
// Explicitly close() file before reopening for reading
|
|
||||||
tempNavFile.close();
|
|
||||||
if (!Storage.openFileForRead("EBP", tmpNavPath, tempNavFile)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
const auto navSize = tempNavFile.size();
|
|
||||||
|
|
||||||
// Note: We can't use `contentBasePath` here as the nav file may be in a different folder to the content.opf
|
// Note: We can't use `contentBasePath` here as the nav file may be in a different folder to the content.opf
|
||||||
// and the HTMLX nav file will have hrefs relative to itself
|
// and the HTMLX nav file will have hrefs relative to itself
|
||||||
@@ -232,28 +202,13 @@ bool Epub::parseTocNavFile() const {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const auto navBuffer = static_cast<uint8_t*>(malloc(1024));
|
// Stream the decompressed nav document straight into the parser instead of round-tripping
|
||||||
if (!navBuffer) {
|
// through a temp file on the SD card (decompress -> write -> reopen -> reread -> delete).
|
||||||
LOG_ERR("EBP", "Could not allocate memory for toc nav parser");
|
if (!readItemContentsToStream(tocNavItem, navParser, 1024)) {
|
||||||
|
LOG_ERR("EBP", "Could not read toc nav file");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
while (tempNavFile.available()) {
|
|
||||||
const auto readSize = tempNavFile.read(navBuffer, 1024);
|
|
||||||
const auto processedSize = navParser.write(navBuffer, readSize);
|
|
||||||
|
|
||||||
if (processedSize != readSize) {
|
|
||||||
LOG_ERR("EBP", "Could not process all toc nav data");
|
|
||||||
free(navBuffer);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
free(navBuffer);
|
|
||||||
// Explicitly close() file before calling Storage.remove()
|
|
||||||
tempNavFile.close();
|
|
||||||
Storage.remove(tmpNavPath.c_str());
|
|
||||||
|
|
||||||
LOG_DBG("EBP", "Parsed TOC nav items");
|
LOG_DBG("EBP", "Parsed TOC nav items");
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -396,6 +351,11 @@ bool Epub::load(const bool buildIfMissing, const bool skipLoadingCss) {
|
|||||||
Storage.removeDir((cachePath + "/sections").c_str());
|
Storage.removeDir((cachePath + "/sections").c_str());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Release the resolved CSS rule map: it is only needed transiently while building
|
||||||
|
// section caches, and createSectionFile reloads it from cache on demand. Holding it
|
||||||
|
// resident pins tens of KB for the whole reading session (more on warm resume into
|
||||||
|
// an already-cached chapter, where createSectionFile never runs to clear it).
|
||||||
|
cssParser->clear();
|
||||||
LOG_DBG("EBP", "Loaded ePub: %s", filepath.c_str());
|
LOG_DBG("EBP", "Loaded ePub: %s", filepath.c_str());
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
#include "BookMetadataCache.h"
|
#include "BookMetadataCache.h"
|
||||||
|
|
||||||
|
#include <BufferedFile.h>
|
||||||
#include <Logging.h>
|
#include <Logging.h>
|
||||||
#include <Serialization.h>
|
#include <Serialization.h>
|
||||||
#include <Utf8.h>
|
#include <Utf8.h>
|
||||||
@@ -14,6 +15,52 @@ constexpr uint8_t BOOK_CACHE_VERSION = 8; // v8: TOC/book titles stored NFC-com
|
|||||||
constexpr char bookBinFile[] = "/book.bin";
|
constexpr char bookBinFile[] = "/book.bin";
|
||||||
constexpr char tmpSpineBinFile[] = "/spine.bin.tmp";
|
constexpr char tmpSpineBinFile[] = "/spine.bin.tmp";
|
||||||
constexpr char tmpTocBinFile[] = "/toc.bin.tmp";
|
constexpr char tmpTocBinFile[] = "/toc.bin.tmp";
|
||||||
|
// Buffer size for the buildBookBin streams. 3 buffers x 4KB, transient (freed on
|
||||||
|
// return); 4KB = 8 SD sectors per transfer, enough to stop the sector-cache thrash.
|
||||||
|
constexpr size_t BUILD_IO_BUFFER_SIZE = 4096;
|
||||||
|
|
||||||
|
// Entry (de)serializers, templated so they run over HalFile and the Buffered*
|
||||||
|
// wrappers alike (two instantiations each -- a few hundred bytes of flash, in
|
||||||
|
// exchange for the build path streaming at SD speed instead of per-pod).
|
||||||
|
template <typename F>
|
||||||
|
uint32_t writeSpineEntryTo(F& file, const BookMetadataCache::SpineEntry& entry) {
|
||||||
|
const uint32_t pos = file.position();
|
||||||
|
serialization::writeString(file, entry.href);
|
||||||
|
serialization::writePod(file, entry.cumulativeSize);
|
||||||
|
serialization::writePod(file, entry.tocIndex);
|
||||||
|
return pos;
|
||||||
|
}
|
||||||
|
|
||||||
|
template <typename F>
|
||||||
|
uint32_t writeTocEntryTo(F& file, const BookMetadataCache::TocEntry& entry) {
|
||||||
|
const uint32_t pos = file.position();
|
||||||
|
serialization::writeString(file, entry.title);
|
||||||
|
serialization::writeString(file, entry.href);
|
||||||
|
serialization::writeString(file, entry.anchor);
|
||||||
|
serialization::writePod(file, entry.level);
|
||||||
|
serialization::writePod(file, entry.spineIndex);
|
||||||
|
return pos;
|
||||||
|
}
|
||||||
|
|
||||||
|
template <typename F>
|
||||||
|
BookMetadataCache::SpineEntry readSpineEntryFrom(F& file) {
|
||||||
|
BookMetadataCache::SpineEntry entry;
|
||||||
|
serialization::readString(file, entry.href);
|
||||||
|
serialization::readPod(file, entry.cumulativeSize);
|
||||||
|
serialization::readPod(file, entry.tocIndex);
|
||||||
|
return entry;
|
||||||
|
}
|
||||||
|
|
||||||
|
template <typename F>
|
||||||
|
BookMetadataCache::TocEntry readTocEntryFrom(F& file) {
|
||||||
|
BookMetadataCache::TocEntry entry;
|
||||||
|
serialization::readString(file, entry.title);
|
||||||
|
serialization::readString(file, entry.href);
|
||||||
|
serialization::readString(file, entry.anchor);
|
||||||
|
serialization::readPod(file, entry.level);
|
||||||
|
serialization::readPod(file, entry.spineIndex);
|
||||||
|
return entry;
|
||||||
|
}
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
/* ============= WRITING / BUILDING FUNCTIONS ================ */
|
/* ============= WRITING / BUILDING FUNCTIONS ================ */
|
||||||
@@ -30,13 +77,23 @@ bool BookMetadataCache::beginContentOpfPass() {
|
|||||||
LOG_DBG("BMC", "Beginning content opf pass");
|
LOG_DBG("BMC", "Beginning content opf pass");
|
||||||
|
|
||||||
// Open spine file for writing
|
// Open spine file for writing
|
||||||
return Storage.openFileForWrite("BMC", cachePath + tmpSpineBinFile, spineFile);
|
if (!Storage.openFileForWrite("BMC", cachePath + tmpSpineBinFile, spineFile)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// Wrapper OOM is fine: createSpineEntry falls back to unbuffered writes.
|
||||||
|
passOut = makeUniqueNoThrow<serialization::BufferedFileWriter>(spineFile, BUILD_IO_BUFFER_SIZE);
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool BookMetadataCache::endContentOpfPass() {
|
bool BookMetadataCache::endContentOpfPass() {
|
||||||
|
const bool flushed = !passOut || passOut->flush();
|
||||||
|
passOut.reset();
|
||||||
// Explicit close() required: member variable persists beyond function scope
|
// Explicit close() required: member variable persists beyond function scope
|
||||||
spineFile.close();
|
spineFile.close();
|
||||||
return true;
|
if (!flushed) {
|
||||||
|
LOG_ERR("BMC", "Failed writing spine tmp file");
|
||||||
|
}
|
||||||
|
return flushed;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool BookMetadataCache::beginTocPass() {
|
bool BookMetadataCache::beginTocPass() {
|
||||||
@@ -74,10 +131,17 @@ bool BookMetadataCache::beginTocPass() {
|
|||||||
useSpineHrefIndex = false;
|
useSpineHrefIndex = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Wrapper OOM is fine: createTocEntry falls back to unbuffered writes.
|
||||||
|
passOut = makeUniqueNoThrow<serialization::BufferedFileWriter>(tocFile, BUILD_IO_BUFFER_SIZE);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool BookMetadataCache::endTocPass() {
|
bool BookMetadataCache::endTocPass() {
|
||||||
|
const bool flushed = !passOut || passOut->flush();
|
||||||
|
passOut.reset();
|
||||||
|
if (!flushed) {
|
||||||
|
LOG_ERR("BMC", "Failed writing toc tmp file");
|
||||||
|
}
|
||||||
// Explicit close() required: member variables persist beyond function scope
|
// Explicit close() required: member variables persist beyond function scope
|
||||||
tocFile.close();
|
tocFile.close();
|
||||||
spineFile.close();
|
spineFile.close();
|
||||||
@@ -86,7 +150,7 @@ bool BookMetadataCache::endTocPass() {
|
|||||||
spineHrefIndex.shrink_to_fit();
|
spineHrefIndex.shrink_to_fit();
|
||||||
useSpineHrefIndex = false;
|
useSpineHrefIndex = false;
|
||||||
|
|
||||||
return true;
|
return flushed;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool BookMetadataCache::endWrite() {
|
bool BookMetadataCache::endWrite() {
|
||||||
@@ -119,6 +183,14 @@ bool BookMetadataCache::buildBookBin(const std::string& epubPath, const BookMeta
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Buffered streams for the whole build: every access below is sequential per
|
||||||
|
// file, but interleaved ACROSS files, which thrashes SdFat's single shared
|
||||||
|
// sector cache when unbuffered (one 512B SD transaction per 4-byte pod --
|
||||||
|
// measured 31s for a 1,732-spine omnibus). Three 4KB buffers, freed on return.
|
||||||
|
serialization::BufferedFileWriter bookOut(bookFile, BUILD_IO_BUFFER_SIZE);
|
||||||
|
serialization::BufferedFileReader spineIn(spineFile, BUILD_IO_BUFFER_SIZE);
|
||||||
|
serialization::BufferedFileReader tocIn(tocFile, BUILD_IO_BUFFER_SIZE);
|
||||||
|
|
||||||
constexpr uint32_t headerASize =
|
constexpr uint32_t headerASize =
|
||||||
sizeof(BOOK_CACHE_VERSION) + /* LUT Offset */ sizeof(uint32_t) + sizeof(spineCount) + sizeof(tocCount);
|
sizeof(BOOK_CACHE_VERSION) + /* LUT Offset */ sizeof(uint32_t) + sizeof(spineCount) + sizeof(tocCount);
|
||||||
const uint32_t metadataSize = metadata.title.size() + metadata.author.size() + metadata.language.size() +
|
const uint32_t metadataSize = metadata.title.size() + metadata.author.size() + metadata.language.size() +
|
||||||
@@ -128,31 +200,34 @@ bool BookMetadataCache::buildBookBin(const std::string& epubPath, const BookMeta
|
|||||||
const uint32_t lutOffset = headerASize + metadataSize;
|
const uint32_t lutOffset = headerASize + metadataSize;
|
||||||
|
|
||||||
// Header A
|
// Header A
|
||||||
serialization::writePod(bookFile, BOOK_CACHE_VERSION);
|
serialization::writePod(bookOut, BOOK_CACHE_VERSION);
|
||||||
serialization::writePod(bookFile, lutOffset);
|
serialization::writePod(bookOut, lutOffset);
|
||||||
serialization::writePod(bookFile, spineCount);
|
serialization::writePod(bookOut, spineCount);
|
||||||
serialization::writePod(bookFile, tocCount);
|
serialization::writePod(bookOut, tocCount);
|
||||||
// Metadata
|
// Metadata
|
||||||
serialization::writeString(bookFile, metadata.title);
|
serialization::writeString(bookOut, metadata.title);
|
||||||
serialization::writeString(bookFile, metadata.author);
|
serialization::writeString(bookOut, metadata.author);
|
||||||
serialization::writeString(bookFile, metadata.language);
|
serialization::writeString(bookOut, metadata.language);
|
||||||
serialization::writeString(bookFile, metadata.coverItemHref);
|
serialization::writeString(bookOut, metadata.coverItemHref);
|
||||||
serialization::writeString(bookFile, metadata.textReferenceHref);
|
serialization::writeString(bookOut, metadata.textReferenceHref);
|
||||||
|
|
||||||
// Loop through spine entries, writing LUT positions
|
// Loop through spine entries, writing LUT positions
|
||||||
spineFile.seek(0);
|
spineIn.seek(0);
|
||||||
for (int i = 0; i < spineCount; i++) {
|
for (int i = 0; i < spineCount; i++) {
|
||||||
uint32_t pos = spineFile.position();
|
const uint32_t pos = spineIn.position();
|
||||||
auto spineEntry = readSpineEntry(spineFile);
|
readSpineEntryFrom(spineIn);
|
||||||
serialization::writePod(bookFile, pos + lutOffset + lutSize);
|
serialization::writePod(bookOut, pos + lutOffset + lutSize);
|
||||||
}
|
}
|
||||||
|
// Total size of the spine tmp file: entries land in book.bin after the toc LUT
|
||||||
|
// and the full spine block, so toc LUT positions are offset by it.
|
||||||
|
const auto spineBytes = static_cast<uint32_t>(spineIn.position());
|
||||||
|
|
||||||
// Loop through toc entries, writing LUT positions
|
// Loop through toc entries, writing LUT positions
|
||||||
tocFile.seek(0);
|
tocIn.seek(0);
|
||||||
for (int i = 0; i < tocCount; i++) {
|
for (int i = 0; i < tocCount; i++) {
|
||||||
uint32_t pos = tocFile.position();
|
const uint32_t pos = tocIn.position();
|
||||||
auto tocEntry = readTocEntry(tocFile);
|
readTocEntryFrom(tocIn);
|
||||||
serialization::writePod(bookFile, pos + lutOffset + lutSize + static_cast<uint32_t>(spineFile.position()));
|
serialization::writePod(bookOut, pos + lutOffset + lutSize + spineBytes);
|
||||||
}
|
}
|
||||||
|
|
||||||
// LUTs complete
|
// LUTs complete
|
||||||
@@ -160,9 +235,9 @@ bool BookMetadataCache::buildBookBin(const std::string& epubPath, const BookMeta
|
|||||||
|
|
||||||
// Build spineIndex->tocIndex mapping in one pass (O(n) instead of O(n*m))
|
// Build spineIndex->tocIndex mapping in one pass (O(n) instead of O(n*m))
|
||||||
std::deque<int16_t> spineToTocIndex(spineCount, -1);
|
std::deque<int16_t> spineToTocIndex(spineCount, -1);
|
||||||
tocFile.seek(0);
|
tocIn.seek(0);
|
||||||
for (int j = 0; j < tocCount; j++) {
|
for (int j = 0; j < tocCount; j++) {
|
||||||
auto tocEntry = readTocEntry(tocFile);
|
auto tocEntry = readTocEntryFrom(tocIn);
|
||||||
if (tocEntry.spineIndex >= 0 && tocEntry.spineIndex < spineCount) {
|
if (tocEntry.spineIndex >= 0 && tocEntry.spineIndex < spineCount) {
|
||||||
if (spineToTocIndex[tocEntry.spineIndex] == -1) {
|
if (spineToTocIndex[tocEntry.spineIndex] == -1) {
|
||||||
spineToTocIndex[tocEntry.spineIndex] = static_cast<int16_t>(j);
|
spineToTocIndex[tocEntry.spineIndex] = static_cast<int16_t>(j);
|
||||||
@@ -197,9 +272,9 @@ bool BookMetadataCache::buildBookBin(const std::string& epubPath, const BookMeta
|
|||||||
std::deque<ZipFile::SizeTarget> targets;
|
std::deque<ZipFile::SizeTarget> targets;
|
||||||
targets.resize(spineCount);
|
targets.resize(spineCount);
|
||||||
|
|
||||||
spineFile.seek(0);
|
spineIn.seek(0);
|
||||||
for (int i = 0; i < spineCount; i++) {
|
for (int i = 0; i < spineCount; i++) {
|
||||||
auto entry = readSpineEntry(spineFile);
|
auto entry = readSpineEntryFrom(spineIn);
|
||||||
std::string path = FsHelpers::normalisePath(entry.href);
|
std::string path = FsHelpers::normalisePath(entry.href);
|
||||||
|
|
||||||
ZipFile::SizeTarget t;
|
ZipFile::SizeTarget t;
|
||||||
@@ -224,10 +299,10 @@ bool BookMetadataCache::buildBookBin(const std::string& epubPath, const BookMeta
|
|||||||
}
|
}
|
||||||
|
|
||||||
uint32_t cumSize = 0;
|
uint32_t cumSize = 0;
|
||||||
spineFile.seek(0);
|
spineIn.seek(0);
|
||||||
int lastSpineTocIndex = -1;
|
int lastSpineTocIndex = -1;
|
||||||
for (int i = 0; i < spineCount; i++) {
|
for (int i = 0; i < spineCount; i++) {
|
||||||
auto spineEntry = readSpineEntry(spineFile);
|
auto spineEntry = readSpineEntryFrom(spineIn);
|
||||||
|
|
||||||
spineEntry.tocIndex = spineToTocIndex[i];
|
spineEntry.tocIndex = spineToTocIndex[i];
|
||||||
|
|
||||||
@@ -260,23 +335,33 @@ bool BookMetadataCache::buildBookBin(const std::string& epubPath, const BookMeta
|
|||||||
spineEntry.cumulativeSize = cumSize;
|
spineEntry.cumulativeSize = cumSize;
|
||||||
|
|
||||||
// Write out spine data to book.bin
|
// Write out spine data to book.bin
|
||||||
writeSpineEntry(bookFile, spineEntry);
|
writeSpineEntryTo(bookOut, spineEntry);
|
||||||
}
|
}
|
||||||
// Close opened zip file
|
// Close opened zip file
|
||||||
zip.close();
|
zip.close();
|
||||||
|
|
||||||
// Loop through toc entries from toc file writing to book.bin
|
// Loop through toc entries from toc file writing to book.bin
|
||||||
tocFile.seek(0);
|
tocIn.seek(0);
|
||||||
for (int i = 0; i < tocCount; i++) {
|
for (int i = 0; i < tocCount; i++) {
|
||||||
auto tocEntry = readTocEntry(tocFile);
|
auto tocEntry = readTocEntryFrom(tocIn);
|
||||||
writeTocEntry(bookFile, tocEntry);
|
writeTocEntryTo(bookOut, tocEntry);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const bool written = bookOut.flush();
|
||||||
|
|
||||||
// Explicit close() required: member variables persist beyond function scope
|
// Explicit close() required: member variables persist beyond function scope
|
||||||
bookFile.close();
|
bookFile.close();
|
||||||
spineFile.close();
|
spineFile.close();
|
||||||
tocFile.close();
|
tocFile.close();
|
||||||
|
|
||||||
|
if (!written) {
|
||||||
|
// A short write (card full/removed) would leave a truncated book.bin that
|
||||||
|
// still passes the version check on load; remove it so the next open rebuilds.
|
||||||
|
LOG_ERR("BMC", "Failed writing book.bin, removing truncated file");
|
||||||
|
Storage.remove((cachePath + bookBinFile).c_str());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
LOG_DBG("BMC", "Successfully built book.bin");
|
LOG_DBG("BMC", "Successfully built book.bin");
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -294,21 +379,11 @@ bool BookMetadataCache::cleanupTmpFiles() const {
|
|||||||
}
|
}
|
||||||
|
|
||||||
uint32_t BookMetadataCache::writeSpineEntry(HalFile& file, const SpineEntry& entry) const {
|
uint32_t BookMetadataCache::writeSpineEntry(HalFile& file, const SpineEntry& entry) const {
|
||||||
const uint32_t pos = file.position();
|
return writeSpineEntryTo(file, entry);
|
||||||
serialization::writeString(file, entry.href);
|
|
||||||
serialization::writePod(file, entry.cumulativeSize);
|
|
||||||
serialization::writePod(file, entry.tocIndex);
|
|
||||||
return pos;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
uint32_t BookMetadataCache::writeTocEntry(HalFile& file, const TocEntry& entry) const {
|
uint32_t BookMetadataCache::writeTocEntry(HalFile& file, const TocEntry& entry) const {
|
||||||
const uint32_t pos = file.position();
|
return writeTocEntryTo(file, entry);
|
||||||
serialization::writeString(file, entry.title);
|
|
||||||
serialization::writeString(file, entry.href);
|
|
||||||
serialization::writeString(file, entry.anchor);
|
|
||||||
serialization::writePod(file, entry.level);
|
|
||||||
serialization::writePod(file, entry.spineIndex);
|
|
||||||
return pos;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Note: for the LUT to be accurate, this **MUST** be called for all spine items before `addTocEntry` is ever called
|
// Note: for the LUT to be accurate, this **MUST** be called for all spine items before `addTocEntry` is ever called
|
||||||
@@ -320,7 +395,11 @@ void BookMetadataCache::createSpineEntry(const std::string& href) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const SpineEntry entry(href, 0, -1);
|
const SpineEntry entry(href, 0, -1);
|
||||||
writeSpineEntry(spineFile, entry);
|
if (passOut) {
|
||||||
|
writeSpineEntryTo(*passOut, entry);
|
||||||
|
} else {
|
||||||
|
writeSpineEntry(spineFile, entry);
|
||||||
|
}
|
||||||
spineCount++;
|
spineCount++;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -368,7 +447,11 @@ void BookMetadataCache::createTocEntry(const std::string& title, const std::stri
|
|||||||
// Compose the title to NFC at index time so the cache stores precomposed glyphs;
|
// Compose the title to NFC at index time so the cache stores precomposed glyphs;
|
||||||
// device fonts have no combining-mark positioning, so NFD titles render broken.
|
// device fonts have no combining-mark positioning, so NFD titles render broken.
|
||||||
const TocEntry entry(utf8ComposeNfc(title), href, anchor, level, spineIndex);
|
const TocEntry entry(utf8ComposeNfc(title), href, anchor, level, spineIndex);
|
||||||
writeTocEntry(tocFile, entry);
|
if (passOut) {
|
||||||
|
writeTocEntryTo(*passOut, entry);
|
||||||
|
} else {
|
||||||
|
writeTocEntry(tocFile, entry);
|
||||||
|
}
|
||||||
tocCount++;
|
tocCount++;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -442,19 +525,7 @@ BookMetadataCache::TocEntry BookMetadataCache::getTocEntry(const int index) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
BookMetadataCache::SpineEntry BookMetadataCache::readSpineEntry(HalFile& file) const {
|
BookMetadataCache::SpineEntry BookMetadataCache::readSpineEntry(HalFile& file) const {
|
||||||
SpineEntry entry;
|
return readSpineEntryFrom(file);
|
||||||
serialization::readString(file, entry.href);
|
|
||||||
serialization::readPod(file, entry.cumulativeSize);
|
|
||||||
serialization::readPod(file, entry.tocIndex);
|
|
||||||
return entry;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
BookMetadataCache::TocEntry BookMetadataCache::readTocEntry(HalFile& file) const {
|
BookMetadataCache::TocEntry BookMetadataCache::readTocEntry(HalFile& file) const { return readTocEntryFrom(file); }
|
||||||
TocEntry entry;
|
|
||||||
serialization::readString(file, entry.title);
|
|
||||||
serialization::readString(file, entry.href);
|
|
||||||
serialization::readString(file, entry.anchor);
|
|
||||||
serialization::readPod(file, entry.level);
|
|
||||||
serialization::readPod(file, entry.spineIndex);
|
|
||||||
return entry;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include <BufferedFile.h>
|
||||||
#include <HalStorage.h>
|
#include <HalStorage.h>
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <deque>
|
#include <deque>
|
||||||
|
#include <memory>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
|
||||||
class BookMetadataCache {
|
class BookMetadataCache {
|
||||||
@@ -54,6 +56,11 @@ class BookMetadataCache {
|
|||||||
// Temp file handles during build
|
// Temp file handles during build
|
||||||
HalFile spineFile;
|
HalFile spineFile;
|
||||||
HalFile tocFile;
|
HalFile tocFile;
|
||||||
|
// Buffers the per-entry tmp-file writes during the OPF/TOC passes: those
|
||||||
|
// writes interleave with zip-inflate SD reads, and unbuffered they thrash
|
||||||
|
// SdFat's shared sector cache (one 512B transaction per 4-byte pod). One
|
||||||
|
// wrapper serves whichever pass is active (spine, then toc).
|
||||||
|
std::unique_ptr<serialization::BufferedFileWriter> passOut;
|
||||||
|
|
||||||
// Index for fast href→spineIndex lookup (used only for large EPUBs)
|
// Index for fast href→spineIndex lookup (used only for large EPUBs)
|
||||||
struct SpineHrefIndexEntry {
|
struct SpineHrefIndexEntry {
|
||||||
|
|||||||
@@ -0,0 +1,73 @@
|
|||||||
|
#include "BookPages.h"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cmath>
|
||||||
|
#include <limits>
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
// Coarse seed used only before any section has an exact or live count; replaced
|
||||||
|
// by the calibrated average as soon as one is available.
|
||||||
|
constexpr double DEFAULT_BYTES_PER_PAGE = 2000.0;
|
||||||
|
// A section's serialized pageCount is a uint16_t, so no estimate needs to exceed this.
|
||||||
|
constexpr int MAX_SECTION_PAGES = std::numeric_limits<uint16_t>::max();
|
||||||
|
|
||||||
|
int clampToInt(const uint64_t v) {
|
||||||
|
return v > static_cast<uint64_t>(std::numeric_limits<int>::max()) ? std::numeric_limits<int>::max()
|
||||||
|
: static_cast<int>(v);
|
||||||
|
}
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
BookPagePosition computeBookPagePosition(const BookPageEntry* entries, const int sectionCount, const int spineIndex,
|
||||||
|
const int pageInSection, const int liveSectionPages) {
|
||||||
|
BookPagePosition pos;
|
||||||
|
if (!entries || sectionCount <= 0) {
|
||||||
|
return pos;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calibrate bytes-per-page from every section with an exact count (empty
|
||||||
|
// known-0 chapters excluded), plus the live estimate for spineIndex when it
|
||||||
|
// has no exact count yet.
|
||||||
|
uint64_t knownBytes = 0;
|
||||||
|
uint64_t knownPages = 0;
|
||||||
|
for (int i = 0; i < sectionCount; ++i) {
|
||||||
|
if (entries[i].pages > 0) {
|
||||||
|
knownBytes += entries[i].bytes;
|
||||||
|
knownPages += static_cast<uint32_t>(entries[i].pages);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const bool useLive = spineIndex >= 0 && spineIndex < sectionCount && entries[spineIndex].pages < 0 &&
|
||||||
|
liveSectionPages > 0 && entries[spineIndex].bytes > 0;
|
||||||
|
if (useLive) {
|
||||||
|
knownBytes += entries[spineIndex].bytes;
|
||||||
|
knownPages += static_cast<uint32_t>(liveSectionPages);
|
||||||
|
}
|
||||||
|
const double bytesPerPage = (knownBytes > 0 && knownPages > 0)
|
||||||
|
? static_cast<double>(knownBytes) / static_cast<double>(knownPages)
|
||||||
|
: DEFAULT_BYTES_PER_PAGE;
|
||||||
|
|
||||||
|
uint64_t total = 0;
|
||||||
|
uint64_t before = 0;
|
||||||
|
bool exact = true;
|
||||||
|
for (int i = 0; i < sectionCount; ++i) {
|
||||||
|
uint32_t pages;
|
||||||
|
if (entries[i].pages >= 0) {
|
||||||
|
pages = static_cast<uint32_t>(entries[i].pages); // exact (0 = genuinely empty chapter)
|
||||||
|
} else if (i == spineIndex && liveSectionPages > 0) {
|
||||||
|
pages = static_cast<uint32_t>(std::min(liveSectionPages, MAX_SECTION_PAGES));
|
||||||
|
exact = false;
|
||||||
|
} else {
|
||||||
|
const double raw = std::floor(static_cast<double>(entries[i].bytes) / bytesPerPage + 0.5);
|
||||||
|
pages = static_cast<uint32_t>(std::clamp(raw, 1.0, static_cast<double>(MAX_SECTION_PAGES)));
|
||||||
|
exact = false;
|
||||||
|
}
|
||||||
|
total += pages;
|
||||||
|
if (i < spineIndex) {
|
||||||
|
before += pages;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pos.totalPages = clampToInt(total);
|
||||||
|
pos.currentPage = clampToInt(before + static_cast<uint64_t>(std::max(0, pageInSection)) + 1);
|
||||||
|
pos.isEstimate = !exact;
|
||||||
|
return pos;
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
|
||||||
|
// Whole-book page accounting for book-global "page X of Y".
|
||||||
|
//
|
||||||
|
// The finalized section cache files (sections/*.bin) are the single source of
|
||||||
|
// truth for exact per-section counts; the caller harvests them into an array of
|
||||||
|
// BookPageEntry (nothing is persisted separately). Sections without an exact
|
||||||
|
// count are estimated from their byte size, calibrated against the sections
|
||||||
|
// whose counts are known — so the total gets more accurate as more of the book
|
||||||
|
// is paginated, and becomes exact once every section is.
|
||||||
|
|
||||||
|
struct BookPageEntry {
|
||||||
|
uint32_t bytes = 0; // uncompressed XHTML size, for estimating unknown sections
|
||||||
|
int32_t pages = -1; // exact count from a finalized section cache; -1 = unknown (0 = empty chapter)
|
||||||
|
};
|
||||||
|
|
||||||
|
struct BookPagePosition {
|
||||||
|
int currentPage = 0;
|
||||||
|
int totalPages = 0;
|
||||||
|
bool isEstimate = true; // true until every section has an exact count
|
||||||
|
};
|
||||||
|
|
||||||
|
// Book-global position: currentPage = pages before spineIndex + pageInSection + 1.
|
||||||
|
// liveSectionPages is the in-progress build's estimate for spineIndex (see
|
||||||
|
// Section::estimatedTotalPages); it is used for that section when it has no exact
|
||||||
|
// count yet and folded into the bytes-per-page calibration. Pure function: no I/O.
|
||||||
|
BookPagePosition computeBookPagePosition(const BookPageEntry* entries, int sectionCount, int spineIndex,
|
||||||
|
int pageInSection, int liveSectionPages);
|
||||||
+32
-1
@@ -39,7 +39,17 @@ std::unique_ptr<PageLine> PageLine::deserialize(HalFile& file) {
|
|||||||
serialization::readPod(file, yPos);
|
serialization::readPod(file, yPos);
|
||||||
|
|
||||||
auto tb = TextBlock::deserialize(file);
|
auto tb = TextBlock::deserialize(file);
|
||||||
return std::unique_ptr<PageLine>(new PageLine(std::move(tb), xPos, yPos));
|
if (!tb) {
|
||||||
|
LOG_ERR("PGE", "Deserialization failed: null TextBlock");
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto* line = new (std::nothrow) PageLine(std::move(tb), xPos, yPos);
|
||||||
|
if (!line) {
|
||||||
|
LOG_ERR("PGE", "Deserialization failed: could not allocate PageLine");
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
return std::unique_ptr<PageLine>(line);
|
||||||
}
|
}
|
||||||
|
|
||||||
void PageImage::render(GfxRenderer& renderer, const int fontId, const int xOffset, const int yOffset) {
|
void PageImage::render(GfxRenderer& renderer, const int fontId, const int xOffset, const int yOffset) {
|
||||||
@@ -47,6 +57,10 @@ void PageImage::render(GfxRenderer& renderer, const int fontId, const int xOffse
|
|||||||
imageBlock->render(renderer, xPos + xOffset, yPos + yOffset);
|
imageBlock->render(renderer, xPos + xOffset, yPos + yOffset);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void PageImage::renderPlaceholder(GfxRenderer& renderer, const int xOffset, const int yOffset) const {
|
||||||
|
imageBlock->renderPlaceholder(renderer, xPos + xOffset, yPos + yOffset);
|
||||||
|
}
|
||||||
|
|
||||||
bool PageImage::serialize(HalFile& file) {
|
bool PageImage::serialize(HalFile& file) {
|
||||||
serialization::writePod(file, xPos);
|
serialization::writePod(file, xPos);
|
||||||
serialization::writePod(file, yPos);
|
serialization::writePod(file, yPos);
|
||||||
@@ -115,6 +129,17 @@ void Page::renderImages(GfxRenderer& renderer, const int fontId, const int xOffs
|
|||||||
[](const PageElement& element) { return element.getTag() == TAG_PageImage; });
|
[](const PageElement& element) { return element.getTag() == TAG_PageImage; });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void Page::renderWithImagePlaceholders(GfxRenderer& renderer, const int fontId, const int xOffset,
|
||||||
|
const int yOffset) const {
|
||||||
|
for (const auto& element : elements) {
|
||||||
|
if (element->getTag() == TAG_PageImage) {
|
||||||
|
static_cast<const PageImage&>(*element).renderPlaceholder(renderer, xOffset, yOffset);
|
||||||
|
} else {
|
||||||
|
element->render(renderer, fontId, xOffset, yOffset);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
bool Page::serialize(HalFile& file) const {
|
bool Page::serialize(HalFile& file) const {
|
||||||
const uint16_t count = elements.size();
|
const uint16_t count = elements.size();
|
||||||
serialization::writePod(file, count);
|
serialization::writePod(file, count);
|
||||||
@@ -155,9 +180,15 @@ std::unique_ptr<Page> Page::deserialize(HalFile& file) {
|
|||||||
|
|
||||||
if (tag == TAG_PageLine) {
|
if (tag == TAG_PageLine) {
|
||||||
auto pl = PageLine::deserialize(file);
|
auto pl = PageLine::deserialize(file);
|
||||||
|
if (!pl) {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
page->elements.push_back(std::move(pl));
|
page->elements.push_back(std::move(pl));
|
||||||
} else if (tag == TAG_PageImage) {
|
} else if (tag == TAG_PageImage) {
|
||||||
auto pi = PageImage::deserialize(file);
|
auto pi = PageImage::deserialize(file);
|
||||||
|
if (!pi) {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
page->elements.push_back(std::move(pi));
|
page->elements.push_back(std::move(pi));
|
||||||
} else if (tag == TAG_PageHorizontalRule) {
|
} else if (tag == TAG_PageHorizontalRule) {
|
||||||
auto rule = PageHorizontalRule::deserialize(file);
|
auto rule = PageHorizontalRule::deserialize(file);
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ class PageImage final : public PageElement {
|
|||||||
PageImage(std::shared_ptr<ImageBlock> block, const int16_t xPos, const int16_t yPos)
|
PageImage(std::shared_ptr<ImageBlock> block, const int16_t xPos, const int16_t yPos)
|
||||||
: PageElement(xPos, yPos), imageBlock(std::move(block)) {}
|
: PageElement(xPos, yPos), imageBlock(std::move(block)) {}
|
||||||
void render(GfxRenderer& renderer, int fontId, int xOffset, int yOffset) override;
|
void render(GfxRenderer& renderer, int fontId, int xOffset, int yOffset) override;
|
||||||
|
void renderPlaceholder(GfxRenderer& renderer, int xOffset, int yOffset) const;
|
||||||
bool serialize(HalFile& file) override;
|
bool serialize(HalFile& file) override;
|
||||||
PageElementTag getTag() const override { return TAG_PageImage; }
|
PageElementTag getTag() const override { return TAG_PageImage; }
|
||||||
static std::unique_ptr<PageImage> deserialize(HalFile& file);
|
static std::unique_ptr<PageImage> deserialize(HalFile& file);
|
||||||
@@ -89,6 +90,7 @@ class Page {
|
|||||||
|
|
||||||
void render(GfxRenderer& renderer, int fontId, int xOffset, int yOffset) const;
|
void render(GfxRenderer& renderer, int fontId, int xOffset, int yOffset) const;
|
||||||
void renderImages(GfxRenderer& renderer, int fontId, int xOffset, int yOffset) const;
|
void renderImages(GfxRenderer& renderer, int fontId, int xOffset, int yOffset) const;
|
||||||
|
void renderWithImagePlaceholders(GfxRenderer& renderer, int fontId, int xOffset, int yOffset) const;
|
||||||
bool serialize(HalFile& file) const;
|
bool serialize(HalFile& file) const;
|
||||||
static std::unique_ptr<Page> deserialize(HalFile& file);
|
static std::unique_ptr<Page> deserialize(HalFile& file);
|
||||||
|
|
||||||
@@ -98,6 +100,13 @@ class Page {
|
|||||||
[](const std::shared_ptr<PageElement>& el) { return el->getTag() == TAG_PageImage; });
|
[](const std::shared_ptr<PageElement>& el) { return el->getTag() == TAG_PageImage; });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool hasImagesNeedingDecode() const {
|
||||||
|
return std::any_of(elements.begin(), elements.end(), [](const std::shared_ptr<PageElement>& element) {
|
||||||
|
return element->getTag() == TAG_PageImage &&
|
||||||
|
static_cast<const PageImage&>(*element).getImageBlock().needsDecode();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Get bounding box of all images on the page (union of image rects)
|
// Get bounding box of all images on the page (union of image rects)
|
||||||
// Returns false if no images. Coordinates are relative to page origin.
|
// Returns false if no images. Coordinates are relative to page origin.
|
||||||
bool getImageBoundingBox(int16_t& outX, int16_t& outY, int16_t& outW, int16_t& outH) const {
|
bool getImageBoundingBox(int16_t& outX, int16_t& outY, int16_t& outW, int16_t& outH) const {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
#include <BidiUtils.h>
|
#include <BidiUtils.h>
|
||||||
#include <GfxRenderer.h>
|
#include <GfxRenderer.h>
|
||||||
|
#include <Logging.h>
|
||||||
#include <Utf8.h>
|
#include <Utf8.h>
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
@@ -1133,8 +1134,14 @@ void ParsedText::extractLine(const size_t breakIndex, const int pageWidth, const
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!lineHasFocusSplit) {
|
if (!lineHasFocusSplit) {
|
||||||
processLine(std::make_shared<TextBlock>(std::move(lineWords), std::move(lineXPos), std::move(lineWordStyles),
|
// TextBlock flattens the vectors into its arena; they stay owned here and die at return.
|
||||||
std::vector<uint8_t>{}, std::vector<uint16_t>{}, blockStyle));
|
auto block = std::make_shared<TextBlock>(lineWords, lineXPos, lineWordStyles, std::vector<uint8_t>{},
|
||||||
|
std::vector<uint16_t>{}, blockStyle);
|
||||||
|
if (!block->valid()) {
|
||||||
|
LOG_ERR("PTX", "Dropping line: TextBlock arena allocation failed");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
processLine(std::move(block));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1179,6 +1186,10 @@ void ParsedText::extractLine(const size_t breakIndex, const int pageWidth, const
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
processLine(std::make_shared<TextBlock>(std::move(outWords), std::move(outXPos), std::move(outStyles),
|
auto block = std::make_shared<TextBlock>(outWords, outXPos, outStyles, outBoundaries, outSuffixX, blockStyle);
|
||||||
std::move(outBoundaries), std::move(outSuffixX), blockStyle));
|
if (!block->valid()) {
|
||||||
|
LOG_ERR("PTX", "Dropping line: TextBlock arena allocation failed");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
processLine(std::move(block));
|
||||||
}
|
}
|
||||||
|
|||||||
+562
-137
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
#include <HalStorage.h>
|
#include <HalStorage.h>
|
||||||
#include <Logging.h>
|
#include <Logging.h>
|
||||||
|
#include <Memory.h>
|
||||||
#include <Serialization.h>
|
#include <Serialization.h>
|
||||||
|
|
||||||
#include "Epub/css/CssParser.h"
|
#include "Epub/css/CssParser.h"
|
||||||
@@ -10,34 +11,94 @@
|
|||||||
#include "parsers/ChapterHtmlSlimParser.h"
|
#include "parsers/ChapterHtmlSlimParser.h"
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
// v27: words NFC-composed at layout time; bump invalidates NFD section caches.
|
// v28: text decoration bits now include line-through in serialized wordStyles.
|
||||||
constexpr uint8_t SECTION_FILE_VERSION = 27;
|
// v29: TextBlock word data stored as one flat arena (offset table + NUL-terminated
|
||||||
|
// text blob) instead of length-prefixed strings and per-field arrays.
|
||||||
|
// v30: Arabic shaping changed both drawing and measurement (getTextAdvanceX now
|
||||||
|
// measures the shaped visual text); cached word positions from v29 no longer
|
||||||
|
// match what drawText renders.
|
||||||
|
constexpr uint8_t SECTION_FILE_VERSION = 30;
|
||||||
|
// Written into the version field while a build is in progress; patched to
|
||||||
|
// SECTION_FILE_VERSION only when the build is finalized. An abandoned /
|
||||||
|
// crash-interrupted .bin therefore carries version 0, which loadSectionFile rejects
|
||||||
|
// as unknown and clears -- so an incomplete file is never mistaken for a valid one.
|
||||||
|
constexpr uint8_t SECTION_FILE_INCOMPLETE_VERSION = 0;
|
||||||
|
// Written when a build is suspended partway (reader exited or device slept mid-build).
|
||||||
|
// The file carries valid pages 0..pageCount-1, all LUTs, and a trailer with the parse
|
||||||
|
// watermark (bytesConsumed, totalBytes) appended after the li LUT. loadSectionFile
|
||||||
|
// accepts it so a resume shows those pages instantly; the reader extends it by
|
||||||
|
// rebuilding in the background. Uses the same header layout as SECTION_FILE_VERSION,
|
||||||
|
// so finalized files are untouched by this feature; older firmware treats the sentinel
|
||||||
|
// as an unknown version and rebuilds, which is a safe downgrade.
|
||||||
|
// MUST change in lockstep with SECTION_FILE_VERSION: the sentinel IS the partial's
|
||||||
|
// format version, so a stale-format partial otherwise passes the header check and
|
||||||
|
// only fails (noisily, via the block-decode error path) when a page is loaded.
|
||||||
|
// Derived so the pairing can't be forgotten: 0xFE for v28, 0xFD for v29, ...
|
||||||
|
constexpr uint8_t SECTION_FILE_PARTIAL_VERSION = 0xFE - (SECTION_FILE_VERSION - 28);
|
||||||
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(uint8_t) + sizeof(bool) + sizeof(uint32_t) + sizeof(uint32_t) +
|
sizeof(uint8_t) + sizeof(bool) + sizeof(uint32_t) + sizeof(uint32_t) +
|
||||||
sizeof(uint32_t) + sizeof(uint32_t);
|
sizeof(uint32_t) + sizeof(uint32_t);
|
||||||
|
|
||||||
struct PageLutEntry {
|
// Read the render-parameter block of the header (everything between the version
|
||||||
uint32_t fileOffset;
|
// byte and pageCount), in writeSectionFileHeader order. The file cursor must sit
|
||||||
uint16_t paragraphIndex;
|
// just past the version byte.
|
||||||
uint16_t listItemIndex;
|
Section::RenderParams readHeaderRenderParams(HalFile& f) {
|
||||||
};
|
Section::RenderParams p;
|
||||||
|
serialization::readPod(f, p.fontId);
|
||||||
|
serialization::readPod(f, p.lineCompression);
|
||||||
|
serialization::readPod(f, p.extraParagraphSpacing);
|
||||||
|
serialization::readPod(f, p.paragraphAlignment);
|
||||||
|
serialization::readPod(f, p.viewportWidth);
|
||||||
|
serialization::readPod(f, p.viewportHeight);
|
||||||
|
serialization::readPod(f, p.hyphenationEnabled);
|
||||||
|
serialization::readPod(f, p.embeddedStyle);
|
||||||
|
serialization::readPod(f, p.imageRendering);
|
||||||
|
serialization::readPod(f, p.focusReadingEnabled);
|
||||||
|
return p;
|
||||||
|
}
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
|
bool Section::RenderParams::operator==(const RenderParams& o) const {
|
||||||
|
return fontId == o.fontId && lineCompression == o.lineCompression &&
|
||||||
|
extraParagraphSpacing == o.extraParagraphSpacing && paragraphAlignment == o.paragraphAlignment &&
|
||||||
|
viewportWidth == o.viewportWidth && viewportHeight == o.viewportHeight &&
|
||||||
|
hyphenationEnabled == o.hyphenationEnabled && embeddedStyle == o.embeddedStyle &&
|
||||||
|
imageRendering == o.imageRendering && focusReadingEnabled == o.focusReadingEnabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Out-of-line so the unique_ptr<ChapterHtmlSlimParser> in BuildContext can be
|
||||||
|
// constructed/destroyed where the parser's full definition is visible.
|
||||||
|
Section::Section(const std::shared_ptr<Epub>& epub, const int spineIndex, GfxRenderer& renderer)
|
||||||
|
: epub(epub),
|
||||||
|
spineIndex(spineIndex),
|
||||||
|
renderer(renderer),
|
||||||
|
filePath(epub->getCachePath() + "/sections/" + std::to_string(spineIndex) + ".bin") {}
|
||||||
|
|
||||||
|
// Suspend any in-progress build so every section.reset() / navigation / sleep path
|
||||||
|
// persists the pages already laid out as a partial .bin instead of discarding them
|
||||||
|
// (no-op once a build has completed or never started).
|
||||||
|
Section::~Section() { suspendBuild(); }
|
||||||
|
|
||||||
uint32_t Section::onPageComplete(std::unique_ptr<Page> page) {
|
uint32_t Section::onPageComplete(std::unique_ptr<Page> page) {
|
||||||
if (!file) {
|
if (!file) {
|
||||||
LOG_ERR("SCT", "File not open for writing page %d", pageCount);
|
LOG_ERR("SCT", "File not open for writing page %d", builtPageCount_);
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
const uint32_t position = file.position();
|
const uint32_t position = file.position();
|
||||||
if (!page->serialize(file)) {
|
if (!page->serialize(file)) {
|
||||||
LOG_ERR("SCT", "Failed to serialize page %d", pageCount);
|
LOG_ERR("SCT", "Failed to serialize page %d", builtPageCount_);
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
LOG_DBG("SCT", "Page %d processed", pageCount);
|
LOG_DBG("SCT", "Page %d processed", builtPageCount_);
|
||||||
|
|
||||||
pageCount++;
|
builtPageCount_++;
|
||||||
|
// pageCount is the pages available to read: a rebuild over a partial only raises it
|
||||||
|
// once it has laid out more pages than the partial already covers.
|
||||||
|
if (builtPageCount_ > pageCount) {
|
||||||
|
pageCount = builtPageCount_;
|
||||||
|
}
|
||||||
return position;
|
return position;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -56,7 +117,9 @@ void Section::writeSectionFileHeader(const int fontId, const float lineCompressi
|
|||||||
sizeof(embeddedStyle) + sizeof(imageRendering) + sizeof(focusReadingEnabled) +
|
sizeof(embeddedStyle) + sizeof(imageRendering) + sizeof(focusReadingEnabled) +
|
||||||
sizeof(uint32_t) + sizeof(uint32_t) + sizeof(uint32_t) + sizeof(uint32_t),
|
sizeof(uint32_t) + sizeof(uint32_t) + sizeof(uint32_t) + sizeof(uint32_t),
|
||||||
"Header size mismatch");
|
"Header size mismatch");
|
||||||
serialization::writePod(file, SECTION_FILE_VERSION);
|
// Written as the incomplete sentinel; finalizeBuild() patches it to
|
||||||
|
// SECTION_FILE_VERSION as the last step, committing the file.
|
||||||
|
serialization::writePod(file, SECTION_FILE_INCOMPLETE_VERSION);
|
||||||
serialization::writePod(file, fontId);
|
serialization::writePod(file, fontId);
|
||||||
serialization::writePod(file, lineCompression);
|
serialization::writePod(file, lineCompression);
|
||||||
serialization::writePod(file, extraParagraphSpacing);
|
serialization::writePod(file, extraParagraphSpacing);
|
||||||
@@ -83,42 +146,24 @@ bool Section::loadSectionFile(const int fontId, const float lineCompression, con
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Match parameters
|
// Match parameters
|
||||||
|
bool filePartial = false;
|
||||||
{
|
{
|
||||||
uint8_t version;
|
uint8_t version;
|
||||||
serialization::readPod(file, version);
|
serialization::readPod(file, version);
|
||||||
if (version != SECTION_FILE_VERSION) {
|
if (version != SECTION_FILE_VERSION && version != SECTION_FILE_PARTIAL_VERSION) {
|
||||||
// Explicit close() required: member variable persists beyond function scope
|
// Explicit close() required: member variable persists beyond function scope
|
||||||
file.close();
|
file.close();
|
||||||
LOG_ERR("SCT", "Deserialization failed: Unknown version %u", version);
|
LOG_ERR("SCT", "Deserialization failed: Unknown version %u", version);
|
||||||
clearCache();
|
clearCache();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
filePartial = (version == SECTION_FILE_PARTIAL_VERSION);
|
||||||
|
|
||||||
int fileFontId;
|
const RenderParams fileParams = readHeaderRenderParams(file);
|
||||||
uint16_t fileViewportWidth, fileViewportHeight;
|
const RenderParams params = {fontId, lineCompression, extraParagraphSpacing, paragraphAlignment,
|
||||||
float fileLineCompression;
|
viewportWidth, viewportHeight, hyphenationEnabled, embeddedStyle,
|
||||||
bool fileExtraParagraphSpacing;
|
imageRendering, focusReadingEnabled};
|
||||||
uint8_t fileParagraphAlignment;
|
if (!(fileParams == params)) {
|
||||||
bool fileHyphenationEnabled;
|
|
||||||
bool fileEmbeddedStyle;
|
|
||||||
uint8_t fileImageRendering;
|
|
||||||
bool fileFocusReadingEnabled;
|
|
||||||
serialization::readPod(file, fileFontId);
|
|
||||||
serialization::readPod(file, fileLineCompression);
|
|
||||||
serialization::readPod(file, fileExtraParagraphSpacing);
|
|
||||||
serialization::readPod(file, fileParagraphAlignment);
|
|
||||||
serialization::readPod(file, fileViewportWidth);
|
|
||||||
serialization::readPod(file, fileViewportHeight);
|
|
||||||
serialization::readPod(file, fileHyphenationEnabled);
|
|
||||||
serialization::readPod(file, fileEmbeddedStyle);
|
|
||||||
serialization::readPod(file, fileImageRendering);
|
|
||||||
serialization::readPod(file, fileFocusReadingEnabled);
|
|
||||||
|
|
||||||
if (fontId != fileFontId || lineCompression != fileLineCompression ||
|
|
||||||
extraParagraphSpacing != fileExtraParagraphSpacing || paragraphAlignment != fileParagraphAlignment ||
|
|
||||||
viewportWidth != fileViewportWidth || viewportHeight != fileViewportHeight ||
|
|
||||||
hyphenationEnabled != fileHyphenationEnabled || embeddedStyle != fileEmbeddedStyle ||
|
|
||||||
imageRendering != fileImageRendering || focusReadingEnabled != fileFocusReadingEnabled) {
|
|
||||||
file.close();
|
file.close();
|
||||||
LOG_ERR("SCT", "Deserialization failed: Parameters do not match");
|
LOG_ERR("SCT", "Deserialization failed: Parameters do not match");
|
||||||
clearCache();
|
clearCache();
|
||||||
@@ -127,14 +172,42 @@ bool Section::loadSectionFile(const int fontId, const float lineCompression, con
|
|||||||
}
|
}
|
||||||
|
|
||||||
serialization::readPod(file, pageCount);
|
serialization::readPod(file, pageCount);
|
||||||
|
|
||||||
|
if (filePartial) {
|
||||||
|
// A partial's pageCount is the watermark of a suspended build. Read the watermark
|
||||||
|
// trailer (appended after the li LUT) so estimatedTotalPages can extrapolate.
|
||||||
|
uint32_t liLutOffset = 0;
|
||||||
|
file.seek(HEADER_SIZE - sizeof(uint32_t));
|
||||||
|
serialization::readPod(file, liLutOffset);
|
||||||
|
const uint32_t trailerOffset = liLutOffset + static_cast<uint32_t>(pageCount) * sizeof(uint16_t);
|
||||||
|
const bool trailerValid =
|
||||||
|
pageCount > 0 && liLutOffset >= HEADER_SIZE && trailerOffset + 2 * sizeof(uint32_t) <= file.size();
|
||||||
|
if (!trailerValid) {
|
||||||
|
file.close();
|
||||||
|
LOG_ERR("SCT", "Deserialization failed: malformed partial section");
|
||||||
|
clearCache();
|
||||||
|
pageCount = 0;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
file.seek(trailerOffset);
|
||||||
|
serialization::readPod(file, partialBytesConsumed_);
|
||||||
|
serialization::readPod(file, partialTotalBytes_);
|
||||||
|
partial_ = true;
|
||||||
|
partialPageCount_ = pageCount;
|
||||||
|
}
|
||||||
|
|
||||||
// Explicit close() required: member variable persists beyond function scope
|
// Explicit close() required: member variable persists beyond function scope
|
||||||
file.close();
|
file.close();
|
||||||
LOG_DBG("SCT", "Deserialization succeeded: %d pages", pageCount);
|
LOG_DBG("SCT", "Deserialization succeeded: %d pages%s", pageCount, filePartial ? " (partial)" : "");
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Your updated class method (assuming you are using the 'SD' object, which is a wrapper for a specific filesystem)
|
// Your updated class method (assuming you are using the 'SD' object, which is a wrapper for a specific filesystem)
|
||||||
bool Section::clearCache() const {
|
bool Section::clearCache() const {
|
||||||
|
const std::string tmpBin = binTmpPath();
|
||||||
|
if (Storage.exists(tmpBin.c_str())) {
|
||||||
|
Storage.remove(tmpBin.c_str());
|
||||||
|
}
|
||||||
if (!Storage.exists(filePath.c_str())) {
|
if (!Storage.exists(filePath.c_str())) {
|
||||||
LOG_DBG("SCT", "Cache does not exist, no action needed");
|
LOG_DBG("SCT", "Cache does not exist, no action needed");
|
||||||
return true;
|
return true;
|
||||||
@@ -154,8 +227,43 @@ bool Section::createSectionFile(const int fontId, const float lineCompression, c
|
|||||||
const uint16_t viewportHeight, const bool hyphenationEnabled, const bool embeddedStyle,
|
const uint16_t viewportHeight, const bool hyphenationEnabled, const bool embeddedStyle,
|
||||||
const uint8_t imageRendering, const bool focusReadingEnabled,
|
const uint8_t imageRendering, const bool focusReadingEnabled,
|
||||||
const std::function<void()>& popupFn) {
|
const std::function<void()>& popupFn) {
|
||||||
|
// One-shot build: start, then lay out the whole section in a single pass.
|
||||||
|
if (!startBuild(fontId, lineCompression, extraParagraphSpacing, paragraphAlignment, viewportWidth, viewportHeight,
|
||||||
|
hyphenationEnabled, embeddedStyle, imageRendering, focusReadingEnabled, popupFn)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!buildSomeMore(0)) { // 0 = build to completion
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return buildComplete_;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Section::startBuild(const int fontId, const float lineCompression, const bool extraParagraphSpacing,
|
||||||
|
const uint8_t paragraphAlignment, const uint16_t viewportWidth, const uint16_t viewportHeight,
|
||||||
|
const bool hyphenationEnabled, const bool embeddedStyle, const uint8_t imageRendering,
|
||||||
|
const bool focusReadingEnabled, const std::function<void()>& popupFn) {
|
||||||
|
if (build_) {
|
||||||
|
LOG_ERR("SCT", "startBuild called while a build is already active");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
buildComplete_ = false;
|
||||||
|
builtPageCount_ = 0;
|
||||||
|
// Pages from a loaded partial stay readable (from filePath) while this build writes
|
||||||
|
// to the tmp .bin, so availability never drops below the partial's watermark.
|
||||||
|
pageCount = partial_ ? partialPageCount_ : 0;
|
||||||
|
|
||||||
|
// Remove a stale tmp .bin from a crash-interrupted build; this build recreates it.
|
||||||
|
{
|
||||||
|
const std::string staleTmp = binTmpPath();
|
||||||
|
if (Storage.exists(staleTmp.c_str())) {
|
||||||
|
Storage.remove(staleTmp.c_str());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
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 htmlDir = epub->getCachePath() + "/html";
|
||||||
|
const auto htmlPath = htmlDir + "/" + std::to_string(spineIndex) + ".html";
|
||||||
|
const auto tmpHtmlPath = htmlDir + "/.tmp_" + std::to_string(spineIndex) + ".html";
|
||||||
|
|
||||||
// Create cache directory if it doesn't exist
|
// Create cache directory if it doesn't exist
|
||||||
{
|
{
|
||||||
@@ -163,62 +271,101 @@ bool Section::createSectionFile(const int fontId, const float lineCompression, c
|
|||||||
Storage.mkdir(sectionsDir.c_str());
|
Storage.mkdir(sectionsDir.c_str());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Retry logic for SD card timing issues
|
// Reuse the previously unzipped HTML if we already have it. The unzipped HTML is keyed only on the
|
||||||
bool success = false;
|
// book (it lives in the per-book cache dir), not on render settings, so it survives the invalidation
|
||||||
uint32_t fileSize = 0;
|
// that wipes the layout (.bin) caches when font/margin/orientation change -- rebuilds then skip zip
|
||||||
for (int attempt = 0; attempt < 3 && !success; attempt++) {
|
// inflation entirely. It's promoted by an atomic rename as soon as the inflate succeeds (below), so
|
||||||
if (attempt > 0) {
|
// even a window-only giant spine -- whose .bin never finalizes -- still caches its HTML, letting a
|
||||||
LOG_DBG("SCT", "Retrying stream (attempt %d)...", attempt + 1);
|
// reopen skip the multi-second inflate. If htmlPath exists it is known-complete.
|
||||||
delay(50); // Brief delay before retry
|
const bool reusedHtml = Storage.exists(htmlPath.c_str());
|
||||||
|
bool htmlCached = reusedHtml;
|
||||||
|
if (reusedHtml) {
|
||||||
|
LOG_DBG("SCT", "Reusing cached HTML %s", htmlPath.c_str());
|
||||||
|
} else {
|
||||||
|
Storage.mkdir(htmlDir.c_str());
|
||||||
|
|
||||||
|
// Retry logic for SD card timing issues
|
||||||
|
bool streamed = false;
|
||||||
|
uint32_t fileSize = 0;
|
||||||
|
for (int attempt = 0; attempt < 3 && !streamed; attempt++) {
|
||||||
|
if (attempt > 0) {
|
||||||
|
LOG_DBG("SCT", "Retrying stream (attempt %d)...", attempt + 1);
|
||||||
|
delay(50); // Brief delay before retry
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove any incomplete file from previous attempt before retrying
|
||||||
|
if (Storage.exists(tmpHtmlPath.c_str())) {
|
||||||
|
Storage.remove(tmpHtmlPath.c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
HalFile tmpHtml;
|
||||||
|
if (!Storage.openFileForWrite("SCT", tmpHtmlPath, tmpHtml)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// Larger chunks mean far fewer SD writes inflating the HTML; a 1KB chunk turned a 584KB
|
||||||
|
// single-spine novel into ~570 tiny writes (multi-second). 8KB keeps the transient buffers
|
||||||
|
// small while cutting the write count 8x.
|
||||||
|
streamed = epub->readItemContentsToStream(localPath, tmpHtml, 8192);
|
||||||
|
fileSize = tmpHtml.size();
|
||||||
|
// Explicitly close() file before calling Storage.remove()
|
||||||
|
tmpHtml.close();
|
||||||
|
|
||||||
|
// If streaming failed, remove the incomplete file immediately
|
||||||
|
if (!streamed && Storage.exists(tmpHtmlPath.c_str())) {
|
||||||
|
Storage.remove(tmpHtmlPath.c_str());
|
||||||
|
LOG_DBG("SCT", "Removed incomplete temp file after failed attempt");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove any incomplete file from previous attempt before retrying
|
if (!streamed) {
|
||||||
if (Storage.exists(tmpHtmlPath.c_str())) {
|
LOG_ERR("SCT", "Failed to stream item contents to temp file after retries");
|
||||||
Storage.remove(tmpHtmlPath.c_str());
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
HalFile tmpHtml;
|
LOG_DBG("SCT", "Streamed temp HTML to %s (%d bytes)", tmpHtmlPath.c_str(), fileSize);
|
||||||
if (!Storage.openFileForWrite("SCT", tmpHtmlPath, tmpHtml)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
success = epub->readItemContentsToStream(localPath, tmpHtml, 1024);
|
|
||||||
fileSize = tmpHtml.size();
|
|
||||||
// Explicitly close() file before calling Storage.remove()
|
|
||||||
tmpHtml.close();
|
|
||||||
|
|
||||||
// If streaming failed, remove the incomplete file immediately
|
// Promote to the persistent HTML cache immediately -- the inflate is complete and the bytes are
|
||||||
if (!success && Storage.exists(tmpHtmlPath.c_str())) {
|
// valid regardless of whether the layout build finishes, so reopening (even a window-only spine
|
||||||
Storage.remove(tmpHtmlPath.c_str());
|
// that never finalizes its .bin) skips re-inflation. If the rename fails we just parse the temp.
|
||||||
LOG_DBG("SCT", "Removed incomplete temp file after failed attempt");
|
if (Storage.rename(tmpHtmlPath.c_str(), htmlPath.c_str())) {
|
||||||
|
htmlCached = true;
|
||||||
|
} else {
|
||||||
|
LOG_DBG("SCT", "Failed to promote HTML cache; parsing from temp");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!success) {
|
if (!Storage.openFileForWrite("SCT", binTmpPath(), file)) {
|
||||||
LOG_ERR("SCT", "Failed to stream item contents to temp file after retries");
|
if (!reusedHtml) Storage.remove(tmpHtmlPath.c_str());
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
LOG_DBG("SCT", "Streamed temp HTML to %s (%d bytes)", tmpHtmlPath.c_str(), fileSize);
|
|
||||||
|
|
||||||
if (!Storage.openFileForWrite("SCT", filePath, file)) {
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
// Header is written with the incomplete-version sentinel; finalizeBuild() commits it.
|
||||||
writeSectionFileHeader(fontId, lineCompression, extraParagraphSpacing, paragraphAlignment, viewportWidth,
|
writeSectionFileHeader(fontId, lineCompression, extraParagraphSpacing, paragraphAlignment, viewportWidth,
|
||||||
viewportHeight, hyphenationEnabled, embeddedStyle, imageRendering, focusReadingEnabled);
|
viewportHeight, hyphenationEnabled, embeddedStyle, imageRendering, focusReadingEnabled);
|
||||||
std::vector<PageLutEntry> lut = {};
|
|
||||||
|
auto ctx = makeUniqueNoThrow<BuildContext>();
|
||||||
|
if (!ctx) {
|
||||||
|
LOG_ERR("SCT", "OOM: BuildContext");
|
||||||
|
file.close();
|
||||||
|
Storage.remove(binTmpPath().c_str());
|
||||||
|
if (!reusedHtml) Storage.remove(tmpHtmlPath.c_str());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// htmlCached == "htmlPath is the live cache" (reused, or just promoted). finalizeBuild/abandonBuild
|
||||||
|
// then leave the cached HTML alone; only an un-promoted temp (rename failed) is theirs to clean up.
|
||||||
|
ctx->reusedHtml = htmlCached;
|
||||||
|
ctx->htmlPath = htmlPath;
|
||||||
|
ctx->tmpHtmlPath = tmpHtmlPath;
|
||||||
|
ctx->parsePath = htmlCached ? htmlPath : tmpHtmlPath;
|
||||||
|
|
||||||
// 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
|
||||||
size_t lastSlash = localPath.find_last_of('/');
|
const size_t lastSlash = localPath.find_last_of('/');
|
||||||
std::string contentBase = (lastSlash != std::string::npos) ? localPath.substr(0, lastSlash + 1) : "";
|
ctx->contentBase = (lastSlash != std::string::npos) ? localPath.substr(0, lastSlash + 1) : "";
|
||||||
std::string imageBasePath = epub->getCachePath() + "/img_" + std::to_string(spineIndex) + "_";
|
ctx->imageBasePath = epub->getCachePath() + "/img_" + std::to_string(spineIndex) + "_";
|
||||||
|
|
||||||
CssParser* cssParser = nullptr;
|
|
||||||
if (embeddedStyle) {
|
if (embeddedStyle) {
|
||||||
cssParser = epub->getCssParser();
|
ctx->cssParser = epub->getCssParser();
|
||||||
if (cssParser) {
|
if (ctx->cssParser && !ctx->cssParser->loadFromCache()) {
|
||||||
if (!cssParser->loadFromCache()) {
|
LOG_ERR("SCT", "Failed to load CSS from cache");
|
||||||
LOG_ERR("SCT", "Failed to load CSS from cache");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -235,113 +382,376 @@ bool Section::createSectionFile(const int fontId, const float lineCompression, c
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ChapterHtmlSlimParser visitor(
|
// The parser stores the path/contentBase/imageBasePath by reference, so they must
|
||||||
epub, tmpHtmlPath, renderer, fontId, lineCompression, extraParagraphSpacing, paragraphAlignment, viewportWidth,
|
// live in the BuildContext (which outlives the parser). The page-complete callback
|
||||||
viewportHeight, hyphenationEnabled, focusReadingEnabled,
|
// captures the BuildContext pointer to append to its in-RAM LUT; build_ owns the
|
||||||
[this, &lut](std::unique_ptr<Page> page, const uint16_t paragraphIndex, const uint16_t listItemIndex) {
|
// context for the parser's whole lifetime.
|
||||||
lut.push_back({this->onPageComplete(std::move(page)), paragraphIndex, listItemIndex});
|
BuildContext* ctxPtr = ctx.get();
|
||||||
|
ctx->parser = makeUniqueNoThrow<ChapterHtmlSlimParser>(
|
||||||
|
epub, ctxPtr->parsePath, renderer, fontId, lineCompression, extraParagraphSpacing, paragraphAlignment,
|
||||||
|
viewportWidth, viewportHeight, hyphenationEnabled, focusReadingEnabled,
|
||||||
|
[this, ctxPtr](std::unique_ptr<Page> page, const uint16_t paragraphIndex, const uint16_t listItemIndex) {
|
||||||
|
ctxPtr->lut.push_back({this->onPageComplete(std::move(page)), paragraphIndex, listItemIndex});
|
||||||
},
|
},
|
||||||
embeddedStyle, contentBase, imageBasePath, imageRendering, std::move(tocAnchors), popupFn, cssParser);
|
embeddedStyle, ctxPtr->contentBase, ctxPtr->imageBasePath, imageRendering, std::move(tocAnchors), popupFn,
|
||||||
Hyphenator::setPreferredLanguage(epub->getLanguage());
|
ctxPtr->cssParser);
|
||||||
success = visitor.parseAndBuildPages();
|
if (!ctx->parser) {
|
||||||
|
LOG_ERR("SCT", "OOM: ChapterHtmlSlimParser");
|
||||||
Storage.remove(tmpHtmlPath.c_str());
|
if (ctx->cssParser) ctx->cssParser->clear();
|
||||||
if (!success) {
|
|
||||||
LOG_ERR("SCT", "Failed to parse XML and build pages");
|
|
||||||
// Explicitly close() file before calling Storage.remove()
|
|
||||||
file.close();
|
file.close();
|
||||||
Storage.remove(filePath.c_str());
|
Storage.remove(binTmpPath().c_str());
|
||||||
if (cssParser) {
|
if (!reusedHtml) Storage.remove(tmpHtmlPath.c_str());
|
||||||
cssParser->clear();
|
|
||||||
}
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Hyphenator::setPreferredLanguage(epub->getLanguage());
|
||||||
|
build_ = std::move(ctx);
|
||||||
|
|
||||||
|
if (!build_->parser->beginParse()) {
|
||||||
|
LOG_ERR("SCT", "Failed to begin parse");
|
||||||
|
abandonBuild();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
build_->totalBytes = build_->parser->parseTotalBytes();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Section::buildSomeMore(const int maxPages) {
|
||||||
|
if (!build_ || !build_->parser) {
|
||||||
|
LOG_ERR("SCT", "buildSomeMore with no active build");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// Pace on pages laid out by THIS build, not pageCount: during a rebuild over a partial,
|
||||||
|
// pageCount stays pinned at the partial's watermark until the build passes it, which
|
||||||
|
// would otherwise turn one "small" chunk into a blocking rebuild of the whole watermark.
|
||||||
|
const int startCount = builtPageCount_;
|
||||||
|
for (;;) {
|
||||||
|
const auto status = build_->parser->parseStep();
|
||||||
|
if (status == ChapterHtmlSlimParser::ParseStatus::Error) {
|
||||||
|
LOG_ERR("SCT", "Parse error during incremental build");
|
||||||
|
abandonBuild();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (status == ChapterHtmlSlimParser::ParseStatus::Done) {
|
||||||
|
return finalizeBuild();
|
||||||
|
}
|
||||||
|
// ParseStatus::More: yield once we've laid out the requested number of pages.
|
||||||
|
if (maxPages > 0 && (builtPageCount_ - startCount) >= maxPages) {
|
||||||
|
build_->bytesConsumed = build_->parser->parseBytesConsumed();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Section::hasHtmlCache() const {
|
||||||
|
const std::string htmlPath = epub->getCachePath() + "/html/" + std::to_string(spineIndex) + ".html";
|
||||||
|
return Storage.exists(htmlPath.c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
std::optional<uint16_t> Section::findAnchorDuringBuild(const std::string& anchor) const {
|
||||||
|
if (!build_ || !build_->parser) return std::nullopt;
|
||||||
|
for (const auto& [key, page] : build_->parser->getAnchors()) {
|
||||||
|
if (key == anchor) return page;
|
||||||
|
}
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::optional<uint16_t> Section::findAnchor(const std::string& anchor) const {
|
||||||
|
if (const auto page = findAnchorDuringBuild(anchor)) {
|
||||||
|
return page;
|
||||||
|
}
|
||||||
|
// Fall back to the on-disk anchor map: a finalized section, or a partial whose map
|
||||||
|
// covers everything up to its watermark (nullopt past it -- build further and retry).
|
||||||
|
return getPageForAnchor(anchor);
|
||||||
|
}
|
||||||
|
|
||||||
|
uint16_t Section::estimatedTotalPages() const {
|
||||||
|
// Extrapolation from a suspended session's watermark trailer. A static snapshot, so no EMA
|
||||||
|
// damping is needed. Also the best guess while a rebuild is running but hasn't laid out
|
||||||
|
// enough pages yet to extrapolate from its own progress.
|
||||||
|
const auto partialEstimate = [this]() -> uint16_t {
|
||||||
|
if (!partial_ || partialBytesConsumed_ == 0 || partialTotalBytes_ <= partialBytesConsumed_) {
|
||||||
|
return pageCount;
|
||||||
|
}
|
||||||
|
const uint64_t est = static_cast<uint64_t>(partialPageCount_) * partialTotalBytes_ / partialBytesConsumed_;
|
||||||
|
if (est <= pageCount) return pageCount;
|
||||||
|
return est > 60000 ? 60000 : static_cast<uint16_t>(est);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!build_) {
|
||||||
|
return partial_ ? partialEstimate() : pageCount; // partial -> extrapolate, finalized -> exact
|
||||||
|
}
|
||||||
|
const uint32_t consumed = build_->bytesConsumed;
|
||||||
|
const uint32_t total = build_->totalBytes;
|
||||||
|
if (builtPageCount_ == 0 || consumed == 0 || total <= consumed) return partialEstimate();
|
||||||
|
|
||||||
|
// Raw extrapolation: scale the pages built so far by the fraction of HTML still unparsed. This
|
||||||
|
// re-derives from a growing, non-uniform sample, so it jitters up and down as the build crosses
|
||||||
|
// dense vs sparse regions of the chapter.
|
||||||
|
const uint64_t raw = static_cast<uint64_t>(builtPageCount_) * total / consumed;
|
||||||
|
|
||||||
|
// Damp that jitter with an exponential moving average. Step it once per build advance (keyed on
|
||||||
|
// bytesConsumed) rather than per status-bar redraw, so the smoothing rate doesn't depend on how
|
||||||
|
// often we repaint. As the build nears the end, consumed -> total and raw -> the built count, so
|
||||||
|
// the average settles onto the true count (and finalizeBuild then returns the exact pageCount).
|
||||||
|
constexpr float ALPHA = 0.25f; // weight of each new sample; lower = steadier but slower to settle
|
||||||
|
if (build_->smoothedEstimate <= 0) {
|
||||||
|
build_->smoothedEstimate = static_cast<float>(raw); // seed on the first estimate
|
||||||
|
} else if (consumed != build_->smoothedAtConsumed) {
|
||||||
|
build_->smoothedEstimate += ALPHA * (static_cast<float>(raw) - build_->smoothedEstimate);
|
||||||
|
}
|
||||||
|
build_->smoothedAtConsumed = consumed;
|
||||||
|
|
||||||
|
const uint64_t est = static_cast<uint64_t>(build_->smoothedEstimate + 0.5f);
|
||||||
|
if (est <= pageCount) return pageCount; // never fewer than the pages already available
|
||||||
|
return est > 60000 ? 60000 : static_cast<uint16_t>(est);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write the LUTs and anchor map into the open tmp .bin, patch the header with the built
|
||||||
|
// page count and table offsets, stamp `version` as the commit point, then swap the tmp
|
||||||
|
// file over filePath. For SECTION_FILE_PARTIAL_VERSION a watermark trailer
|
||||||
|
// (bytesConsumed, totalBytes) is appended after the li LUT so a later open can estimate
|
||||||
|
// the total page count. The parser must still be alive (anchors are read from it).
|
||||||
|
// On failure the tmp is removed and any pre-existing file at filePath is left intact.
|
||||||
|
bool Section::commitBuildFile(const uint8_t version, const uint32_t bytesConsumed, const uint32_t totalBytes) {
|
||||||
|
const bool asPartial = (version == SECTION_FILE_PARTIAL_VERSION);
|
||||||
|
|
||||||
|
const auto failCommit = [this]() {
|
||||||
|
// Explicit close() required before remove (member variable, O_RDWR handle).
|
||||||
|
file.close();
|
||||||
|
Storage.remove(binTmpPath().c_str());
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
const uint32_t lutOffset = file.position();
|
const uint32_t lutOffset = file.position();
|
||||||
bool hasFailedLutRecords = false;
|
for (const auto& entry : build_->lut) {
|
||||||
// Write LUT
|
|
||||||
for (const auto& entry : lut) {
|
|
||||||
if (entry.fileOffset == 0) {
|
if (entry.fileOffset == 0) {
|
||||||
hasFailedLutRecords = true;
|
LOG_ERR("SCT", "Failed to write LUT due to invalid page positions");
|
||||||
break;
|
return failCommit();
|
||||||
}
|
}
|
||||||
serialization::writePod(file, entry.fileOffset);
|
serialization::writePod(file, entry.fileOffset);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (hasFailedLutRecords) {
|
// Write anchor-to-page map for fragment navigation (e.g. footnote targets). For a
|
||||||
LOG_ERR("SCT", "Failed to write LUT due to invalid page positions");
|
// partial, skip anchors that landed on the incomplete trailing page the suspend drops.
|
||||||
// Explicitly close() file before calling Storage.remove()
|
|
||||||
file.close();
|
|
||||||
Storage.remove(filePath.c_str());
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Write anchor-to-page map for fragment navigation (e.g. footnote targets)
|
|
||||||
const uint32_t anchorMapOffset = file.position();
|
const uint32_t anchorMapOffset = file.position();
|
||||||
const auto& anchors = visitor.getAnchors();
|
const auto& anchors = build_->parser->getAnchors();
|
||||||
serialization::writePod(file, static_cast<uint16_t>(anchors.size()));
|
uint16_t anchorCount = 0;
|
||||||
for (const auto& [anchor, page] : anchors) {
|
for (const auto& [anchor, page] : anchors) {
|
||||||
|
if (!asPartial || page < builtPageCount_) anchorCount++;
|
||||||
|
}
|
||||||
|
serialization::writePod(file, anchorCount);
|
||||||
|
for (const auto& [anchor, page] : anchors) {
|
||||||
|
if (asPartial && page >= builtPageCount_) continue;
|
||||||
serialization::writeString(file, anchor);
|
serialization::writeString(file, anchor);
|
||||||
serialization::writePod(file, page);
|
serialization::writePod(file, page);
|
||||||
}
|
}
|
||||||
|
|
||||||
const uint32_t paragraphLutOffset = file.position();
|
const uint32_t paragraphLutOffset = file.position();
|
||||||
serialization::writePod(file, static_cast<uint16_t>(lut.size()));
|
serialization::writePod(file, static_cast<uint16_t>(build_->lut.size()));
|
||||||
for (const auto& entry : lut) {
|
for (const auto& entry : build_->lut) {
|
||||||
serialization::writePod(file, entry.paragraphIndex);
|
serialization::writePod(file, entry.paragraphIndex);
|
||||||
}
|
}
|
||||||
|
|
||||||
const uint32_t liLutFileOffset = static_cast<uint32_t>(file.position());
|
const uint32_t liLutFileOffset = static_cast<uint32_t>(file.position());
|
||||||
for (const auto& entry : lut) {
|
for (const auto& entry : build_->lut) {
|
||||||
serialization::writePod(file, entry.listItemIndex);
|
serialization::writePod(file, entry.listItemIndex);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Patch header with final pageCount, lutOffset, anchorMapOffset, paragraphLutOffset, and liLutOffset
|
if (asPartial) {
|
||||||
file.seek(HEADER_SIZE - sizeof(uint32_t) * 4 - sizeof(pageCount));
|
// Watermark trailer, located on load as liLutOffset + pageCount * sizeof(uint16_t).
|
||||||
serialization::writePod(file, pageCount);
|
serialization::writePod(file, bytesConsumed);
|
||||||
|
serialization::writePod(file, totalBytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Patch header with the built page count and section offsets...
|
||||||
|
file.seek(HEADER_SIZE - sizeof(uint32_t) * 4 - sizeof(builtPageCount_));
|
||||||
|
serialization::writePod(file, builtPageCount_);
|
||||||
serialization::writePod(file, lutOffset);
|
serialization::writePod(file, lutOffset);
|
||||||
serialization::writePod(file, anchorMapOffset);
|
serialization::writePod(file, anchorMapOffset);
|
||||||
serialization::writePod(file, paragraphLutOffset);
|
serialization::writePod(file, paragraphLutOffset);
|
||||||
serialization::writePod(file, liLutFileOffset);
|
serialization::writePod(file, liLutFileOffset);
|
||||||
|
// ...then commit by overwriting the sentinel version with the real one. Writing the
|
||||||
|
// version last makes it the commit point: a crash before here leaves version 0.
|
||||||
|
file.seek(0);
|
||||||
|
serialization::writePod(file, version);
|
||||||
// Explicit close() required: member variable persists beyond function scope
|
// Explicit close() required: member variable persists beyond function scope
|
||||||
file.close();
|
file.close();
|
||||||
if (cssParser) {
|
|
||||||
cssParser->clear();
|
// Swap into place. A crash between remove and rename loses the old file but keeps a
|
||||||
|
// fully-committed tmp; the next build just removes it and rebuilds.
|
||||||
|
if (Storage.exists(filePath.c_str())) {
|
||||||
|
Storage.remove(filePath.c_str());
|
||||||
|
}
|
||||||
|
if (!Storage.rename(binTmpPath().c_str(), filePath.c_str())) {
|
||||||
|
LOG_ERR("SCT", "Failed to move built section into place");
|
||||||
|
Storage.remove(binTmpPath().c_str());
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::unique_ptr<Page> Section::loadPageFromSectionFile() {
|
bool Section::finalizeBuild() {
|
||||||
if (!Storage.openFileForRead("SCT", filePath, file)) {
|
// Flush the trailing page (emits the last page via the completePageFn into the LUT).
|
||||||
|
build_->parser->finishParse();
|
||||||
|
|
||||||
|
if (!build_->reusedHtml) {
|
||||||
|
// Parse succeeded: promote the freshly unzipped HTML to the persistent cache so future
|
||||||
|
// rebuilds skip zip inflation. If promotion fails, drop the temp -- the build still succeeded.
|
||||||
|
if (!Storage.rename(build_->tmpHtmlPath.c_str(), build_->htmlPath.c_str())) {
|
||||||
|
LOG_DBG("SCT", "Failed to promote HTML cache, removing temp");
|
||||||
|
Storage.remove(build_->tmpHtmlPath.c_str());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const bool committed = commitBuildFile(SECTION_FILE_VERSION, 0, 0);
|
||||||
|
if (build_->cssParser) build_->cssParser->clear();
|
||||||
|
build_.reset();
|
||||||
|
if (!committed) {
|
||||||
|
// commitBuildFile removed filePath before the failed swap, so nothing valid remains.
|
||||||
|
partial_ = false;
|
||||||
|
partialPageCount_ = 0;
|
||||||
|
pageCount = 0;
|
||||||
|
builtPageCount_ = 0;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
buildComplete_ = true;
|
||||||
|
partial_ = false;
|
||||||
|
partialPageCount_ = 0;
|
||||||
|
pageCount = builtPageCount_;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Section::suspendBuild() {
|
||||||
|
if (!build_) return;
|
||||||
|
|
||||||
|
// Only worth persisting if this build produced pages a pre-existing partial doesn't
|
||||||
|
// already cover; otherwise keep the older (bigger) partial and just drop the tmp.
|
||||||
|
const bool worthKeeping = builtPageCount_ > 0 && (!partial_ || builtPageCount_ > partialPageCount_);
|
||||||
|
|
||||||
|
bool committed = false;
|
||||||
|
if (worthKeeping) {
|
||||||
|
// Capture the parse watermark and commit before tearing the parser down (the anchor
|
||||||
|
// map is read from it). The incomplete trailing page is intentionally not flushed:
|
||||||
|
// only fully laid-out pages are persisted, and the rebuild re-derives the rest.
|
||||||
|
const uint32_t consumed = static_cast<uint32_t>(build_->parser->parseBytesConsumed());
|
||||||
|
committed = commitBuildFile(SECTION_FILE_PARTIAL_VERSION, consumed, build_->totalBytes);
|
||||||
|
if (committed) {
|
||||||
|
partial_ = true;
|
||||||
|
partialPageCount_ = builtPageCount_;
|
||||||
|
partialBytesConsumed_ = consumed;
|
||||||
|
partialTotalBytes_ = build_->totalBytes;
|
||||||
|
LOG_INF("SCT", "Suspended build: %u pages persisted", builtPageCount_);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (build_->parser) build_->parser->abortParse();
|
||||||
|
if (build_->cssParser) build_->cssParser->clear();
|
||||||
|
if (!committed && file) {
|
||||||
|
// Explicit close() required before remove (member variable, O_RDWR handle).
|
||||||
|
file.close();
|
||||||
|
Storage.remove(binTmpPath().c_str());
|
||||||
|
}
|
||||||
|
if (!build_->reusedHtml && Storage.exists(build_->tmpHtmlPath.c_str())) {
|
||||||
|
Storage.remove(build_->tmpHtmlPath.c_str());
|
||||||
|
}
|
||||||
|
build_.reset();
|
||||||
|
buildComplete_ = false;
|
||||||
|
pageCount = partial_ ? partialPageCount_ : 0;
|
||||||
|
builtPageCount_ = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Section::abandonBuild() {
|
||||||
|
if (!build_) return;
|
||||||
|
if (build_->parser) build_->parser->abortParse();
|
||||||
|
if (build_->cssParser) build_->cssParser->clear();
|
||||||
|
if (file) {
|
||||||
|
// Explicit close() required before remove (member variable, O_RDWR handle).
|
||||||
|
file.close();
|
||||||
|
Storage.remove(binTmpPath().c_str());
|
||||||
|
}
|
||||||
|
// A parse error would recur against the same HTML, so drop any partial too -- resuming
|
||||||
|
// from it would just re-enter the failing build every open.
|
||||||
|
if (Storage.exists(filePath.c_str())) {
|
||||||
|
Storage.remove(filePath.c_str());
|
||||||
|
}
|
||||||
|
if (!build_->reusedHtml && Storage.exists(build_->tmpHtmlPath.c_str())) {
|
||||||
|
Storage.remove(build_->tmpHtmlPath.c_str());
|
||||||
|
}
|
||||||
|
build_.reset();
|
||||||
|
buildComplete_ = false;
|
||||||
|
partial_ = false;
|
||||||
|
partialPageCount_ = 0;
|
||||||
|
pageCount = 0;
|
||||||
|
builtPageCount_ = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::unique_ptr<Page> Section::loadPageDuringBuild(const int page) {
|
||||||
|
if (!build_ || page < 0 || page >= static_cast<int>(build_->lut.size()) || !file) {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
const uint32_t pos = build_->lut[page].fileOffset;
|
||||||
|
if (pos == 0) {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
// The .bin is open O_RDWR for the build. Read the already-written page, then restore
|
||||||
|
// the write cursor so the next onPageComplete keeps appending where it left off.
|
||||||
|
const uint32_t writePos = file.position();
|
||||||
|
file.seek(pos);
|
||||||
|
auto p = Page::deserialize(file);
|
||||||
|
file.seek(writePos);
|
||||||
|
return p;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read a page from the committed file at filePath (finalized section or partial from a
|
||||||
|
// previous session). Uses a local handle so it is safe while a build holds the member
|
||||||
|
// `file` open on the tmp .bin.
|
||||||
|
std::unique_ptr<Page> Section::loadPageAt(const int page) const {
|
||||||
|
HalFile f;
|
||||||
|
if (!Storage.openFileForRead("SCT", filePath, f)) {
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
file.seek(HEADER_SIZE - sizeof(uint32_t) * 4);
|
f.seek(HEADER_SIZE - sizeof(uint32_t) * 4);
|
||||||
uint32_t lutOffset;
|
uint32_t lutOffset;
|
||||||
serialization::readPod(file, lutOffset);
|
serialization::readPod(f, lutOffset);
|
||||||
file.seek(lutOffset + sizeof(uint32_t) * currentPage);
|
f.seek(lutOffset + sizeof(uint32_t) * page);
|
||||||
uint32_t pagePos;
|
uint32_t pagePos;
|
||||||
serialization::readPod(file, pagePos);
|
serialization::readPod(f, pagePos);
|
||||||
file.seek(pagePos);
|
f.seek(pagePos);
|
||||||
|
|
||||||
auto page = Page::deserialize(file);
|
return Page::deserialize(f);
|
||||||
// Explicit close() required: member variable persists beyond function scope
|
// No f.close() needed -- DESTRUCTOR_CLOSES_FILE=1 handles it at scope exit
|
||||||
file.close();
|
}
|
||||||
return page;
|
|
||||||
|
std::unique_ptr<Page> Section::loadPage(const int page) {
|
||||||
|
if (page < 0) {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
if (build_ && page < static_cast<int>(build_->lut.size())) {
|
||||||
|
return loadPageDuringBuild(page);
|
||||||
|
}
|
||||||
|
// Not (yet) in the active build: serve from the file on disk -- a finalized section,
|
||||||
|
// or a partial from a previous session whose pages the rebuild hasn't reached again.
|
||||||
|
const int onDisk = partial_ ? partialPageCount_ : (build_ ? 0 : pageCount);
|
||||||
|
if (page >= onDisk) {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
return loadPageAt(page);
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string Section::getTextFromSectionFile() {
|
std::string Section::getTextFromSectionFile() {
|
||||||
std::string fullText;
|
std::string fullText;
|
||||||
auto p = this->loadPageFromSectionFile();
|
auto p = loadPage(currentPage);
|
||||||
if (p) {
|
if (p) {
|
||||||
for (const auto& el : p->elements) {
|
for (const auto& el : p->elements) {
|
||||||
if (el->getTag() == TAG_PageLine) {
|
if (el->getTag() == TAG_PageLine) {
|
||||||
const auto& line = static_cast<const PageLine&>(*el);
|
const auto& line = static_cast<const PageLine&>(*el);
|
||||||
if (line.getBlock()) {
|
if (line.getBlock()) {
|
||||||
const auto& words = line.getBlock()->getWords();
|
const auto& block = *line.getBlock();
|
||||||
for (const auto& w : words) {
|
for (uint16_t i = 0; i < block.wordCount(); i++) {
|
||||||
if (!fullText.empty()) fullText += " ";
|
if (!fullText.empty()) fullText += " ";
|
||||||
fullText += w;
|
fullText += block.wordText(i);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -350,7 +760,7 @@ std::string Section::getTextFromSectionFile() {
|
|||||||
return fullText;
|
return fullText;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::optional<uint16_t> Section::getCachedPageCount() const {
|
std::optional<uint16_t> Section::getCachedPageCount(const RenderParams* mustMatch) const {
|
||||||
HalFile f;
|
HalFile f;
|
||||||
if (!Storage.openFileForRead("SCT", filePath, f)) {
|
if (!Storage.openFileForRead("SCT", filePath, f)) {
|
||||||
return std::nullopt;
|
return std::nullopt;
|
||||||
@@ -361,6 +771,21 @@ std::optional<uint16_t> Section::getCachedPageCount() const {
|
|||||||
return std::nullopt;
|
return std::nullopt;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Only a finalized section's count is the chapter total; a partial's count is just the
|
||||||
|
// suspended build's watermark, which would skew progress mapping. Callers fall back to
|
||||||
|
// their own estimates.
|
||||||
|
uint8_t version;
|
||||||
|
serialization::readPod(f, version);
|
||||||
|
if (version != SECTION_FILE_VERSION) {
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
// A file cached under other render settings paginates differently; its count is
|
||||||
|
// only stale-valid for rough mapping, so reject it when the caller needs a match.
|
||||||
|
if (mustMatch && !(readHeaderRenderParams(f) == *mustMatch)) {
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
f.seek(HEADER_SIZE - sizeof(uint32_t) * 4 - sizeof(uint16_t));
|
f.seek(HEADER_SIZE - sizeof(uint32_t) * 4 - sizeof(uint16_t));
|
||||||
uint16_t count;
|
uint16_t count;
|
||||||
serialization::readPod(f, count);
|
serialization::readPod(f, count);
|
||||||
|
|||||||
+126
-8
@@ -3,11 +3,14 @@
|
|||||||
#include <memory>
|
#include <memory>
|
||||||
#include <optional>
|
#include <optional>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
#include "Epub.h"
|
#include "Epub.h"
|
||||||
|
|
||||||
class Page;
|
class Page;
|
||||||
class GfxRenderer;
|
class GfxRenderer;
|
||||||
|
class ChapterHtmlSlimParser;
|
||||||
|
class CssParser;
|
||||||
|
|
||||||
class Section {
|
class Section {
|
||||||
std::shared_ptr<Epub> epub;
|
std::shared_ptr<Epub> epub;
|
||||||
@@ -21,16 +24,84 @@ class Section {
|
|||||||
bool embeddedStyle, uint8_t imageRendering, bool focusReadingEnabled);
|
bool embeddedStyle, uint8_t imageRendering, bool focusReadingEnabled);
|
||||||
uint32_t onPageComplete(std::unique_ptr<Page> page);
|
uint32_t onPageComplete(std::unique_ptr<Page> page);
|
||||||
|
|
||||||
|
// Page-offset table entry, kept in RAM while an incremental build is running so
|
||||||
|
// already-built pages can be located in the partially-written .bin.
|
||||||
|
struct PageLutEntry {
|
||||||
|
uint32_t fileOffset;
|
||||||
|
uint16_t paragraphIndex;
|
||||||
|
uint16_t listItemIndex;
|
||||||
|
};
|
||||||
|
// Held only while an incremental build is in progress (see startBuild). Carries the
|
||||||
|
// live parser plus the strings it references (the parser stores them by reference)
|
||||||
|
// and the in-RAM page-offset table.
|
||||||
|
struct BuildContext {
|
||||||
|
std::unique_ptr<ChapterHtmlSlimParser> parser;
|
||||||
|
std::vector<PageLutEntry> lut;
|
||||||
|
std::string parsePath;
|
||||||
|
std::string contentBase;
|
||||||
|
std::string imageBasePath;
|
||||||
|
std::string htmlPath;
|
||||||
|
std::string tmpHtmlPath;
|
||||||
|
bool reusedHtml = false;
|
||||||
|
CssParser* cssParser = nullptr;
|
||||||
|
// HTML byte progress, for estimating the section's total page count while it's still building.
|
||||||
|
uint32_t bytesConsumed = 0;
|
||||||
|
uint32_t totalBytes = 0;
|
||||||
|
// Exponentially-smoothed page-count estimate (0 = not yet seeded) and the bytesConsumed at its
|
||||||
|
// last update. The raw byte-ratio estimate jitters as the build crosses dense/sparse regions;
|
||||||
|
// the EMA is stepped once per build advance (not per redraw) to damp that wobble.
|
||||||
|
float smoothedEstimate = 0;
|
||||||
|
uint32_t smoothedAtConsumed = 0;
|
||||||
|
};
|
||||||
|
std::unique_ptr<BuildContext> build_;
|
||||||
|
bool buildComplete_ = false;
|
||||||
|
// Pages laid out by the active build (== build_->lut.size()). Distinct from pageCount,
|
||||||
|
// which is the pages *available to read* and also counts a loaded partial file's pages.
|
||||||
|
uint16_t builtPageCount_ = 0;
|
||||||
|
// A partial section file (suspended build from a previous session) is loaded at filePath.
|
||||||
|
// Its pages 0..partialPageCount_-1 are readable while a rebuild extends past them.
|
||||||
|
bool partial_ = false;
|
||||||
|
uint16_t partialPageCount_ = 0;
|
||||||
|
// Parse watermark from the partial's trailer, for estimating the total page count.
|
||||||
|
uint32_t partialBytesConsumed_ = 0;
|
||||||
|
uint32_t partialTotalBytes_ = 0;
|
||||||
|
bool finalizeBuild();
|
||||||
|
// Write the LUTs/anchor map (and, for a partial, the watermark trailer), patch the
|
||||||
|
// header, stamp the version byte, and swap the tmp .bin over filePath.
|
||||||
|
bool commitBuildFile(uint8_t version, uint32_t bytesConsumed, uint32_t totalBytes);
|
||||||
|
// Builds write here and are swapped over filePath only on commit, so a prior
|
||||||
|
// partial/finalized file stays readable while a rebuild is in progress.
|
||||||
|
std::string binTmpPath() const { return filePath + ".part"; }
|
||||||
|
std::unique_ptr<Page> loadPageAt(int page) const;
|
||||||
|
// Read a page already laid out by the in-progress build (page < build LUT size), from
|
||||||
|
// the partially-written tmp .bin without disturbing the build's write cursor.
|
||||||
|
std::unique_ptr<Page> loadPageDuringBuild(int page);
|
||||||
|
|
||||||
public:
|
public:
|
||||||
|
// The render parameters that determine pagination, in section cache header order
|
||||||
|
// (see writeSectionFileHeader). A cached page count is only valid for one set.
|
||||||
|
struct RenderParams {
|
||||||
|
int fontId = 0;
|
||||||
|
float lineCompression = 0.0f;
|
||||||
|
bool extraParagraphSpacing = false;
|
||||||
|
uint8_t paragraphAlignment = 0;
|
||||||
|
uint16_t viewportWidth = 0;
|
||||||
|
uint16_t viewportHeight = 0;
|
||||||
|
bool hyphenationEnabled = false;
|
||||||
|
bool embeddedStyle = false;
|
||||||
|
uint8_t imageRendering = 0;
|
||||||
|
bool focusReadingEnabled = false;
|
||||||
|
|
||||||
|
bool operator==(const RenderParams& o) const;
|
||||||
|
};
|
||||||
|
|
||||||
uint16_t pageCount = 0;
|
uint16_t pageCount = 0;
|
||||||
int currentPage = 0;
|
int currentPage = 0;
|
||||||
|
|
||||||
explicit Section(const std::shared_ptr<Epub>& epub, const int spineIndex, GfxRenderer& renderer)
|
// Constructor and destructor are out-of-line: BuildContext holds a unique_ptr to the
|
||||||
: epub(epub),
|
// forward-declared ChapterHtmlSlimParser, whose full definition is only visible in the .cpp.
|
||||||
spineIndex(spineIndex),
|
explicit Section(const std::shared_ptr<Epub>& epub, int spineIndex, GfxRenderer& renderer);
|
||||||
renderer(renderer),
|
~Section();
|
||||||
filePath(epub->getCachePath() + "/sections/" + std::to_string(spineIndex) + ".bin") {}
|
|
||||||
~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 focusReadingEnabled);
|
uint8_t imageRendering, bool focusReadingEnabled);
|
||||||
@@ -39,14 +110,61 @@ class Section {
|
|||||||
uint16_t viewportWidth, uint16_t viewportHeight, bool hyphenationEnabled, bool embeddedStyle,
|
uint16_t viewportWidth, uint16_t viewportHeight, bool hyphenationEnabled, bool embeddedStyle,
|
||||||
uint8_t imageRendering, bool focusReadingEnabled,
|
uint8_t imageRendering, bool focusReadingEnabled,
|
||||||
const std::function<void()>& popupFn = nullptr);
|
const std::function<void()>& popupFn = nullptr);
|
||||||
std::unique_ptr<Page> loadPageFromSectionFile();
|
|
||||||
|
// Incremental build: lay out the section a few pages at a time so a large chapter
|
||||||
|
// can show its first page immediately and keep the UI responsive while the rest
|
||||||
|
// builds. createSectionFile() above is the one-shot wrapper over these.
|
||||||
|
// if (!startBuild(...)) fail;
|
||||||
|
// each tick: buildSomeMore(N); render up to pageCount; when isBuildComplete() stop.
|
||||||
|
bool startBuild(int fontId, float lineCompression, bool extraParagraphSpacing, uint8_t paragraphAlignment,
|
||||||
|
uint16_t viewportWidth, uint16_t viewportHeight, bool hyphenationEnabled, bool embeddedStyle,
|
||||||
|
uint8_t imageRendering, bool focusReadingEnabled, const std::function<void()>& popupFn = nullptr);
|
||||||
|
// Lay out up to maxPages more pages (maxPages <= 0 = build to completion). Returns
|
||||||
|
// false on error (the build is abandoned). Sets isBuildComplete() when finished.
|
||||||
|
bool buildSomeMore(int maxPages);
|
||||||
|
bool isBuilding() const { return static_cast<bool>(build_); }
|
||||||
|
bool isBuildComplete() const { return buildComplete_; }
|
||||||
|
// Best-known total page count: the exact pageCount once finalized, or a smoothed byte-based
|
||||||
|
// estimate (pages so far scaled by totalBytes/bytesConsumed, damped by an EMA) while a giant spine
|
||||||
|
// is still building, so "page X of Y" / progress don't read off the small build watermark.
|
||||||
|
uint16_t estimatedTotalPages() const;
|
||||||
|
void abandonBuild();
|
||||||
|
// Persist an in-progress build as a partial section file (version sentinel + LUTs +
|
||||||
|
// watermark trailer) instead of discarding it, so the next open of this spine can show
|
||||||
|
// its pages instantly and only rebuild in the background. Called by the destructor, so
|
||||||
|
// any teardown path (exit, sleep, navigation) keeps the work already done. Keeps a
|
||||||
|
// pre-existing partial when it covers more pages than this build reached.
|
||||||
|
void suspendBuild();
|
||||||
|
// True when a partial file was loaded: pageCount is a watermark, not the chapter total.
|
||||||
|
bool isPartial() const { return partial_; }
|
||||||
|
|
||||||
|
// Unified page read: from the active build if it has reached the page, otherwise from
|
||||||
|
// the on-disk file (finalized section, or a partial the rebuild hasn't caught up to).
|
||||||
|
std::unique_ptr<Page> loadPage(int page);
|
||||||
|
|
||||||
std::string getTextFromSectionFile();
|
std::string getTextFromSectionFile();
|
||||||
|
|
||||||
|
// Resolve an anchor from the in-progress build first, then the on-disk anchor map
|
||||||
|
// (covers finalized sections and partials from a previous session).
|
||||||
|
std::optional<uint16_t> findAnchor(const std::string& anchor) const;
|
||||||
|
|
||||||
|
// True if this spine's unzipped HTML is already cached, so a build won't pay the (multi-second on a
|
||||||
|
// giant spine) zip inflation. Lets the reader skip the indexing popup on a fast reopen/rebuild.
|
||||||
|
bool hasHtmlCache() const;
|
||||||
|
|
||||||
// Look up the page number for an anchor id from the section cache file.
|
// Look up the page number for an anchor id from the section cache file.
|
||||||
std::optional<uint16_t> getPageForAnchor(const std::string& anchor) const;
|
std::optional<uint16_t> getPageForAnchor(const std::string& anchor) const;
|
||||||
|
|
||||||
|
// Look up an anchor among the pages built so far by the in-progress build, so an anchor jump
|
||||||
|
// (TOC / chapter select, usually the chapter top = page 0) can resolve without laying out the
|
||||||
|
// whole chapter. Returns nullopt if the anchor hasn't been reached yet (build more) or no build.
|
||||||
|
std::optional<uint16_t> findAnchorDuringBuild(const std::string& anchor) const;
|
||||||
|
|
||||||
// Get the page count from the section cache file without fully loading it.
|
// Get the page count from the section cache file without fully loading it.
|
||||||
std::optional<uint16_t> getCachedPageCount() const;
|
// Finalized files only (a partial's count is just a build watermark). When
|
||||||
|
// `mustMatch` is given, the header's render parameters must equal it, so a
|
||||||
|
// count cached under different settings is never trusted.
|
||||||
|
std::optional<uint16_t> getCachedPageCount(const RenderParams* mustMatch = nullptr) const;
|
||||||
|
|
||||||
// Look up the page number for a synthetic paragraph index from XPath p[N].
|
// Look up the page number for a synthetic paragraph index from XPath p[N].
|
||||||
std::optional<uint16_t> getPageForParagraphIndex(uint16_t pIndex) const;
|
std::optional<uint16_t> getPageForParagraphIndex(uint16_t pIndex) const;
|
||||||
|
|||||||
@@ -32,6 +32,11 @@ struct BlockStyle {
|
|||||||
bool isRtl = false; // true if resolved direction is RTL
|
bool isRtl = false; // true if resolved direction is RTL
|
||||||
bool directionDefined = false; // true if direction was explicitly set in CSS/HTML
|
bool directionDefined = false; // true if direction was explicitly set in CSS/HTML
|
||||||
|
|
||||||
|
// Set when this block was created by a <br> element. Used by startNewTextBlock to inject
|
||||||
|
// a full line-height gap when the <br> block stays empty (section-break use case).
|
||||||
|
// NOT propagated through getCombinedBlockStyle so it can't leak into sibling blocks.
|
||||||
|
bool fromBrElement = false;
|
||||||
|
|
||||||
// Combined insets (margin + padding)
|
// Combined insets (margin + padding)
|
||||||
[[nodiscard]] int16_t leftInset() const { return marginLeft + paddingLeft; }
|
[[nodiscard]] int16_t leftInset() const { return marginLeft + paddingLeft; }
|
||||||
[[nodiscard]] int16_t rightInset() const { return marginRight + paddingRight; }
|
[[nodiscard]] int16_t rightInset() const { return marginRight + paddingRight; }
|
||||||
@@ -92,6 +97,9 @@ struct BlockStyle {
|
|||||||
result.directionDefined = true;
|
result.directionDefined = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// fromBrElement is consumed by startNewTextBlock when an empty <br> block
|
||||||
|
// is merged with the following paragraph; never propagate it further.
|
||||||
|
result.fromBrElement = false;
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,8 @@
|
|||||||
#include <Logging.h>
|
#include <Logging.h>
|
||||||
#include <Serialization.h>
|
#include <Serialization.h>
|
||||||
|
|
||||||
|
#include <cstdlib>
|
||||||
|
|
||||||
#include "Epub/converters/DirectPixelWriter.h"
|
#include "Epub/converters/DirectPixelWriter.h"
|
||||||
#include "Epub/converters/ImageDecoderFactory.h"
|
#include "Epub/converters/ImageDecoderFactory.h"
|
||||||
|
|
||||||
@@ -29,6 +31,54 @@ std::string getCachePath(const std::string& imagePath) {
|
|||||||
return imagePath + ".pxc";
|
return imagePath + ".pxc";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool readValidCacheHeader(HalFile& cacheFile, const int expectedWidth, const int expectedHeight, uint16_t& cachedWidth,
|
||||||
|
uint16_t& cachedHeight) {
|
||||||
|
if (cacheFile.read(&cachedWidth, 2) != 2 || cacheFile.read(&cachedHeight, 2) != 2) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const int widthDiff = abs(cachedWidth - expectedWidth);
|
||||||
|
const int heightDiff = abs(cachedHeight - expectedHeight);
|
||||||
|
if (widthDiff > 1 || heightDiff > 1) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const size_t bytesPerRow = (cachedWidth + 3) / 4;
|
||||||
|
const size_t expectedSize = 4 + bytesPerRow * cachedHeight;
|
||||||
|
return cacheFile.size() >= expectedSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pages are deserialized afresh on each visit. Keep a bounded, allocation-free
|
||||||
|
// record so an image that failed renders its placeholder directly for the rest
|
||||||
|
// of the reader session instead of paying another placeholder refresh and
|
||||||
|
// decode. The reader clears this on entry so transient memory/storage failures
|
||||||
|
// are retried.
|
||||||
|
constexpr size_t MAX_SESSION_IMAGE_FAILURES = 16;
|
||||||
|
uint64_t failedImageHashes[MAX_SESSION_IMAGE_FAILURES];
|
||||||
|
size_t failedImageCount = 0;
|
||||||
|
|
||||||
|
uint64_t imagePathHash(const std::string& path) {
|
||||||
|
uint64_t hash = 14695981039346656037ull;
|
||||||
|
for (const char c : path) {
|
||||||
|
hash ^= static_cast<uint8_t>(c);
|
||||||
|
hash *= 1099511628211ull;
|
||||||
|
}
|
||||||
|
return hash;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool imageFailedThisSession(const std::string& path) {
|
||||||
|
const uint64_t hash = imagePathHash(path);
|
||||||
|
for (size_t i = 0; i < failedImageCount; i++) {
|
||||||
|
if (failedImageHashes[i] == hash) return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void rememberImageFailure(const std::string& path) {
|
||||||
|
if (failedImageCount == MAX_SESSION_IMAGE_FAILURES || imageFailedThisSession(path)) return;
|
||||||
|
failedImageHashes[failedImageCount++] = imagePathHash(path);
|
||||||
|
}
|
||||||
|
|
||||||
bool renderFromCache(GfxRenderer& renderer, const std::string& cachePath, int x, int y, int expectedWidth,
|
bool renderFromCache(GfxRenderer& renderer, const std::string& cachePath, int x, int y, int expectedWidth,
|
||||||
int expectedHeight) {
|
int expectedHeight) {
|
||||||
HalFile cacheFile;
|
HalFile cacheFile;
|
||||||
@@ -37,16 +87,8 @@ bool renderFromCache(GfxRenderer& renderer, const std::string& cachePath, int x,
|
|||||||
}
|
}
|
||||||
|
|
||||||
uint16_t cachedWidth, cachedHeight;
|
uint16_t cachedWidth, cachedHeight;
|
||||||
if (cacheFile.read(&cachedWidth, 2) != 2 || cacheFile.read(&cachedHeight, 2) != 2) {
|
if (!readValidCacheHeader(cacheFile, expectedWidth, expectedHeight, cachedWidth, cachedHeight)) {
|
||||||
return false;
|
LOG_ERR("IMG", "Invalid image cache: %s", cachePath.c_str());
|
||||||
}
|
|
||||||
|
|
||||||
// Verify dimensions are close (allow 1 pixel tolerance for rounding differences)
|
|
||||||
int widthDiff = abs(cachedWidth - expectedWidth);
|
|
||||||
int heightDiff = abs(cachedHeight - expectedHeight);
|
|
||||||
if (widthDiff > 1 || heightDiff > 1) {
|
|
||||||
LOG_ERR("IMG", "Cache dimension mismatch: %dx%d vs %dx%d", cachedWidth, cachedHeight, expectedWidth,
|
|
||||||
expectedHeight);
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -119,6 +161,28 @@ bool renderFromCache(GfxRenderer& renderer, const std::string& cachePath, int x,
|
|||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
|
bool ImageBlock::hasValidCache() const {
|
||||||
|
const auto cachePath = getCachePath(imagePath);
|
||||||
|
HalFile cacheFile;
|
||||||
|
if (!Storage.openFileForRead("IMG", cachePath, cacheFile)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint16_t cachedWidth, cachedHeight;
|
||||||
|
return readValidCacheHeader(cacheFile, width, height, cachedWidth, cachedHeight);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ImageBlock::needsDecode() const { return !imageFailedThisSession(imagePath) && !hasValidCache(); }
|
||||||
|
|
||||||
|
void ImageBlock::clearSessionRenderFailures() { failedImageCount = 0; }
|
||||||
|
|
||||||
|
void ImageBlock::renderPlaceholder(GfxRenderer& renderer, const int x, const int y) const {
|
||||||
|
renderer.fillRect(x, y, width, height, true);
|
||||||
|
if (width > 2 && height > 2) {
|
||||||
|
renderer.fillRect(x + 1, y + 1, width - 2, height - 2, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void ImageBlock::render(GfxRenderer& renderer, const int x, const int y) {
|
void ImageBlock::render(GfxRenderer& renderer, const int x, const int y) {
|
||||||
// The font-prewarm scan pass only accumulates glyphs; an image contributes
|
// The font-prewarm scan pass only accumulates glyphs; an image contributes
|
||||||
// none, and its DirectPixelWriter output bypasses the renderer's scan-mode
|
// none, and its DirectPixelWriter output bypasses the renderer's scan-mode
|
||||||
@@ -150,6 +214,11 @@ void ImageBlock::render(GfxRenderer& renderer, const int x, const int y) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (imageFailedThisSession(imagePath)) {
|
||||||
|
renderPlaceholder(renderer, x, y);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Try to render from cache first
|
// Try to render from cache first
|
||||||
std::string cachePath = getCachePath(imagePath);
|
std::string cachePath = getCachePath(imagePath);
|
||||||
if (renderFromCache(renderer, cachePath, x, y, width, height)) {
|
if (renderFromCache(renderer, cachePath, x, y, width, height)) {
|
||||||
@@ -161,6 +230,8 @@ void ImageBlock::render(GfxRenderer& renderer, const int x, const int y) {
|
|||||||
HalFile file;
|
HalFile file;
|
||||||
if (!Storage.openFileForRead("IMG", imagePath, file)) {
|
if (!Storage.openFileForRead("IMG", imagePath, file)) {
|
||||||
LOG_ERR("IMG", "Image file not found: %s", imagePath.c_str());
|
LOG_ERR("IMG", "Image file not found: %s", imagePath.c_str());
|
||||||
|
rememberImageFailure(imagePath);
|
||||||
|
renderPlaceholder(renderer, x, y);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
size_t fileSize = file.size();
|
size_t fileSize = file.size();
|
||||||
@@ -168,6 +239,8 @@ void ImageBlock::render(GfxRenderer& renderer, const int x, const int y) {
|
|||||||
|
|
||||||
if (fileSize == 0) {
|
if (fileSize == 0) {
|
||||||
LOG_ERR("IMG", "Image file is empty: %s", imagePath.c_str());
|
LOG_ERR("IMG", "Image file is empty: %s", imagePath.c_str());
|
||||||
|
rememberImageFailure(imagePath);
|
||||||
|
renderPlaceholder(renderer, x, y);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -187,6 +260,8 @@ void ImageBlock::render(GfxRenderer& renderer, const int x, const int y) {
|
|||||||
ImageToFramebufferDecoder* decoder = ImageDecoderFactory::getDecoder(imagePath);
|
ImageToFramebufferDecoder* decoder = ImageDecoderFactory::getDecoder(imagePath);
|
||||||
if (!decoder) {
|
if (!decoder) {
|
||||||
LOG_ERR("IMG", "No decoder found for image: %s", imagePath.c_str());
|
LOG_ERR("IMG", "No decoder found for image: %s", imagePath.c_str());
|
||||||
|
rememberImageFailure(imagePath);
|
||||||
|
renderPlaceholder(renderer, x, y);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -195,6 +270,8 @@ void ImageBlock::render(GfxRenderer& renderer, const int x, const int y) {
|
|||||||
bool success = decoder->decodeToFramebuffer(imagePath, renderer, config);
|
bool success = decoder->decodeToFramebuffer(imagePath, renderer, config);
|
||||||
if (!success) {
|
if (!success) {
|
||||||
LOG_ERR("IMG", "Failed to decode image: %s", imagePath.c_str());
|
LOG_ERR("IMG", "Failed to decode image: %s", imagePath.c_str());
|
||||||
|
rememberImageFailure(imagePath);
|
||||||
|
renderPlaceholder(renderer, x, y);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -16,6 +16,10 @@ class ImageBlock final : public Block {
|
|||||||
int16_t getHeight() const { return height; }
|
int16_t getHeight() const { return height; }
|
||||||
|
|
||||||
bool imageExists() const;
|
bool imageExists() const;
|
||||||
|
bool hasValidCache() const;
|
||||||
|
bool needsDecode() const;
|
||||||
|
void renderPlaceholder(GfxRenderer& renderer, int x, int y) const;
|
||||||
|
static void clearSessionRenderFailures();
|
||||||
|
|
||||||
BlockType getType() override { return IMAGE_BLOCK; }
|
BlockType getType() override { return IMAGE_BLOCK; }
|
||||||
bool isEmpty() override { return false; }
|
bool isEmpty() override { return false; }
|
||||||
|
|||||||
@@ -3,30 +3,159 @@
|
|||||||
#include <BidiUtils.h>
|
#include <BidiUtils.h>
|
||||||
#include <GfxRenderer.h>
|
#include <GfxRenderer.h>
|
||||||
#include <Logging.h>
|
#include <Logging.h>
|
||||||
|
#include <Memory.h>
|
||||||
#include <Serialization.h>
|
#include <Serialization.h>
|
||||||
|
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
|
|
||||||
void TextBlock::render(const GfxRenderer& renderer, const int fontId, const int x, const int y) const {
|
size_t TextBlock::arenaSize(const uint16_t wordCount, const bool hasFocus, const uint16_t textBytes) {
|
||||||
|
// Layout documented in TextBlock.h: 16-bit arrays first, then 8-bit arrays, then text.
|
||||||
|
size_t size = static_cast<size_t>(wordCount) * (sizeof(uint16_t) + sizeof(int16_t) + sizeof(uint8_t));
|
||||||
|
if (hasFocus) {
|
||||||
|
size += static_cast<size_t>(wordCount) * (sizeof(uint16_t) + sizeof(uint8_t));
|
||||||
|
}
|
||||||
|
return size + textBytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
void TextBlock::bindArenaPointers() {
|
||||||
|
uint8_t* base = arena.get();
|
||||||
|
const size_t wc = numWords;
|
||||||
|
textOffArr = reinterpret_cast<const uint16_t*>(base);
|
||||||
|
xposArr = reinterpret_cast<const int16_t*>(base + wc * 2);
|
||||||
|
size_t off = wc * 4;
|
||||||
|
if (focusPresent) {
|
||||||
|
focusSuffixXArr = reinterpret_cast<const uint16_t*>(base + off);
|
||||||
|
off += wc * 2;
|
||||||
|
}
|
||||||
|
stylesArr = base + off;
|
||||||
|
off += wc;
|
||||||
|
if (focusPresent) {
|
||||||
|
focusBoundaryArr = base + off;
|
||||||
|
off += wc;
|
||||||
|
}
|
||||||
|
textArr = reinterpret_cast<const char*>(base + off);
|
||||||
|
}
|
||||||
|
|
||||||
|
TextBlock::TextBlock(const std::vector<std::string>& words, const std::vector<int16_t>& wordXpos,
|
||||||
|
const std::vector<EpdFontFamily::Style>& wordStyles, const std::vector<uint8_t>& focusBoundary,
|
||||||
|
const std::vector<uint16_t>& focusSuffixX, const BlockStyle& blockStyle)
|
||||||
|
: blockStyle(blockStyle) {
|
||||||
// Focus annotations are optional: empty vectors mean no word in this block has a split.
|
// Focus annotations are optional: empty vectors mean no word in this block has a split.
|
||||||
// When present, they must be sized in lockstep with words[].
|
// When present, they must be sized in lockstep with words[].
|
||||||
const bool hasFocus = !wordFocusBoundary.empty();
|
const bool hasFocus = !focusBoundary.empty();
|
||||||
if (words.size() != wordXpos.size() || words.size() != wordStyles.size() ||
|
if (words.size() != wordXpos.size() || words.size() != wordStyles.size() || words.size() > 10000 ||
|
||||||
(hasFocus && (words.size() != wordFocusBoundary.size() || words.size() != wordFocusSuffixX.size()))) {
|
(hasFocus && (words.size() != focusBoundary.size() || words.size() != focusSuffixX.size()))) {
|
||||||
LOG_ERR("TXB", "Render skipped: size mismatch (words=%u, xpos=%u, styles=%u, boundary=%u, suffixX=%u)\n",
|
LOG_ERR("TXB", "Construction failed: size mismatch (words=%u, xpos=%u, styles=%u, boundary=%u, suffixX=%u)",
|
||||||
(uint32_t)words.size(), (uint32_t)wordXpos.size(), (uint32_t)wordStyles.size(),
|
static_cast<uint32_t>(words.size()), static_cast<uint32_t>(wordXpos.size()),
|
||||||
(uint32_t)wordFocusBoundary.size(), (uint32_t)wordFocusSuffixX.size());
|
static_cast<uint32_t>(wordStyles.size()), static_cast<uint32_t>(focusBoundary.size()),
|
||||||
|
static_cast<uint32_t>(focusSuffixX.size()));
|
||||||
|
isValid = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
numWords = static_cast<uint16_t>(words.size());
|
||||||
|
focusPresent = hasFocus;
|
||||||
|
if (numWords == 0) {
|
||||||
|
return; // valid empty block, no arena
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pass 1: total text size, one NUL per word. A line is at most a physical
|
||||||
|
// row of the page, so uint16_t offsets are ample; reject anything larger.
|
||||||
|
size_t totalText = 0;
|
||||||
|
for (const auto& w : words) totalText += w.size() + 1;
|
||||||
|
if (totalText > UINT16_MAX) {
|
||||||
|
LOG_ERR("TXB", "Construction failed: text size %u exceeds arena limit", static_cast<uint32_t>(totalText));
|
||||||
|
numWords = 0;
|
||||||
|
focusPresent = false;
|
||||||
|
isValid = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
textBytes = static_cast<uint16_t>(totalText);
|
||||||
|
|
||||||
|
const size_t size = arenaSize(numWords, focusPresent, textBytes);
|
||||||
|
arena = makeUniqueNoThrow<uint8_t[]>(size);
|
||||||
|
if (!arena) {
|
||||||
|
LOG_ERR("TXB", "OOM: arena %u bytes", static_cast<uint32_t>(size));
|
||||||
|
numWords = 0;
|
||||||
|
textBytes = 0;
|
||||||
|
focusPresent = false;
|
||||||
|
isValid = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
bindArenaPointers();
|
||||||
|
|
||||||
|
// Pass 2: fill. Mutable aliases of the const views bound above.
|
||||||
|
auto* textOff = const_cast<uint16_t*>(textOffArr);
|
||||||
|
auto* xpos = const_cast<int16_t*>(xposArr);
|
||||||
|
auto* styles = const_cast<uint8_t*>(stylesArr);
|
||||||
|
auto* text = const_cast<char*>(textArr);
|
||||||
|
uint16_t off = 0;
|
||||||
|
for (uint16_t i = 0; i < numWords; i++) {
|
||||||
|
textOff[i] = off;
|
||||||
|
xpos[i] = wordXpos[i];
|
||||||
|
styles[i] = static_cast<uint8_t>(wordStyles[i]);
|
||||||
|
memcpy(text + off, words[i].data(), words[i].size());
|
||||||
|
off += static_cast<uint16_t>(words[i].size());
|
||||||
|
text[off++] = '\0';
|
||||||
|
}
|
||||||
|
if (focusPresent) {
|
||||||
|
auto* suffixX = const_cast<uint16_t*>(focusSuffixXArr);
|
||||||
|
auto* boundary = const_cast<uint8_t*>(focusBoundaryArr);
|
||||||
|
for (uint16_t i = 0; i < numWords; i++) {
|
||||||
|
suffixX[i] = focusSuffixX[i];
|
||||||
|
boundary[i] = focusBoundary[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void TextBlock::render(const GfxRenderer& renderer, const int fontId, const int x, const int y) const {
|
||||||
|
if (!isValid) {
|
||||||
|
LOG_ERR("TXB", "Render skipped: invalid block");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const bool scanning = renderer.isFontCacheScanning();
|
const bool scanning = renderer.isFontCacheScanning();
|
||||||
const int ascender = renderer.getFontAscenderSize(fontId);
|
const int ascender = renderer.getFontAscenderSize(fontId);
|
||||||
for (size_t i = 0; i < words.size(); i++) {
|
|
||||||
const int wordX = wordXpos[i] + x;
|
struct DecorationLineTracker {
|
||||||
const EpdFontFamily::Style currentStyle = wordStyles[i];
|
EpdFontFamily::Style style;
|
||||||
const auto baseDir = static_cast<BidiUtils::BidiBaseDir>(
|
int yOffset;
|
||||||
BidiUtils::detectParagraphLevel(words[i].c_str(), blockStyle.isRtl ? 1 : 0));
|
int startX = -1;
|
||||||
const uint8_t boundary = hasFocus ? wordFocusBoundary[i] : 0;
|
int endX = -1;
|
||||||
|
int yPos = 0;
|
||||||
|
|
||||||
|
bool active() const { return startX != -1; }
|
||||||
|
void reset() {
|
||||||
|
startX = -1;
|
||||||
|
endX = -1;
|
||||||
|
yPos = 0;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
DecorationLineTracker decorationLines[] = {
|
||||||
|
{EpdFontFamily::UNDERLINE, ascender + 2},
|
||||||
|
{EpdFontFamily::STRIKETHROUGH, ascender * 4 / 5},
|
||||||
|
};
|
||||||
|
|
||||||
|
const auto flushDecoration = [&](DecorationLineTracker& line) {
|
||||||
|
if (line.active()) {
|
||||||
|
renderer.drawLine(line.startX, line.yPos, line.endX, line.yPos, 2, true);
|
||||||
|
line.reset();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const auto flushDecorations = [&]() {
|
||||||
|
for (auto& line : decorationLines) {
|
||||||
|
flushDecoration(line);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
for (uint16_t i = 0; i < numWords; i++) {
|
||||||
|
const char* word = wordText(i);
|
||||||
|
const int wordX = xposArr[i] + x;
|
||||||
|
const EpdFontFamily::Style currentStyle = wordStyle(i);
|
||||||
|
const auto baseDir =
|
||||||
|
static_cast<BidiUtils::BidiBaseDir>(BidiUtils::detectParagraphLevel(word, blockStyle.isRtl ? 1 : 0));
|
||||||
|
const uint8_t boundary = focusBoundary(i);
|
||||||
|
|
||||||
// SUP/SUB shift the baseline passed to drawText; the glyph is also scaled 50% inside
|
// SUP/SUB shift the baseline passed to drawText; the glyph is also scaled 50% inside
|
||||||
// drawText, so these offsets are chosen relative to the full-size ascender:
|
// drawText, so these offsets are chosen relative to the full-size ascender:
|
||||||
@@ -49,54 +178,81 @@ void TextBlock::render(const GfxRenderer& renderer, const int fontId, const int
|
|||||||
static_assert(sizeof(boldBuf) >= MAX_FOCUS_PREFIX_BYTES,
|
static_assert(sizeof(boldBuf) >= MAX_FOCUS_PREFIX_BYTES,
|
||||||
"boldBuf too small for max focus prefix (9 codepoints * 4 UTF-8 bytes + null)");
|
"boldBuf too small for max focus prefix (9 codepoints * 4 UTF-8 bytes + null)");
|
||||||
const auto boldStyle = static_cast<EpdFontFamily::Style>(currentStyle | EpdFontFamily::BOLD);
|
const auto boldStyle = static_cast<EpdFontFamily::Style>(currentStyle | EpdFontFamily::BOLD);
|
||||||
const size_t boldLen = std::min<size_t>({static_cast<size_t>(boundary), words[i].size(), sizeof(boldBuf) - 1});
|
const size_t boldLen =
|
||||||
memcpy(boldBuf, words[i].c_str(), boldLen);
|
std::min<size_t>({static_cast<size_t>(boundary), static_cast<size_t>(wordTextLen(i)), sizeof(boldBuf) - 1});
|
||||||
|
memcpy(boldBuf, word, boldLen);
|
||||||
boldBuf[boldLen] = '\0';
|
boldBuf[boldLen] = '\0';
|
||||||
renderer.drawText(fontId, wordX, wordY, boldBuf, true, boldStyle, baseDir);
|
renderer.drawText(fontId, wordX, wordY, boldBuf, true, boldStyle, baseDir);
|
||||||
const int suffixX = wordX + wordFocusSuffixX[i];
|
const int suffixX = wordX + focusSuffixXArr[i];
|
||||||
renderer.drawText(fontId, suffixX, wordY, words[i].c_str() + boldLen, true, currentStyle, baseDir);
|
renderer.drawText(fontId, suffixX, wordY, word + boldLen, true, currentStyle, baseDir);
|
||||||
} else {
|
} else {
|
||||||
renderer.drawText(fontId, wordX, wordY, words[i].c_str(), true, currentStyle, baseDir);
|
renderer.drawText(fontId, wordX, wordY, word, true, currentStyle, baseDir);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!scanning && (currentStyle & EpdFontFamily::UNDERLINE) != 0) {
|
if (scanning) {
|
||||||
const std::string& w = words[i];
|
continue;
|
||||||
int underlineWidth = renderer.getTextWidth(fontId, w.c_str(), currentStyle, baseDir);
|
}
|
||||||
const int underlineY = wordY + ascender + 2;
|
|
||||||
|
if (EpdFontFamily::hasTextDecoration(currentStyle)) {
|
||||||
|
int lineStartX = wordX;
|
||||||
|
int lineWidth = renderer.getTextWidth(fontId, word, currentStyle, baseDir);
|
||||||
|
|
||||||
if ((currentStyle & (EpdFontFamily::SUP | EpdFontFamily::SUB)) != 0) {
|
if ((currentStyle & (EpdFontFamily::SUP | EpdFontFamily::SUB)) != 0) {
|
||||||
underlineWidth = (underlineWidth + 1) / 2;
|
lineWidth = (lineWidth + 1) / 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
renderer.drawLine(wordX, underlineY, wordX + underlineWidth, underlineY, true);
|
// Do not decorate the synthetic em-space used for paragraph indentation.
|
||||||
|
if (wordTextLen(i) >= 3 && static_cast<uint8_t>(word[0]) == 0xE2 && static_cast<uint8_t>(word[1]) == 0x80 &&
|
||||||
|
static_cast<uint8_t>(word[2]) == 0x83) {
|
||||||
|
const char* visibleText = word + 3;
|
||||||
|
lineStartX += renderer.getTextAdvanceX(fontId, "\xe2\x80\x83", currentStyle);
|
||||||
|
lineWidth = renderer.getTextWidth(fontId, visibleText, currentStyle, baseDir);
|
||||||
|
if ((currentStyle & (EpdFontFamily::SUP | EpdFontFamily::SUB)) != 0) {
|
||||||
|
lineWidth = (lineWidth + 1) / 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (auto& line : decorationLines) {
|
||||||
|
if ((currentStyle & line.style) == 0) {
|
||||||
|
flushDecoration(line);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const int lineY = wordY + line.yOffset;
|
||||||
|
if (line.active() && line.yPos != lineY) {
|
||||||
|
flushDecoration(line);
|
||||||
|
}
|
||||||
|
if (!line.active()) {
|
||||||
|
line.startX = lineStartX;
|
||||||
|
line.yPos = lineY;
|
||||||
|
}
|
||||||
|
line.endX = lineStartX + lineWidth;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
flushDecorations();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
flushDecorations();
|
||||||
}
|
}
|
||||||
|
|
||||||
bool TextBlock::serialize(HalFile& file) const {
|
bool TextBlock::serialize(HalFile& file) const {
|
||||||
// Focus annotations are optional; vectors are either empty (no splits in this block)
|
if (!isValid) {
|
||||||
// or sized in lockstep with words[].
|
LOG_ERR("TXB", "Serialization failed: invalid block");
|
||||||
const bool hasFocus = !wordFocusBoundary.empty();
|
|
||||||
if (words.size() != wordXpos.size() || words.size() != wordStyles.size() ||
|
|
||||||
(hasFocus && (words.size() != wordFocusBoundary.size() || words.size() != wordFocusSuffixX.size()))) {
|
|
||||||
LOG_ERR("TXB", "Serialization failed: size mismatch (words=%u, xpos=%u, styles=%u, boundary=%u, suffixX=%u)\n",
|
|
||||||
static_cast<uint32_t>(words.size()), static_cast<uint32_t>(wordXpos.size()),
|
|
||||||
static_cast<uint32_t>(wordStyles.size()), static_cast<uint32_t>(wordFocusBoundary.size()),
|
|
||||||
static_cast<uint32_t>(wordFocusSuffixX.size()));
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Word data
|
// Word data: scalars, then the arena verbatim -- its in-memory layout is
|
||||||
serialization::writePod(file, static_cast<uint16_t>(words.size()));
|
// exactly the on-disk layout (see TextBlock.h), so one write covers all
|
||||||
for (const auto& w : words) serialization::writeString(file, w);
|
// per-word arrays and the text blob.
|
||||||
for (auto x : wordXpos) serialization::writePod(file, x);
|
serialization::writePod(file, numWords);
|
||||||
for (auto s : wordStyles) serialization::writePod(file, s);
|
serialization::writePod(file, static_cast<uint8_t>(focusPresent ? 1 : 0));
|
||||||
// Focus block: 1-byte presence flag, followed by per-word vectors only when present.
|
serialization::writePod(file, textBytes);
|
||||||
// Saves 3 bytes/word when focus reading is disabled or no word on this line was split.
|
if (numWords > 0) {
|
||||||
serialization::writePod(file, static_cast<uint8_t>(hasFocus ? 1 : 0));
|
const size_t size = arenaSize(numWords, focusPresent, textBytes);
|
||||||
if (hasFocus) {
|
if (file.write(arena.get(), size) != size) {
|
||||||
for (auto b : wordFocusBoundary) serialization::writePod(file, b);
|
LOG_ERR("TXB", "Serialization failed: arena write (%u bytes)", static_cast<uint32_t>(size));
|
||||||
for (auto sx : wordFocusSuffixX) serialization::writePod(file, sx);
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Style (alignment + margins/padding/indent)
|
// Style (alignment + margins/padding/indent)
|
||||||
@@ -120,41 +276,64 @@ bool TextBlock::serialize(HalFile& file) const {
|
|||||||
|
|
||||||
std::unique_ptr<TextBlock> TextBlock::deserialize(HalFile& file) {
|
std::unique_ptr<TextBlock> TextBlock::deserialize(HalFile& file) {
|
||||||
uint16_t wc;
|
uint16_t wc;
|
||||||
std::vector<std::string> words;
|
uint8_t hasFocus;
|
||||||
std::vector<int16_t> wordXpos;
|
uint16_t textBytes;
|
||||||
std::vector<EpdFontFamily::Style> wordStyles;
|
|
||||||
std::vector<uint8_t> wordFocusBoundary;
|
|
||||||
std::vector<uint16_t> wordFocusSuffixX;
|
|
||||||
BlockStyle blockStyle;
|
|
||||||
|
|
||||||
// Word count
|
|
||||||
serialization::readPod(file, wc);
|
serialization::readPod(file, wc);
|
||||||
|
serialization::readPod(file, hasFocus);
|
||||||
|
serialization::readPod(file, textBytes);
|
||||||
|
|
||||||
// Sanity check: prevent allocation of unreasonably large vectors (max 10000 words per block)
|
// Sanity checks: cap the arena allocation and reject impossible geometry
|
||||||
|
// (every word carries at least its NUL terminator).
|
||||||
if (wc > 10000) {
|
if (wc > 10000) {
|
||||||
LOG_ERR("TXB", "Deserialization failed: word count %u exceeds maximum", wc);
|
LOG_ERR("TXB", "Deserialization failed: word count %u exceeds maximum", wc);
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
if ((wc == 0 && textBytes != 0) || (wc > 0 && textBytes < wc)) {
|
||||||
|
LOG_ERR("TXB", "Deserialization failed: bad text size %u for %u words", textBytes, wc);
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
// Word data
|
std::unique_ptr<TextBlock> block(new (std::nothrow) TextBlock());
|
||||||
words.resize(wc);
|
if (!block) {
|
||||||
wordXpos.resize(wc);
|
LOG_ERR("TXB", "OOM: TextBlock");
|
||||||
wordStyles.resize(wc);
|
return nullptr;
|
||||||
for (auto& w : words) serialization::readString(file, w);
|
}
|
||||||
for (auto& x : wordXpos) serialization::readPod(file, x);
|
block->numWords = wc;
|
||||||
for (auto& s : wordStyles) serialization::readPod(file, s);
|
block->textBytes = textBytes;
|
||||||
// Focus block: presence flag, then vectors only if present. Empty vectors when absent
|
block->focusPresent = hasFocus != 0;
|
||||||
// signal "no splits in this block" to render() (zero per-word RAM cost).
|
|
||||||
uint8_t hasFocus;
|
if (wc > 0) {
|
||||||
serialization::readPod(file, hasFocus);
|
const size_t size = arenaSize(wc, block->focusPresent, textBytes);
|
||||||
if (hasFocus) {
|
block->arena = makeUniqueNoThrow<uint8_t[]>(size);
|
||||||
wordFocusBoundary.resize(wc);
|
if (!block->arena) {
|
||||||
wordFocusSuffixX.resize(wc);
|
LOG_ERR("TXB", "OOM: arena %u bytes", static_cast<uint32_t>(size));
|
||||||
for (auto& b : wordFocusBoundary) serialization::readPod(file, b);
|
return nullptr;
|
||||||
for (auto& sx : wordFocusSuffixX) serialization::readPod(file, sx);
|
}
|
||||||
|
if (file.read(block->arena.get(), size) != size) {
|
||||||
|
LOG_ERR("TXB", "Deserialization failed: arena read (%u bytes)", static_cast<uint32_t>(size));
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
block->bindArenaPointers();
|
||||||
|
|
||||||
|
// Validate offsets before anything dereferences wordText(): offset 0 first,
|
||||||
|
// strictly increasing, in bounds, and every word NUL-terminated (word i ends
|
||||||
|
// at the byte before offset i+1; the last word at the last text byte).
|
||||||
|
const uint16_t* textOff = block->textOffArr;
|
||||||
|
const char* text = block->textArr;
|
||||||
|
if (textOff[0] != 0 || text[textBytes - 1] != '\0') {
|
||||||
|
LOG_ERR("TXB", "Deserialization failed: corrupt text layout");
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
for (uint16_t i = 1; i < wc; i++) {
|
||||||
|
if (textOff[i] <= textOff[i - 1] || textOff[i] >= textBytes || text[textOff[i] - 1] != '\0') {
|
||||||
|
LOG_ERR("TXB", "Deserialization failed: corrupt word offset %u", i);
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Style (alignment + margins/padding/indent)
|
// Style (alignment + margins/padding/indent)
|
||||||
|
BlockStyle& blockStyle = block->blockStyle;
|
||||||
serialization::readPod(file, blockStyle.alignment);
|
serialization::readPod(file, blockStyle.alignment);
|
||||||
serialization::readPod(file, blockStyle.textAlignDefined);
|
serialization::readPod(file, blockStyle.textAlignDefined);
|
||||||
serialization::readPod(file, blockStyle.marginTop);
|
serialization::readPod(file, blockStyle.marginTop);
|
||||||
@@ -170,7 +349,5 @@ std::unique_ptr<TextBlock> TextBlock::deserialize(HalFile& file) {
|
|||||||
serialization::readPod(file, blockStyle.isRtl);
|
serialization::readPod(file, blockStyle.isRtl);
|
||||||
serialization::readPod(file, blockStyle.directionDefined);
|
serialization::readPod(file, blockStyle.directionDefined);
|
||||||
|
|
||||||
return std::unique_ptr<TextBlock>(new TextBlock(std::move(words), std::move(wordXpos), std::move(wordStyles),
|
return block;
|
||||||
std::move(wordFocusBoundary), std::move(wordFocusSuffixX),
|
|
||||||
blockStyle));
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,42 +9,83 @@
|
|||||||
#include "Block.h"
|
#include "Block.h"
|
||||||
#include "BlockStyle.h"
|
#include "BlockStyle.h"
|
||||||
|
|
||||||
// Represents a line of text on a page
|
// Represents a line of text on a page.
|
||||||
|
//
|
||||||
|
// All per-word data lives in ONE flat heap allocation (the arena) instead of
|
||||||
|
// six parallel vectors: a resident page holds ~25-30 of these blocks, and the
|
||||||
|
// vector-of-string layout cost ~250 throwing allocations per page load, which
|
||||||
|
// was the primary driver of heap fragmentation on the ESP32-C3.
|
||||||
|
//
|
||||||
|
// Arena layout, in order (2-byte alignment holds by construction: all 16-bit
|
||||||
|
// arrays come first and the arena base is allocator-aligned; RISC-V faults on
|
||||||
|
// unaligned multi-byte access):
|
||||||
|
// uint16_t textOff[wordCount] byte offset of word i's text in text[]
|
||||||
|
// int16_t xpos[wordCount]
|
||||||
|
// uint16_t focusSuffixX[wordCount] present only when focusPresent
|
||||||
|
// uint8_t styles[wordCount]
|
||||||
|
// uint8_t focusBoundary[wordCount] present only when focusPresent
|
||||||
|
// char text[textBytes] all words back to back, NUL-terminated
|
||||||
|
//
|
||||||
|
// Each word is stored NUL-terminated so render() can hand `text + textOff[i]`
|
||||||
|
// straight to C APIs (drawText) with no std::string materialization.
|
||||||
|
//
|
||||||
|
// Focus split semantics (unchanged from the vector layout): boundary N > 0
|
||||||
|
// means the first N bytes of word i render bold, the remainder in the base
|
||||||
|
// style. N is bounded to 9 codepoints (<= 36 UTF-8 bytes) by the clamp in
|
||||||
|
// ParsedText::addWord. focusSuffixX is the pre-computed pixel offset from the
|
||||||
|
// word start to the regular suffix. Both arrays are omitted from the arena
|
||||||
|
// entirely when no word on the line has a split (zero per-word RAM cost when
|
||||||
|
// focus reading is disabled).
|
||||||
class TextBlock final : public Block {
|
class TextBlock final : public Block {
|
||||||
private:
|
private:
|
||||||
std::vector<std::string> words;
|
|
||||||
std::vector<int16_t> wordXpos;
|
|
||||||
std::vector<EpdFontFamily::Style> wordStyles;
|
|
||||||
// Per-word focus boundary: N > 0 means the first N bytes of words[i] are rendered bold,
|
|
||||||
// the remainder in the base style. 0 means no split (whole word uses wordStyles[i]).
|
|
||||||
// N encodes the bold PREFIX length only — bounded to 9 codepoints (≤36 UTF-8 bytes) by
|
|
||||||
// FOCUS_READING_PERCENT's 1..9 clamp in ParsedText::addWord, so it always fits in uint8_t.
|
|
||||||
// Vector is empty when no focus splits exist anywhere in the block (zero per-word RAM cost
|
|
||||||
// when focus reading is disabled, or on lines that happen to contain no splittable words).
|
|
||||||
std::vector<uint8_t> wordFocusBoundary;
|
|
||||||
// Pre-computed pixel offset from word start to the regular suffix, stored when boundary > 0.
|
|
||||||
// Eliminates getTextAdvanceX from the render path. 0 when boundary == 0.
|
|
||||||
// Empty in lockstep with wordFocusBoundary.
|
|
||||||
std::vector<uint16_t> wordFocusSuffixX;
|
|
||||||
BlockStyle blockStyle;
|
BlockStyle blockStyle;
|
||||||
|
uint16_t numWords = 0;
|
||||||
|
uint16_t textBytes = 0; // total size of the text region, including NULs
|
||||||
|
bool focusPresent = false;
|
||||||
|
bool isValid = true;
|
||||||
|
// The ONLY allocation: makeUniqueNoThrow, so OOM yields an invalid block
|
||||||
|
// instead of abort() (bare new is not nothrow with -fno-exceptions).
|
||||||
|
std::unique_ptr<uint8_t[]> arena;
|
||||||
|
// Typed views into the arena, bound once after the arena is filled. All
|
||||||
|
// 16-bit bases sit at even offsets, so direct dereference is alignment-safe.
|
||||||
|
const uint16_t* textOffArr = nullptr;
|
||||||
|
const int16_t* xposArr = nullptr;
|
||||||
|
const uint16_t* focusSuffixXArr = nullptr; // null when !focusPresent
|
||||||
|
const uint8_t* stylesArr = nullptr;
|
||||||
|
const uint8_t* focusBoundaryArr = nullptr; // null when !focusPresent
|
||||||
|
const char* textArr = nullptr;
|
||||||
|
|
||||||
|
TextBlock() = default; // deserialize() fills the fields directly
|
||||||
|
static size_t arenaSize(uint16_t wordCount, bool hasFocus, uint16_t textBytes);
|
||||||
|
void bindArenaPointers();
|
||||||
|
|
||||||
public:
|
public:
|
||||||
explicit TextBlock(std::vector<std::string> words, std::vector<int16_t> word_xpos,
|
// Flatten-on-construct: copies the layout-time vectors into the arena; the
|
||||||
std::vector<EpdFontFamily::Style> word_styles, std::vector<uint8_t> focus_boundary,
|
// vectors die with the caller. On arena OOM the block is empty and valid()
|
||||||
std::vector<uint16_t> focus_suffix_x, const BlockStyle& blockStyle = BlockStyle())
|
// is false -- callers must check and fail the line instead of using it.
|
||||||
: words(std::move(words)),
|
explicit TextBlock(const std::vector<std::string>& words, const std::vector<int16_t>& wordXpos,
|
||||||
wordXpos(std::move(word_xpos)),
|
const std::vector<EpdFontFamily::Style>& wordStyles, const std::vector<uint8_t>& focusBoundary,
|
||||||
wordStyles(std::move(word_styles)),
|
const std::vector<uint16_t>& focusSuffixX, const BlockStyle& blockStyle = BlockStyle());
|
||||||
wordFocusBoundary(std::move(focus_boundary)),
|
|
||||||
wordFocusSuffixX(std::move(focus_suffix_x)),
|
|
||||||
blockStyle(blockStyle) {}
|
|
||||||
~TextBlock() override = default;
|
~TextBlock() override = default;
|
||||||
|
TextBlock(const TextBlock&) = delete;
|
||||||
|
TextBlock& operator=(const TextBlock&) = delete;
|
||||||
|
|
||||||
void setBlockStyle(const BlockStyle& blockStyle) { this->blockStyle = blockStyle; }
|
void setBlockStyle(const BlockStyle& blockStyle) { this->blockStyle = blockStyle; }
|
||||||
const BlockStyle& getBlockStyle() const { return blockStyle; }
|
const BlockStyle& getBlockStyle() const { return blockStyle; }
|
||||||
const std::vector<std::string>& getWords() const { return words; }
|
bool isEmpty() override { return numWords == 0; }
|
||||||
bool isEmpty() override { return words.empty(); }
|
bool valid() const { return isValid; }
|
||||||
size_t wordCount() const { return words.size(); }
|
uint16_t wordCount() const { return numWords; }
|
||||||
// given a renderer works out where to break the words into lines
|
// NUL-terminated by construction; safe to pass to C APIs directly.
|
||||||
|
const char* wordText(const uint16_t i) const { return textArr + textOffArr[i]; }
|
||||||
|
uint16_t wordTextLen(const uint16_t i) const {
|
||||||
|
const uint16_t end = (i + 1 < numWords) ? textOffArr[i + 1] : textBytes;
|
||||||
|
return end - textOffArr[i] - 1; // exclude the NUL
|
||||||
|
}
|
||||||
|
int16_t wordXpos(const uint16_t i) const { return xposArr[i]; }
|
||||||
|
EpdFontFamily::Style wordStyle(const uint16_t i) const { return static_cast<EpdFontFamily::Style>(stylesArr[i]); }
|
||||||
|
uint8_t focusBoundary(const uint16_t i) const { return focusPresent ? focusBoundaryArr[i] : 0; }
|
||||||
|
uint16_t focusSuffixX(const uint16_t i) const { return focusPresent ? focusSuffixXArr[i] : 0; }
|
||||||
|
|
||||||
void render(const GfxRenderer& renderer, int fontId, int x, int y) const;
|
void render(const GfxRenderer& renderer, int fontId, int x, int y) const;
|
||||||
BlockType getType() override { return TEXT_BLOCK; }
|
BlockType getType() override { return TEXT_BLOCK; }
|
||||||
bool serialize(HalFile& file) const;
|
bool serialize(HalFile& file) const;
|
||||||
|
|||||||
@@ -99,24 +99,58 @@ int bytesPerPixelFromType(int pixelType) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
int requiredPngInternalBufferBytes(int srcWidth, int pixelType) {
|
int packedRowBytes(int srcWidth, int bitsPerSample) { return (srcWidth * bitsPerSample + 7) / 8; }
|
||||||
|
|
||||||
|
int requiredPngInternalBufferBytes(int srcWidth, int pixelType, int bitsPerSample) {
|
||||||
// +1 filter byte per scanline, *2 for current+previous lines, +32 for alignment margin.
|
// +1 filter byte per scanline, *2 for current+previous lines, +32 for alignment margin.
|
||||||
int pitch = srcWidth * bytesPerPixelFromType(pixelType);
|
int pitch = srcWidth * bytesPerPixelFromType(pixelType);
|
||||||
|
if ((pixelType == PNG_PIXEL_GRAYSCALE || pixelType == PNG_PIXEL_INDEXED) && bitsPerSample < 8) {
|
||||||
|
pitch = packedRowBytes(srcWidth, bitsPerSample);
|
||||||
|
}
|
||||||
return ((pitch + 1) * 2) + 32;
|
return ((pitch + 1) * 2) + 32;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool isSupportedBitDepth(int pixelType, int bitsPerSample) {
|
||||||
|
if (bitsPerSample == 8) return true;
|
||||||
|
if (bitsPerSample != 1 && bitsPerSample != 2 && bitsPerSample != 4) return false;
|
||||||
|
return pixelType == PNG_PIXEL_GRAYSCALE || pixelType == PNG_PIXEL_INDEXED;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t readPackedSample(const uint8_t* pixels, int x, int bitsPerSample) {
|
||||||
|
if (bitsPerSample == 8) return pixels[x];
|
||||||
|
|
||||||
|
const int bitOffset = x * bitsPerSample;
|
||||||
|
const int shift = 8 - bitsPerSample - (bitOffset & 7);
|
||||||
|
const uint8_t mask = (1U << bitsPerSample) - 1;
|
||||||
|
return (pixels[bitOffset >> 3] >> shift) & mask;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t expandSampleToByte(uint8_t sample, int bitsPerSample) {
|
||||||
|
if (bitsPerSample == 8) return sample;
|
||||||
|
const uint8_t maxSample = (1U << bitsPerSample) - 1;
|
||||||
|
return static_cast<uint8_t>((sample * 255U) / maxSample);
|
||||||
|
}
|
||||||
|
|
||||||
// Convert entire source line to grayscale with alpha blending to white background.
|
// Convert entire source line to grayscale with alpha blending to white background.
|
||||||
|
// 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(uint8_t* pPixels, uint8_t* grayLine, int width, int pixelType, uint8_t* palette, int hasAlpha) {
|
void convertLineToGray(const uint8_t* pPixels, uint8_t* grayLine, int width, int pixelType, int bitsPerSample,
|
||||||
|
uint8_t* palette, int hasAlpha) {
|
||||||
switch (pixelType) {
|
switch (pixelType) {
|
||||||
case PNG_PIXEL_GRAYSCALE:
|
case PNG_PIXEL_GRAYSCALE:
|
||||||
memcpy(grayLine, pPixels, width);
|
if (bitsPerSample == 8) {
|
||||||
|
memcpy(grayLine, pPixels, width);
|
||||||
|
} else {
|
||||||
|
for (int x = 0; x < width; x++) {
|
||||||
|
grayLine[x] = expandSampleToByte(readPackedSample(pPixels, x, bitsPerSample), bitsPerSample);
|
||||||
|
}
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case PNG_PIXEL_TRUECOLOR:
|
case PNG_PIXEL_TRUECOLOR:
|
||||||
for (int x = 0; x < width; x++) {
|
for (int x = 0; x < width; x++) {
|
||||||
uint8_t* p = &pPixels[x * 3];
|
const uint8_t* p = &pPixels[x * 3];
|
||||||
grayLine[x] = (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);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
@@ -125,7 +159,7 @@ void convertLineToGray(uint8_t* pPixels, uint8_t* grayLine, int width, int pixel
|
|||||||
if (palette) {
|
if (palette) {
|
||||||
if (hasAlpha) {
|
if (hasAlpha) {
|
||||||
for (int x = 0; x < width; x++) {
|
for (int x = 0; x < width; x++) {
|
||||||
uint8_t idx = pPixels[x];
|
uint8_t idx = readPackedSample(pPixels, x, bitsPerSample);
|
||||||
uint8_t* p = &palette[idx * 3];
|
uint8_t* p = &palette[idx * 3];
|
||||||
uint8_t gray = (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);
|
||||||
uint8_t alpha = palette[768 + idx];
|
uint8_t alpha = palette[768 + idx];
|
||||||
@@ -133,12 +167,15 @@ void convertLineToGray(uint8_t* pPixels, uint8_t* grayLine, int width, int pixel
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
for (int x = 0; x < width; x++) {
|
for (int x = 0; x < width; x++) {
|
||||||
uint8_t* p = &palette[pPixels[x] * 3];
|
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);
|
grayLine[x] = (uint8_t)((p[0] * 77 + p[1] * 150 + p[2] * 29) >> 8);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
memcpy(grayLine, pPixels, width);
|
for (int x = 0; x < width; x++) {
|
||||||
|
grayLine[x] = expandSampleToByte(readPackedSample(pPixels, x, bitsPerSample), bitsPerSample);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
@@ -152,7 +189,7 @@ void convertLineToGray(uint8_t* pPixels, uint8_t* grayLine, int width, int pixel
|
|||||||
|
|
||||||
case PNG_PIXEL_TRUECOLOR_ALPHA:
|
case PNG_PIXEL_TRUECOLOR_ALPHA:
|
||||||
for (int x = 0; x < width; x++) {
|
for (int x = 0; x < width; x++) {
|
||||||
uint8_t* p = &pPixels[x * 4];
|
const uint8_t* p = &pPixels[x * 4];
|
||||||
uint8_t gray = (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);
|
||||||
uint8_t alpha = p[3];
|
uint8_t alpha = p[3];
|
||||||
grayLine[x] = (uint8_t)((gray * alpha + 255 * (255 - alpha)) / 255);
|
grayLine[x] = (uint8_t)((gray * alpha + 255 * (255 - alpha)) / 255);
|
||||||
@@ -172,74 +209,83 @@ int pngDrawCallback(PNGDRAW* pDraw) {
|
|||||||
int srcY = pDraw->y;
|
int srcY = pDraw->y;
|
||||||
int srcWidth = ctx->srcWidth;
|
int srcWidth = ctx->srcWidth;
|
||||||
|
|
||||||
// Calculate destination Y with scaling
|
// Map source rows with the exact output-height ratio. During downscaling,
|
||||||
int dstY = (int)(srcY * ctx->scale);
|
// 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
|
||||||
|
// only the first row of an upscale leaves zero-filled (black) gaps in the
|
||||||
|
// streamed pixel cache.
|
||||||
|
int firstDstY = (srcY * ctx->dstHeight) / ctx->srcHeight;
|
||||||
|
int endDstY = firstDstY + 1;
|
||||||
|
if (ctx->dstHeight > ctx->srcHeight) {
|
||||||
|
endDstY = ((srcY + 1) * ctx->dstHeight) / ctx->srcHeight;
|
||||||
|
}
|
||||||
|
|
||||||
// Skip if we already rendered this destination row (multiple source rows map to same dest)
|
if (firstDstY <= ctx->lastDstY) firstDstY = ctx->lastDstY + 1;
|
||||||
if (dstY == ctx->lastDstY) return 1;
|
if (firstDstY >= endDstY || firstDstY >= ctx->dstHeight) return 1;
|
||||||
ctx->lastDstY = dstY;
|
if (endDstY > ctx->dstHeight) endDstY = ctx->dstHeight;
|
||||||
|
|
||||||
// Check bounds
|
|
||||||
if (dstY >= ctx->dstHeight) return 1;
|
|
||||||
|
|
||||||
int outY = ctx->config->y + dstY;
|
|
||||||
if (outY >= ctx->screenHeight) return 1;
|
|
||||||
|
|
||||||
// Convert entire source line to grayscale (improves cache locality)
|
// Convert entire source line to grayscale (improves cache locality)
|
||||||
convertLineToGray(pDraw->pPixels, ctx->grayLineBuffer, srcWidth, pDraw->iPixelType, pDraw->pPalette,
|
convertLineToGray(pDraw->pPixels, ctx->grayLineBuffer, srcWidth, pDraw->iPixelType, pDraw->iBpp, pDraw->pPalette,
|
||||||
pDraw->iHasAlpha);
|
pDraw->iHasAlpha);
|
||||||
|
|
||||||
// Render scaled row 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;
|
||||||
int outXBase = ctx->config->x;
|
int outXBase = ctx->config->x;
|
||||||
int screenWidth = ctx->screenWidth;
|
int screenWidth = ctx->screenWidth;
|
||||||
bool useDithering = ctx->config->useDithering;
|
bool useDithering = ctx->config->useDithering;
|
||||||
bool caching = ctx->caching;
|
|
||||||
|
|
||||||
// Pre-compute orientation and render-mode state once per row
|
// Pre-compute orientation and render-mode state once per callback.
|
||||||
DirectPixelWriter pw;
|
DirectPixelWriter pw;
|
||||||
pw.init(*ctx->renderer);
|
pw.init(*ctx->renderer);
|
||||||
pw.beginRow(outY);
|
|
||||||
|
|
||||||
// The cache streams to disk one row at a time. Flushing rows below this one
|
for (int dstY = firstDstY; dstY < endDstY; dstY++) {
|
||||||
// (PNGdec delivers scanlines top to bottom) repositions the single-row band.
|
ctx->lastDstY = dstY;
|
||||||
// A flush failure stops caching for the rest of the decode so we never write
|
int outY = ctx->config->y + dstY;
|
||||||
// past the band buffer; finalize() then drops the partial file.
|
if (outY >= ctx->screenHeight) continue;
|
||||||
DirectCacheWriter cw;
|
|
||||||
if (caching) {
|
|
||||||
if (!ctx->cache.advanceTo(dstY)) {
|
|
||||||
caching = false;
|
|
||||||
ctx->caching = false;
|
|
||||||
} else {
|
|
||||||
cw.init(ctx->cache.buffer, ctx->cache.bytesPerRow, ctx->cache.bandRows, ctx->cache.originX);
|
|
||||||
cw.beginRow(outY, ctx->config->y + ctx->cache.bandStart);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
int srcX = 0;
|
pw.beginRow(outY);
|
||||||
int error = 0;
|
|
||||||
|
|
||||||
for (int dstX = 0; dstX < dstWidth; dstX++) {
|
// The cache streams to disk one row at a time. Flushing rows below this one
|
||||||
int outX = outXBase + dstX;
|
// (PNGdec delivers scanlines top to bottom) repositions the single-row band.
|
||||||
if (outX < screenWidth) {
|
// A flush failure stops caching for the rest of the decode so we never write
|
||||||
uint8_t gray = ctx->grayLineBuffer[srcX];
|
// past the band buffer; finalize() then drops the partial file.
|
||||||
|
bool caching = ctx->caching;
|
||||||
uint8_t ditheredGray;
|
DirectCacheWriter cw;
|
||||||
if (useDithering) {
|
if (caching) {
|
||||||
ditheredGray = applyBayerDither4Level(gray, outX, outY);
|
if (!ctx->cache.advanceTo(dstY)) {
|
||||||
|
caching = false;
|
||||||
|
ctx->caching = false;
|
||||||
} else {
|
} else {
|
||||||
ditheredGray = gray / 85;
|
cw.init(ctx->cache.buffer, ctx->cache.bytesPerRow, ctx->cache.bandRows, ctx->cache.originX);
|
||||||
if (ditheredGray > 3) ditheredGray = 3;
|
cw.beginRow(outY, ctx->config->y + ctx->cache.bandStart);
|
||||||
}
|
}
|
||||||
pw.writePixel(outX, ditheredGray);
|
|
||||||
if (caching) cw.writePixel(outX, ditheredGray);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Bresenham-style stepping: advance srcX based on ratio srcWidth/dstWidth
|
int srcX = 0;
|
||||||
error += srcWidth;
|
int error = 0;
|
||||||
while (error >= dstWidth) {
|
|
||||||
error -= dstWidth;
|
for (int dstX = 0; dstX < dstWidth; dstX++) {
|
||||||
srcX++;
|
int outX = outXBase + dstX;
|
||||||
|
if (outX < screenWidth) {
|
||||||
|
uint8_t gray = ctx->grayLineBuffer[srcX];
|
||||||
|
|
||||||
|
uint8_t ditheredGray;
|
||||||
|
if (useDithering) {
|
||||||
|
ditheredGray = applyBayerDither4Level(gray, outX, outY);
|
||||||
|
} else {
|
||||||
|
ditheredGray = gray / 85;
|
||||||
|
if (ditheredGray > 3) ditheredGray = 3;
|
||||||
|
}
|
||||||
|
pw.writePixel(outX, ditheredGray);
|
||||||
|
if (caching) cw.writePixel(outX, ditheredGray);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bresenham-style stepping: advance srcX based on ratio srcWidth/dstWidth
|
||||||
|
error += srcWidth;
|
||||||
|
while (error >= dstWidth) {
|
||||||
|
error -= dstWidth;
|
||||||
|
srcX++;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -332,30 +378,44 @@ bool PngToFramebufferConverter::decodeToFramebuffer(const std::string& imagePath
|
|||||||
}
|
}
|
||||||
ctx.lastDstY = -1; // Reset row tracking
|
ctx.lastDstY = -1; // Reset row tracking
|
||||||
|
|
||||||
LOG_DBG("PNG", "PNG %dx%d -> %dx%d (scale %.2f), bpp: %d", ctx.srcWidth, ctx.srcHeight, ctx.dstWidth, ctx.dstHeight,
|
|
||||||
ctx.scale, png->getBpp());
|
|
||||||
|
|
||||||
const int pixelType = png->getPixelType();
|
const int pixelType = png->getPixelType();
|
||||||
const int requiredInternal = requiredPngInternalBufferBytes(ctx.srcWidth, pixelType);
|
const int bitsPerSample = png->getBpp();
|
||||||
|
LOG_DBG("PNG", "PNG %dx%d -> %dx%d (scale %.2f), type: %d, bpp: %d", ctx.srcWidth, ctx.srcHeight, ctx.dstWidth,
|
||||||
|
ctx.dstHeight, ctx.scale, pixelType, bitsPerSample);
|
||||||
|
|
||||||
|
const int requiredInternal = requiredPngInternalBufferBytes(ctx.srcWidth, pixelType, bitsPerSample);
|
||||||
if (requiredInternal > PNG_MAX_BUFFERED_PIXELS) {
|
if (requiredInternal > PNG_MAX_BUFFERED_PIXELS) {
|
||||||
LOG_ERR("PNG",
|
LOG_ERR(
|
||||||
"PNG row buffer too small: need %d bytes for width=%d type=%d, configured PNG_MAX_BUFFERED_PIXELS=%d",
|
"PNG",
|
||||||
requiredInternal, ctx.srcWidth, pixelType, PNG_MAX_BUFFERED_PIXELS);
|
"PNG row buffer too small: need %d bytes for width=%d type=%d bpp=%d, configured PNG_MAX_BUFFERED_PIXELS=%d",
|
||||||
|
requiredInternal, ctx.srcWidth, pixelType, bitsPerSample, PNG_MAX_BUFFERED_PIXELS);
|
||||||
LOG_ERR("PNG", "Aborting decode to avoid PNGdec internal buffer overflow");
|
LOG_ERR("PNG", "Aborting decode to avoid PNGdec internal buffer overflow");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (png->getBpp() != 8) {
|
if (!isSupportedBitDepth(pixelType, bitsPerSample)) {
|
||||||
warnUnsupportedFeature("bit depth (" + std::to_string(png->getBpp()) + "bpp)", imagePath);
|
warnUnsupportedFeature(
|
||||||
|
"bit depth (" + std::to_string(bitsPerSample) + "bpp) for pixel type " + std::to_string(pixelType), imagePath);
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Allocate grayscale line buffer on demand (~3.2 KB) - freed after decode
|
// The converter expands each source row to 8-bit grayscale before dithering,
|
||||||
const size_t grayBufSize = PNG_MAX_BUFFERED_PIXELS / 2;
|
// so this scratch buffer is sized by source pixels even when PNGdec reads a
|
||||||
ctx.grayLineBuffer = static_cast<uint8_t*>(malloc(grayBufSize));
|
// packed 1/2/4-bit row internally.
|
||||||
if (!ctx.grayLineBuffer) {
|
constexpr size_t MAX_GRAY_LINE_BUFFER_BYTES = PNG_MAX_BUFFERED_PIXELS / 2;
|
||||||
|
const size_t grayBufSize = static_cast<size_t>(ctx.srcWidth);
|
||||||
|
if (grayBufSize > MAX_GRAY_LINE_BUFFER_BYTES) {
|
||||||
|
LOG_ERR("PNG", "Expanded gray row too wide: need %u bytes for width=%d, max=%u", static_cast<unsigned>(grayBufSize),
|
||||||
|
ctx.srcWidth, static_cast<unsigned>(MAX_GRAY_LINE_BUFFER_BYTES));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto grayLineBuffer = makeUniqueNoThrow<uint8_t[]>(grayBufSize);
|
||||||
|
if (!grayLineBuffer) {
|
||||||
LOG_ERR("PNG", "Failed to allocate gray line buffer");
|
LOG_ERR("PNG", "Failed to allocate gray line buffer");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
ctx.grayLineBuffer = grayLineBuffer.get();
|
||||||
|
|
||||||
// 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
|
||||||
@@ -375,7 +435,6 @@ bool PngToFramebufferConverter::decodeToFramebuffer(const std::string& imagePath
|
|||||||
rc = png->decode(&ctx, 0);
|
rc = png->decode(&ctx, 0);
|
||||||
unsigned long decodeTime = millis() - decodeStart;
|
unsigned long decodeTime = millis() - decodeStart;
|
||||||
|
|
||||||
free(ctx.grayLineBuffer);
|
|
||||||
ctx.grayLineBuffer = nullptr;
|
ctx.grayLineBuffer = nullptr;
|
||||||
|
|
||||||
if (rc != PNG_SUCCESS) {
|
if (rc != PNG_SUCCESS) {
|
||||||
|
|||||||
@@ -67,13 +67,6 @@ constexpr bool iequalsAscii(std::string_view value, std::string_view lowercaseKe
|
|||||||
[](char a, char b) { return asciiToLower(a) == b; });
|
[](char a, char b) { return asciiToLower(a) == b; });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Case-insensitive ASCII substring search. Only needed by text-decoration,
|
|
||||||
// which accepts multi-value strings like "underline solid red".
|
|
||||||
constexpr bool icontainsAscii(std::string_view value, std::string_view lowercaseKeyword) {
|
|
||||||
return std::search(value.begin(), value.end(), lowercaseKeyword.begin(), lowercaseKeyword.end(),
|
|
||||||
[](char a, char b) { return asciiToLower(a) == b; }) != value.end();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Walk s and invoke fn(token) for each non-empty run between delimiters.
|
// Walk s and invoke fn(token) for each non-empty run between delimiters.
|
||||||
// Tokens are boundary-trimmed and yielded as string_views into s; no
|
// Tokens are boundary-trimmed and yielded as string_views into s; no
|
||||||
// allocation. Runs of consecutive delimiters coalesce — no empty tokens are
|
// allocation. Runs of consecutive delimiters coalesce — no empty tokens are
|
||||||
@@ -252,11 +245,20 @@ CssFontWeight CssParser::interpretFontWeight(std::string_view val) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
CssTextDecoration CssParser::interpretDecoration(std::string_view val) {
|
CssTextDecoration CssParser::interpretDecoration(std::string_view val) {
|
||||||
// text-decoration can have multiple space-separated values
|
// text-decoration can have multiple space-separated values. Compare whole tokens
|
||||||
if (icontainsAscii(val, "underline")) {
|
// so malformed values like "notunderline" do not accidentally enable a line.
|
||||||
return CssTextDecoration::Underline;
|
CssTextDecoration result = CssTextDecoration::None;
|
||||||
}
|
bool explicitNone = false;
|
||||||
return CssTextDecoration::None;
|
forEachDelimitedToken(val, isCssWhitespace, [&](const std::string_view token) {
|
||||||
|
if (iequalsAscii(token, "none")) {
|
||||||
|
explicitNone = true;
|
||||||
|
} else if (iequalsAscii(token, "underline")) {
|
||||||
|
result = result | CssTextDecoration::Underline;
|
||||||
|
} else if (iequalsAscii(token, "line-through")) {
|
||||||
|
result = result | CssTextDecoration::LineThrough;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return explicitNone ? CssTextDecoration::None : result;
|
||||||
}
|
}
|
||||||
|
|
||||||
CssLength CssParser::interpretLength(std::string_view val) {
|
CssLength CssParser::interpretLength(std::string_view val) {
|
||||||
@@ -429,6 +431,11 @@ CssStyle CssParser::parseDeclarations(std::string_view declBlock) {
|
|||||||
// Rule processing
|
// Rule processing
|
||||||
|
|
||||||
void CssParser::processRuleBlockWithStyle(std::string_view selectorGroup, const CssStyle& style) {
|
void CssParser::processRuleBlockWithStyle(std::string_view selectorGroup, const CssStyle& style) {
|
||||||
|
// Skip rules that don't define any supported properties to save RAM.
|
||||||
|
if (!style.defined.anySet()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Check if we've reached the rule limit before processing
|
// Check if we've reached the rule limit before processing
|
||||||
if (rulesBySelector_.size() >= MAX_RULES) {
|
if (rulesBySelector_.size() >= MAX_RULES) {
|
||||||
LOG_DBG("CSS", "Reached max rules limit (%zu), stopping CSS parsing", MAX_RULES);
|
LOG_DBG("CSS", "Reached max rules limit (%zu), stopping CSS parsing", MAX_RULES);
|
||||||
@@ -801,6 +808,9 @@ bool CssParser::loadFromCache() {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Size the bucket array up front to avoid incremental rehashes while loading rules.
|
||||||
|
rulesBySelector_.reserve(ruleCount);
|
||||||
|
|
||||||
auto hasRemainingBytes = [&file](const size_t neededBytes) -> bool {
|
auto hasRemainingBytes = [&file](const size_t neededBytes) -> bool {
|
||||||
return static_cast<size_t>(file.available()) >= neededBytes;
|
return static_cast<size_t>(file.available()) >= neededBytes;
|
||||||
};
|
};
|
||||||
@@ -868,7 +878,7 @@ bool CssParser::loadFromCache() {
|
|||||||
rulesBySelector_.clear();
|
rulesBySelector_.clear();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
style.textDecoration = static_cast<CssTextDecoration>(enumVal);
|
style.textDecoration = static_cast<CssTextDecoration>(enumVal & CSS_TEXT_DECORATION_MASK);
|
||||||
|
|
||||||
if (file.read(&enumVal, 1) != 1) {
|
if (file.read(&enumVal, 1) != 1) {
|
||||||
rulesBySelector_.clear();
|
rulesBySelector_.clear();
|
||||||
|
|||||||
@@ -33,7 +33,7 @@
|
|||||||
class CssParser {
|
class CssParser {
|
||||||
public:
|
public:
|
||||||
// Bump when CSS cache format or rules change; section caches are invalidated when this changes
|
// Bump when CSS cache format or rules change; section caches are invalidated when this changes
|
||||||
static constexpr uint8_t CSS_CACHE_VERSION = 6;
|
static constexpr uint8_t CSS_CACHE_VERSION = 8;
|
||||||
|
|
||||||
explicit CssParser(std::string cachePath) : cachePath(std::move(cachePath)) {}
|
explicit CssParser(std::string cachePath) : cachePath(std::move(cachePath)) {}
|
||||||
~CssParser() = default;
|
~CssParser() = default;
|
||||||
|
|||||||
@@ -52,8 +52,19 @@ enum class CssFontStyle : uint8_t { Normal = 0, Italic = 1 };
|
|||||||
// Font weight options - CSS supports 100-900, we simplify to normal/bold
|
// Font weight options - CSS supports 100-900, we simplify to normal/bold
|
||||||
enum class CssFontWeight : uint8_t { Normal = 0, Bold = 1 };
|
enum class CssFontWeight : uint8_t { Normal = 0, Bold = 1 };
|
||||||
|
|
||||||
// Text decoration options
|
// Text decoration options. Values are bit flags so CSS can combine multiple line decorations.
|
||||||
enum class CssTextDecoration : uint8_t { None = 0, Underline = 1 };
|
enum class CssTextDecoration : uint8_t { None = 0, Underline = 1, LineThrough = 2 };
|
||||||
|
|
||||||
|
constexpr CssTextDecoration operator|(const CssTextDecoration a, const CssTextDecoration b) {
|
||||||
|
return static_cast<CssTextDecoration>(static_cast<uint8_t>(a) | static_cast<uint8_t>(b));
|
||||||
|
}
|
||||||
|
|
||||||
|
constexpr CssTextDecoration operator&(const CssTextDecoration a, const CssTextDecoration b) {
|
||||||
|
return static_cast<CssTextDecoration>(static_cast<uint8_t>(a) & static_cast<uint8_t>(b));
|
||||||
|
}
|
||||||
|
|
||||||
|
constexpr uint8_t CSS_TEXT_DECORATION_MASK =
|
||||||
|
static_cast<uint8_t>(CssTextDecoration::Underline) | static_cast<uint8_t>(CssTextDecoration::LineThrough);
|
||||||
|
|
||||||
// Display options - only None and Block are relevant for e-ink rendering
|
// Display options - only None and Block are relevant for e-ink rendering
|
||||||
enum class CssDisplay : uint8_t { Block = 0, None = 1 };
|
enum class CssDisplay : uint8_t { Block = 0, None = 1 };
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ struct Iso639Mapping {
|
|||||||
};
|
};
|
||||||
static constexpr Iso639Mapping kIso639Mappings[] = {{"eng", "en"}, {"fra", "fr"}, {"fre", "fr"}, {"deu", "de"},
|
static constexpr Iso639Mapping kIso639Mappings[] = {{"eng", "en"}, {"fra", "fr"}, {"fre", "fr"}, {"deu", "de"},
|
||||||
{"ger", "de"}, {"rus", "ru"}, {"spa", "es"}, {"ita", "it"},
|
{"ger", "de"}, {"rus", "ru"}, {"spa", "es"}, {"ita", "it"},
|
||||||
{"ukr", "uk"}, {"swe", "sv"}};
|
{"ukr", "uk"}, {"swe", "sv"}, {"fin", "fi"}};
|
||||||
|
|
||||||
// Maps a BCP-47 or ISO 639-2 language tag to a language-specific hyphenator.
|
// Maps a BCP-47 or ISO 639-2 language tag to a language-specific hyphenator.
|
||||||
const LanguageHyphenator* hyphenatorForLanguage(const std::string& langTag) {
|
const LanguageHyphenator* hyphenatorForLanguage(const std::string& langTag) {
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
#include "generated/hyph-de.trie.h"
|
#include "generated/hyph-de.trie.h"
|
||||||
#include "generated/hyph-en.trie.h"
|
#include "generated/hyph-en.trie.h"
|
||||||
#include "generated/hyph-es.trie.h"
|
#include "generated/hyph-es.trie.h"
|
||||||
|
#include "generated/hyph-fi.trie.h"
|
||||||
#include "generated/hyph-fr.trie.h"
|
#include "generated/hyph-fr.trie.h"
|
||||||
#include "generated/hyph-it.trie.h"
|
#include "generated/hyph-it.trie.h"
|
||||||
#include "generated/hyph-pl.trie.h"
|
#include "generated/hyph-pl.trie.h"
|
||||||
@@ -26,8 +27,9 @@ LanguageHyphenator italianHyphenator(it_patterns, isLatinLetter, toLowerLatin);
|
|||||||
LanguageHyphenator swedishHyphenator(sv_patterns, isLatinLetter, toLowerLatin);
|
LanguageHyphenator swedishHyphenator(sv_patterns, isLatinLetter, toLowerLatin);
|
||||||
LanguageHyphenator ukrainianHyphenator(uk_patterns, isCyrillicLetter, toLowerCyrillic);
|
LanguageHyphenator ukrainianHyphenator(uk_patterns, isCyrillicLetter, toLowerCyrillic);
|
||||||
LanguageHyphenator polishHyphenator(pl_patterns, isLatinLetter, toLowerLatin);
|
LanguageHyphenator polishHyphenator(pl_patterns, isLatinLetter, toLowerLatin);
|
||||||
|
LanguageHyphenator finnishHyphenator(fi_patterns, isLatinLetter, toLowerLatin);
|
||||||
|
|
||||||
using EntryArray = std::array<LanguageEntry, 9>;
|
using EntryArray = std::array<LanguageEntry, 10>;
|
||||||
|
|
||||||
const EntryArray& entries() {
|
const EntryArray& entries() {
|
||||||
static const EntryArray kEntries = {{{"english", "en", &englishHyphenator},
|
static const EntryArray kEntries = {{{"english", "en", &englishHyphenator},
|
||||||
@@ -38,7 +40,8 @@ const EntryArray& entries() {
|
|||||||
{"italian", "it", &italianHyphenator},
|
{"italian", "it", &italianHyphenator},
|
||||||
{"polish", "pl", &polishHyphenator},
|
{"polish", "pl", &polishHyphenator},
|
||||||
{"swedish", "sv", &swedishHyphenator},
|
{"swedish", "sv", &swedishHyphenator},
|
||||||
{"ukrainian", "uk", &ukrainianHyphenator}}};
|
{"ukrainian", "uk", &ukrainianHyphenator},
|
||||||
|
{"finnish", "fi", &finnishHyphenator}}};
|
||||||
return kEntries;
|
return kEntries;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,95 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <cstddef>
|
||||||
|
#include <cstdint>
|
||||||
|
|
||||||
|
#include "Epub/hyphenation/SerializedHyphenationTrie.h"
|
||||||
|
|
||||||
|
// Auto-generated by generate_hyphenation_trie.py. Do not edit manually.
|
||||||
|
alignas(4) constexpr uint8_t fi_trie_data[] = {
|
||||||
|
0x01, 0x01, 0x16, 0x0B, 0x0C, 0x17, 0x0C, 0x15, 0x0C, 0x0B, 0x16, 0x29, 0x2B, 0x20, 0x1F, 0x0C,
|
||||||
|
0x33, 0x02, 0x0B, 0x02, 0x0B, 0x0C, 0x01, 0x0C, 0x34, 0x0B, 0x2A, 0x0B, 0x2A, 0x0B, 0x0C, 0x20,
|
||||||
|
0x0B, 0x21, 0xA0, 0x00, 0x41, 0xA0, 0x02, 0x51, 0x21, 0x69, 0xFD, 0x21, 0x6C, 0xFD, 0xA1, 0x00,
|
||||||
|
0x41, 0x62, 0xFD, 0xA0, 0x01, 0xA2, 0xA1, 0x00, 0x81, 0x6F, 0xFD, 0xA3, 0x00, 0x81, 0x69, 0x6F,
|
||||||
|
0x75, 0xF8, 0xF8, 0xF8, 0x28, 0x61, 0x65, 0x69, 0x6F, 0x75, 0x79, 0x6C, 0x72, 0xDE, 0xDE, 0xEA,
|
||||||
|
0xDE, 0xDE, 0xDE, 0xF2, 0xF7, 0x22, 0xA4, 0xB6, 0xCD, 0xCD, 0xA1, 0x00, 0x81, 0x61, 0xD9, 0x28,
|
||||||
|
0x61, 0x65, 0x69, 0x6F, 0x75, 0x79, 0xC3, 0x72, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xF6, 0xFB,
|
||||||
|
0xA2, 0x00, 0x81, 0x61, 0x65, 0xC3, 0xC3, 0x48, 0x61, 0x65, 0x69, 0x6F, 0x75, 0x79, 0x6C, 0x72,
|
||||||
|
0xFF, 0xAB, 0xFF, 0xAB, 0xFF, 0xAB, 0xFF, 0xAB, 0xFF, 0xAB, 0xFF, 0xAB, 0xFF, 0xE3, 0xFF, 0xF9,
|
||||||
|
0x49, 0x61, 0x65, 0x69, 0x6F, 0x75, 0x79, 0xC3, 0x6C, 0x72, 0xFF, 0x92, 0xFF, 0x92, 0xFF, 0x92,
|
||||||
|
0xFF, 0x92, 0xFF, 0x92, 0xFF, 0x92, 0xFF, 0xC5, 0xFF, 0xA6, 0xFF, 0xCA, 0x47, 0x61, 0x65, 0x69,
|
||||||
|
0x6F, 0x75, 0x79, 0xC3, 0xFF, 0x76, 0xFF, 0x76, 0xFF, 0x76, 0xFF, 0x76, 0xFF, 0x76, 0xFF, 0x76,
|
||||||
|
0xFF, 0xA9, 0xA0, 0x00, 0xF1, 0x21, 0x73, 0xFD, 0xA1, 0x00, 0x41, 0x75, 0xFD, 0xA0, 0x00, 0x81,
|
||||||
|
0x43, 0x61, 0x65, 0x69, 0xFF, 0x63, 0xFF, 0x63, 0xFF, 0x63, 0xC1, 0x01, 0xA2, 0x61, 0xFF, 0x59,
|
||||||
|
0x4A, 0x61, 0x65, 0x69, 0x6F, 0x75, 0x79, 0xC3, 0x6C, 0x72, 0x76, 0xFF, 0x42, 0xFF, 0xE8, 0xFF,
|
||||||
|
0x42, 0xFF, 0x42, 0xFF, 0x42, 0xFF, 0x42, 0xFF, 0x75, 0xFF, 0xED, 0xFF, 0xF0, 0xFF, 0xFA, 0xA1,
|
||||||
|
0x00, 0x41, 0x73, 0xCE, 0x47, 0x61, 0x65, 0x69, 0x6F, 0x75, 0x79, 0xC3, 0xFF, 0xFB, 0xFF, 0x1E,
|
||||||
|
0xFF, 0x1E, 0xFF, 0x1E, 0xFF, 0x1E, 0xFF, 0x1E, 0xFF, 0x51, 0xA0, 0x01, 0x52, 0x21, 0x6F, 0xFD,
|
||||||
|
0x22, 0x74, 0x6E, 0xFD, 0xFD, 0xA0, 0x01, 0x73, 0x21, 0x6E, 0xFD, 0x22, 0x61, 0x6F, 0xFD, 0xFA,
|
||||||
|
0x21, 0x61, 0xEA, 0x21, 0x6B, 0xFD, 0x21, 0x65, 0xF2, 0xA4, 0x00, 0x41, 0x6E, 0x6A, 0x69, 0x6C,
|
||||||
|
0xE7, 0xF2, 0xFA, 0xFD, 0x21, 0x73, 0xE1, 0x21, 0x75, 0xFD, 0xA1, 0x00, 0x41, 0x64, 0xFD, 0x21,
|
||||||
|
0x74, 0xD6, 0x21, 0x73, 0xFD, 0x22, 0x65, 0x69, 0xFA, 0xFD, 0x21, 0x61, 0xCB, 0x21, 0x6E, 0xBD,
|
||||||
|
0x22, 0x74, 0x6F, 0xBD, 0xFD, 0x21, 0x69, 0xC0, 0x21, 0x61, 0xFD, 0xA4, 0x00, 0x41, 0x70, 0x73,
|
||||||
|
0x74, 0x6D, 0xEA, 0xEF, 0xF5, 0xFD, 0x21, 0x69, 0xD9, 0xA1, 0x00, 0x41, 0x6C, 0xFD, 0x47, 0x61,
|
||||||
|
0x65, 0x69, 0x6F, 0x75, 0x79, 0xC3, 0xFF, 0xBB, 0xFF, 0xCC, 0xFE, 0xA4, 0xFF, 0xED, 0xFE, 0xA4,
|
||||||
|
0xFF, 0xFB, 0xFE, 0xD7, 0xA0, 0x01, 0x41, 0x21, 0x73, 0xFD, 0x21, 0x75, 0xFD, 0xA1, 0x00, 0x41,
|
||||||
|
0x72, 0xFD, 0x49, 0x61, 0x65, 0x69, 0x6F, 0x75, 0x79, 0xC3, 0x6C, 0x72, 0xFE, 0x80, 0xFF, 0xFB,
|
||||||
|
0xFE, 0x80, 0xFE, 0x80, 0xFE, 0x80, 0xFE, 0x80, 0xFE, 0xB3, 0xFF, 0x2B, 0xFE, 0x94, 0x21, 0x61,
|
||||||
|
0xDC, 0x48, 0x61, 0x65, 0x69, 0x6F, 0x75, 0x79, 0xC3, 0x74, 0xFF, 0x3E, 0xFE, 0x61, 0xFE, 0x61,
|
||||||
|
0xFE, 0x61, 0xFE, 0x61, 0xFE, 0x61, 0xFE, 0x94, 0xFF, 0xFD, 0x42, 0x69, 0x65, 0xFF, 0x80, 0xFF,
|
||||||
|
0x40, 0x42, 0x6F, 0x65, 0xFF, 0x84, 0xFF, 0x47, 0x41, 0x75, 0xFF, 0x32, 0x21, 0x74, 0xFC, 0x42,
|
||||||
|
0x61, 0x6F, 0xFF, 0xFD, 0xFF, 0x36, 0xA4, 0x00, 0x41, 0x73, 0x6C, 0x6A, 0x70, 0xE4, 0xEB, 0xF9,
|
||||||
|
0xF2, 0x41, 0x79, 0xFF, 0x24, 0x21, 0x74, 0xFC, 0x21, 0x69, 0xFD, 0xA1, 0x00, 0x41, 0x73, 0xFD,
|
||||||
|
0x42, 0x2E, 0x6E, 0xFF, 0x15, 0xFF, 0x15, 0x21, 0x61, 0xF9, 0x21, 0x65, 0xFD, 0xA1, 0x00, 0x41,
|
||||||
|
0x64, 0xFD, 0x41, 0x65, 0xFE, 0xF8, 0x21, 0x6A, 0xFC, 0x42, 0x6B, 0x74, 0xFE, 0xFC, 0xFE, 0xFC,
|
||||||
|
0x21, 0x73, 0xF9, 0x21, 0x69, 0xFD, 0xC3, 0x00, 0x41, 0x68, 0x70, 0x73, 0xFF, 0xF0, 0xFF, 0xFD,
|
||||||
|
0xFF, 0x24, 0x41, 0x74, 0xFF, 0x23, 0xC2, 0x00, 0x41, 0x72, 0x68, 0xFF, 0x30, 0xFF, 0xFC, 0xA0,
|
||||||
|
0x00, 0x52, 0x21, 0x72, 0xFD, 0x21, 0x69, 0xFA, 0x21, 0x6C, 0xFD, 0xA0, 0x00, 0x61, 0x21, 0x68,
|
||||||
|
0xFD, 0x4A, 0x61, 0x65, 0x69, 0x6F, 0x75, 0x79, 0xC3, 0x74, 0x70, 0x63, 0xFF, 0x95, 0xFF, 0xAA,
|
||||||
|
0xFF, 0xBC, 0xFF, 0xD5, 0xFD, 0xC1, 0xFF, 0xE5, 0xFD, 0xF4, 0xFF, 0xF1, 0xFF, 0xF7, 0xFF, 0xFD,
|
||||||
|
0x48, 0x61, 0x65, 0x69, 0x6F, 0x75, 0x79, 0xC3, 0x73, 0xFD, 0xA2, 0xFD, 0xA2, 0xFD, 0xA2, 0xFD,
|
||||||
|
0xA2, 0xFD, 0xA2, 0xFD, 0xA2, 0xFD, 0xD5, 0xFF, 0xDE, 0xA0, 0x00, 0x92, 0xA0, 0x00, 0xB2, 0xA0,
|
||||||
|
0x01, 0x01, 0xC3, 0x00, 0x61, 0x69, 0x65, 0x79, 0xFE, 0x20, 0xFE, 0x20, 0xFF, 0xFD, 0x22, 0xA4,
|
||||||
|
0xB6, 0xF4, 0xAD, 0x25, 0x79, 0x61, 0x6F, 0x75, 0xC3, 0xA8, 0xE6, 0xE6, 0xE9, 0xFB, 0x42, 0xB6,
|
||||||
|
0xA4, 0xFF, 0x9D, 0xFF, 0x9D, 0x46, 0x79, 0x61, 0x6F, 0x75, 0xC3, 0x65, 0xFF, 0x96, 0xFF, 0xD4,
|
||||||
|
0xFF, 0xD4, 0xFF, 0xD7, 0xFF, 0xF9, 0xFF, 0xD7, 0x22, 0xA4, 0xB6, 0xDB, 0xED, 0xA0, 0x00, 0x72,
|
||||||
|
0xA0, 0x00, 0x71, 0x21, 0xA4, 0xFD, 0x21, 0xC3, 0xFD, 0x21, 0xA4, 0xFD, 0x21, 0x69, 0xF4, 0xA0,
|
||||||
|
0x01, 0x22, 0x21, 0x70, 0xFD, 0x21, 0x6F, 0xFD, 0x21, 0x69, 0xFD, 0x26, 0x61, 0x6F, 0x75, 0xC3,
|
||||||
|
0x65, 0x6C, 0xE2, 0xE2, 0xE2, 0xEE, 0xF1, 0xFD, 0x22, 0xA4, 0xB6, 0xD8, 0xD8, 0x21, 0x61, 0xD3,
|
||||||
|
0xA0, 0x00, 0xB1, 0x24, 0x75, 0x69, 0x65, 0x6F, 0xCD, 0xCD, 0xFD, 0xFD, 0x24, 0x61, 0x65, 0x6F,
|
||||||
|
0x75, 0xF4, 0xF4, 0xF4, 0xF4, 0x25, 0x79, 0xC3, 0x61, 0x75, 0x69, 0xBB, 0xE3, 0xE8, 0xEE, 0xF7,
|
||||||
|
0xA0, 0x00, 0xD2, 0x22, 0xA4, 0xB6, 0xFD, 0xFD, 0x44, 0x61, 0x65, 0x6F, 0x69, 0xFF, 0x64, 0xFF,
|
||||||
|
0x64, 0xFF, 0x64, 0xFF, 0x64, 0x42, 0x65, 0x61, 0xFF, 0x9B, 0xFF, 0xCB, 0x21, 0x65, 0xC4, 0x22,
|
||||||
|
0x61, 0x75, 0xC1, 0xC1, 0xA0, 0x02, 0x32, 0x21, 0x73, 0xFD, 0x21, 0x6F, 0xFD, 0x49, 0x79, 0xC3,
|
||||||
|
0x75, 0x61, 0x65, 0x69, 0x6F, 0x73, 0x6C, 0xFF, 0x80, 0xFF, 0xD6, 0xFF, 0xDB, 0xFF, 0xB0, 0xFF,
|
||||||
|
0xE8, 0xFF, 0xEF, 0xFF, 0xF2, 0xFD, 0x70, 0xFF, 0xFD, 0x44, 0x69, 0x65, 0x6F, 0x75, 0xFF, 0x23,
|
||||||
|
0xFF, 0x23, 0xFF, 0x23, 0xFF, 0x23, 0x43, 0x75, 0x61, 0x65, 0xFF, 0x5A, 0xFF, 0x8A, 0xFF, 0x8A,
|
||||||
|
0x41, 0x76, 0xFF, 0x5F, 0x21, 0x61, 0xFC, 0xA0, 0x01, 0xC2, 0x21, 0x73, 0xFD, 0x21, 0x69, 0xFD,
|
||||||
|
0x21, 0x65, 0xFD, 0x43, 0x69, 0x6F, 0x6B, 0xFF, 0xF1, 0xFD, 0xF7, 0xFF, 0xFD, 0xA0, 0x01, 0xA4,
|
||||||
|
0x21, 0x73, 0xFD, 0x21, 0x61, 0xFD, 0x43, 0x6E, 0x74, 0x6B, 0xFC, 0x7D, 0xFC, 0x7D, 0xFF, 0xFD,
|
||||||
|
0x41, 0x69, 0xFC, 0x73, 0x22, 0x61, 0x6F, 0xF2, 0xFC, 0x21, 0x69, 0xFB, 0x48, 0xC3, 0x61, 0x75,
|
||||||
|
0x65, 0x6F, 0x69, 0x6C, 0x73, 0xFF, 0x3C, 0xFF, 0xAD, 0xFF, 0xBA, 0xFF, 0x20, 0xFF, 0x20, 0xFF,
|
||||||
|
0x50, 0xFF, 0xD7, 0xFF, 0xFD, 0x44, 0x61, 0x69, 0x75, 0x79, 0xFE, 0xB7, 0xFE, 0xB7, 0xFE, 0xB7,
|
||||||
|
0xFE, 0xB7, 0x42, 0x61, 0x69, 0xFE, 0xEE, 0xFE, 0xEE, 0x42, 0x75, 0x61, 0xFE, 0xE7, 0xFF, 0x17,
|
||||||
|
0x42, 0xA4, 0xB6, 0xFE, 0xE6, 0xFF, 0x30, 0x24, 0x65, 0x61, 0x75, 0xC3, 0xDE, 0xEB, 0xF2, 0xF9,
|
||||||
|
0x43, 0x61, 0x65, 0x6F, 0xFE, 0x8C, 0xFE, 0x8C, 0xFE, 0x8C, 0x42, 0x61, 0x75, 0xFE, 0xC6, 0xFE,
|
||||||
|
0xC6, 0x44, 0x75, 0x61, 0x65, 0x6F, 0xFE, 0xBF, 0xFE, 0xEF, 0xFE, 0xEF, 0xFE, 0xEF, 0x41, 0xB6,
|
||||||
|
0xFE, 0xB2, 0x21, 0xC3, 0xFC, 0x42, 0xA4, 0xB6, 0xFE, 0xB1, 0xFF, 0xFD, 0x43, 0x61, 0x6F, 0x79,
|
||||||
|
0xFE, 0xD4, 0xFE, 0xD4, 0xFE, 0xD4, 0x42, 0x61, 0x65, 0xFE, 0x56, 0xFE, 0x56, 0x26, 0x69, 0x61,
|
||||||
|
0x75, 0xC3, 0x65, 0x6F, 0xC3, 0xCD, 0xD4, 0xE8, 0xEF, 0xF9, 0xA0, 0x01, 0x11, 0x21, 0xA4, 0xFD,
|
||||||
|
0xA0, 0x01, 0xE2, 0x21, 0x6E, 0xFD, 0x21, 0x69, 0xFD, 0x21, 0x64, 0xFD, 0xA0, 0x02, 0x03, 0x21,
|
||||||
|
0x61, 0xFD, 0x21, 0x72, 0xFD, 0x21, 0x75, 0xFD, 0x21, 0x75, 0xFD, 0x23, 0xC3, 0x79, 0x73, 0xE2,
|
||||||
|
0xEE, 0xFD, 0x41, 0x72, 0xFD, 0xD9, 0x42, 0x6C, 0x68, 0xFC, 0x47, 0xFF, 0xFC, 0xC1, 0x00, 0x81,
|
||||||
|
0x69, 0xFB, 0xA6, 0x21, 0x76, 0xFA, 0x59, 0x62, 0x64, 0x66, 0x67, 0x68, 0x6A, 0x6B, 0x6C, 0x6D,
|
||||||
|
0x6E, 0x70, 0x72, 0x73, 0x74, 0x76, 0xC3, 0x79, 0x6F, 0x75, 0x61, 0x65, 0x69, 0x2E, 0x63, 0x71,
|
||||||
|
0xFB, 0xAE, 0xFB, 0xC9, 0xFB, 0xE1, 0xFB, 0xFA, 0xFC, 0x16, 0xFC, 0x16, 0xFC, 0x4A, 0xFC, 0x6E,
|
||||||
|
0xFC, 0x16, 0xFC, 0xE8, 0xFD, 0x0C, 0xFD, 0x2B, 0xFD, 0xCB, 0xFD, 0xEA, 0xFC, 0x16, 0xFE, 0x42,
|
||||||
|
0xFE, 0x65, 0xFE, 0x8F, 0xFE, 0xC7, 0xFF, 0x36, 0xFF, 0x71, 0xFF, 0xB7, 0xFF, 0xE5, 0xFF, 0xF0,
|
||||||
|
0xFF, 0xFD,
|
||||||
|
};
|
||||||
|
|
||||||
|
constexpr SerializedHyphenationPatterns fi_patterns = {
|
||||||
|
0x496u,
|
||||||
|
fi_trie_data,
|
||||||
|
sizeof(fi_trie_data),
|
||||||
|
};
|
||||||
@@ -34,7 +34,8 @@ constexpr const char* BLOCK_TAGS[] = {"p", "li", "div", "br", "blockquote"};
|
|||||||
constexpr const char* BOLD_TAGS[] = {"b", "strong"};
|
constexpr const char* BOLD_TAGS[] = {"b", "strong"};
|
||||||
constexpr const char* ITALIC_TAGS[] = {"i", "em"};
|
constexpr const char* ITALIC_TAGS[] = {"i", "em"};
|
||||||
constexpr const char* UNDERLINE_TAGS[] = {"u", "ins"};
|
constexpr const char* UNDERLINE_TAGS[] = {"u", "ins"};
|
||||||
constexpr const char* IMAGE_TAGS[] = {"img"};
|
constexpr const char* LINETHROUGH_TAGS[] = {"del", "s", "strike"};
|
||||||
|
constexpr const char* IMAGE_TAGS[] = {"img", "image"};
|
||||||
constexpr const char* SKIP_TAGS[] = {"head"};
|
constexpr const char* SKIP_TAGS[] = {"head"};
|
||||||
|
|
||||||
bool isWhitespace(const char c) { return c == ' ' || c == '\r' || c == '\n' || c == '\t'; }
|
bool isWhitespace(const char c) { return c == ' ' || c == '\r' || c == '\n' || c == '\t'; }
|
||||||
@@ -89,13 +90,50 @@ void ChapterHtmlSlimParser::applyDirectionToEntry(StyleStackEntry& entry, const
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update effective bold/italic/underline based on block style and inline style stack
|
EpdFontFamily::Style ChapterHtmlSlimParser::fontStyleForTextDecoration(const CssTextDecoration decoration) {
|
||||||
|
EpdFontFamily::Style style = EpdFontFamily::REGULAR;
|
||||||
|
if ((decoration & CssTextDecoration::Underline) != CssTextDecoration::None) {
|
||||||
|
style = static_cast<EpdFontFamily::Style>(style | EpdFontFamily::UNDERLINE);
|
||||||
|
}
|
||||||
|
if ((decoration & CssTextDecoration::LineThrough) != CssTextDecoration::None) {
|
||||||
|
style = static_cast<EpdFontFamily::Style>(style | EpdFontFamily::STRIKETHROUGH);
|
||||||
|
}
|
||||||
|
return style;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChapterHtmlSlimParser::applyTextDecorationToEntry(StyleStackEntry& entry, const CssStyle& css) {
|
||||||
|
if (css.hasTextDecoration()) {
|
||||||
|
entry.hasTextDecoration = true;
|
||||||
|
entry.textDecoration = css.textDecoration;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChapterHtmlSlimParser::pushDecorationStyleEntry(const CssTextDecoration defaultDecoration,
|
||||||
|
const CssStyle& cssStyle) {
|
||||||
|
StyleStackEntry entry;
|
||||||
|
entry.depth = depth;
|
||||||
|
entry.hasTextDecoration = true;
|
||||||
|
entry.textDecoration = cssStyle.hasTextDecoration() ? cssStyle.textDecoration : defaultDecoration;
|
||||||
|
if (cssStyle.hasFontWeight()) {
|
||||||
|
entry.hasBold = true;
|
||||||
|
entry.bold = cssStyle.fontWeight == CssFontWeight::Bold;
|
||||||
|
}
|
||||||
|
if (cssStyle.hasFontStyle()) {
|
||||||
|
entry.hasItalic = true;
|
||||||
|
entry.italic = cssStyle.fontStyle == CssFontStyle::Italic;
|
||||||
|
}
|
||||||
|
applyDirectionToEntry(entry, cssStyle);
|
||||||
|
inlineStyleStack.push_back(entry);
|
||||||
|
updateEffectiveInlineStyle();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update effective bold/italic/decorations based on block style and inline style stack
|
||||||
void ChapterHtmlSlimParser::updateEffectiveInlineStyle() {
|
void ChapterHtmlSlimParser::updateEffectiveInlineStyle() {
|
||||||
// Start with block-level styles
|
// Start with block-level styles
|
||||||
effectiveBold = currentCssStyle.hasFontWeight() && currentCssStyle.fontWeight == CssFontWeight::Bold;
|
effectiveBold = currentCssStyle.hasFontWeight() && currentCssStyle.fontWeight == CssFontWeight::Bold;
|
||||||
effectiveItalic = currentCssStyle.hasFontStyle() && currentCssStyle.fontStyle == CssFontStyle::Italic;
|
effectiveItalic = currentCssStyle.hasFontStyle() && currentCssStyle.fontStyle == CssFontStyle::Italic;
|
||||||
effectiveUnderline =
|
effectiveTextDecoration =
|
||||||
currentCssStyle.hasTextDecoration() && currentCssStyle.textDecoration == CssTextDecoration::Underline;
|
currentCssStyle.hasTextDecoration() ? currentCssStyle.textDecoration : CssTextDecoration::None;
|
||||||
effectiveDirectionDefined = currentCssStyle.hasDirection();
|
effectiveDirectionDefined = currentCssStyle.hasDirection();
|
||||||
effectiveDirection = currentCssStyle.direction;
|
effectiveDirection = currentCssStyle.direction;
|
||||||
effectiveSup = false;
|
effectiveSup = false;
|
||||||
@@ -109,8 +147,10 @@ void ChapterHtmlSlimParser::updateEffectiveInlineStyle() {
|
|||||||
if (entry.hasItalic) {
|
if (entry.hasItalic) {
|
||||||
effectiveItalic = entry.italic;
|
effectiveItalic = entry.italic;
|
||||||
}
|
}
|
||||||
if (entry.hasUnderline) {
|
// CSS line decorations propagate through descendants; child entries add
|
||||||
effectiveUnderline = entry.underline;
|
// their own lines but cannot cancel an ancestor's already active line.
|
||||||
|
if (entry.hasTextDecoration) {
|
||||||
|
effectiveTextDecoration = effectiveTextDecoration | entry.textDecoration;
|
||||||
}
|
}
|
||||||
if (entry.hasDirection) {
|
if (entry.hasDirection) {
|
||||||
effectiveDirectionDefined = true;
|
effectiveDirectionDefined = true;
|
||||||
@@ -164,7 +204,6 @@ void ChapterHtmlSlimParser::flushPartWordBuffer() {
|
|||||||
// Determine font style from depth-based tracking and CSS effective style
|
// Determine font style from depth-based tracking and CSS effective style
|
||||||
const bool isBold = boldUntilDepth < depth || effectiveBold;
|
const bool isBold = boldUntilDepth < depth || effectiveBold;
|
||||||
const bool isItalic = italicUntilDepth < depth || effectiveItalic;
|
const bool isItalic = italicUntilDepth < depth || effectiveItalic;
|
||||||
const bool isUnderline = underlineUntilDepth < depth || effectiveUnderline;
|
|
||||||
|
|
||||||
// Combine style flags using bitwise OR
|
// Combine style flags using bitwise OR
|
||||||
EpdFontFamily::Style fontStyle = EpdFontFamily::REGULAR;
|
EpdFontFamily::Style fontStyle = EpdFontFamily::REGULAR;
|
||||||
@@ -174,9 +213,7 @@ void ChapterHtmlSlimParser::flushPartWordBuffer() {
|
|||||||
if (isItalic) {
|
if (isItalic) {
|
||||||
fontStyle = static_cast<EpdFontFamily::Style>(fontStyle | EpdFontFamily::ITALIC);
|
fontStyle = static_cast<EpdFontFamily::Style>(fontStyle | EpdFontFamily::ITALIC);
|
||||||
}
|
}
|
||||||
if (isUnderline) {
|
fontStyle = static_cast<EpdFontFamily::Style>(fontStyle | fontStyleForTextDecoration(effectiveTextDecoration));
|
||||||
fontStyle = static_cast<EpdFontFamily::Style>(fontStyle | EpdFontFamily::UNDERLINE);
|
|
||||||
}
|
|
||||||
if (effectiveSup) {
|
if (effectiveSup) {
|
||||||
fontStyle = static_cast<EpdFontFamily::Style>(fontStyle | EpdFontFamily::SUP);
|
fontStyle = static_cast<EpdFontFamily::Style>(fontStyle | EpdFontFamily::SUP);
|
||||||
} else if (effectiveSub) {
|
} else if (effectiveSub) {
|
||||||
@@ -188,6 +225,7 @@ void ChapterHtmlSlimParser::flushPartWordBuffer() {
|
|||||||
currentTextBlock->addWord(partWordBuffer, fontStyle, false, nextWordContinues);
|
currentTextBlock->addWord(partWordBuffer, fontStyle, false, nextWordContinues);
|
||||||
partWordBufferIndex = 0;
|
partWordBufferIndex = 0;
|
||||||
nextWordContinues = false;
|
nextWordContinues = false;
|
||||||
|
listItemBulletOnly = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// start a new text block if needed
|
// start a new text block if needed
|
||||||
@@ -202,8 +240,28 @@ void ChapterHtmlSlimParser::startNewTextBlock(const BlockStyle& blockStyle) {
|
|||||||
// open. Merge those into the new style so the first child in a container inherits
|
// open. Merge those into the new style so the first child in a container inherits
|
||||||
// the container's vertical spacing.
|
// the container's vertical spacing.
|
||||||
const auto style = currentTextBlock->getBlockStyle();
|
const auto style = currentTextBlock->getBlockStyle();
|
||||||
currentTextBlock->setBlockStyle(style.getCombinedBlockStyle(blockStyle, BlockStyle::CombineAxis::Vertical));
|
BlockStyle incoming = blockStyle;
|
||||||
|
if (style.fromBrElement) {
|
||||||
|
// The empty block was created by a <br> section separator. Inject a full line of
|
||||||
|
// blank space before the following paragraph so the scene/section break is visible.
|
||||||
|
// This only fires when the <br> block stayed empty (i.e. no inline text was added).
|
||||||
|
const int16_t lineHeight = static_cast<int16_t>(renderer.getLineHeight(fontId) * lineCompression + 0.5f);
|
||||||
|
incoming.marginTop = static_cast<int16_t>(incoming.marginTop + lineHeight);
|
||||||
|
}
|
||||||
|
|
||||||
|
currentTextBlock->setBlockStyle(style.getCombinedBlockStyle(incoming, BlockStyle::CombineAxis::Vertical));
|
||||||
|
|
||||||
|
flushPendingAnchor();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// <li> added a bullet as the first word, making the block non-empty. When a nested
|
||||||
|
// block-level child (<p>, <div>, etc.) opens, reuse the block instead of flushing
|
||||||
|
// the bullet to its own line. The bullet stays inline with the child's text.
|
||||||
|
if (listItemBulletOnly) {
|
||||||
|
const auto style = currentTextBlock->getBlockStyle();
|
||||||
|
currentTextBlock->setBlockStyle(style.getCombinedBlockStyle(blockStyle, BlockStyle::CombineAxis::Vertical));
|
||||||
|
listItemBulletOnly = false;
|
||||||
flushPendingAnchor();
|
flushPendingAnchor();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -215,6 +273,7 @@ void ChapterHtmlSlimParser::startNewTextBlock(const BlockStyle& blockStyle) {
|
|||||||
flushPendingAnchor();
|
flushPendingAnchor();
|
||||||
currentTextBlock.reset(new ParsedText(extraParagraphSpacing, hyphenationEnabled, focusReadingEnabled, blockStyle));
|
currentTextBlock.reset(new ParsedText(extraParagraphSpacing, hyphenationEnabled, focusReadingEnabled, blockStyle));
|
||||||
wordsExtractedInBlock = 0;
|
wordsExtractedInBlock = 0;
|
||||||
|
listItemBulletOnly = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
void ChapterHtmlSlimParser::emitHorizontalRule(const BlockStyle& blockStyle) {
|
void ChapterHtmlSlimParser::emitHorizontalRule(const BlockStyle& blockStyle) {
|
||||||
@@ -420,14 +479,15 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char*
|
|||||||
headerStyle.bold = false;
|
headerStyle.bold = false;
|
||||||
headerStyle.hasItalic = true;
|
headerStyle.hasItalic = true;
|
||||||
headerStyle.italic = true;
|
headerStyle.italic = true;
|
||||||
headerStyle.hasUnderline = true;
|
|
||||||
headerStyle.underline = false;
|
|
||||||
self->inlineStyleStack.push_back(headerStyle);
|
self->inlineStyleStack.push_back(headerStyle);
|
||||||
self->updateEffectiveInlineStyle();
|
self->updateEffectiveInlineStyle();
|
||||||
|
const CssTextDecoration savedTextDecoration = self->effectiveTextDecoration;
|
||||||
|
self->effectiveTextDecoration = CssTextDecoration::None;
|
||||||
self->characterData(userData, headerText.c_str(), static_cast<int>(headerText.length()));
|
self->characterData(userData, headerText.c_str(), static_cast<int>(headerText.length()));
|
||||||
if (self->partWordBufferIndex > 0) {
|
if (self->partWordBufferIndex > 0) {
|
||||||
self->flushPartWordBuffer();
|
self->flushPartWordBuffer();
|
||||||
}
|
}
|
||||||
|
self->effectiveTextDecoration = savedTextDecoration;
|
||||||
self->nextWordContinues = false;
|
self->nextWordContinues = false;
|
||||||
self->inlineStyleStack.pop_back();
|
self->inlineStyleStack.pop_back();
|
||||||
self->updateEffectiveInlineStyle();
|
self->updateEffectiveInlineStyle();
|
||||||
@@ -448,11 +508,18 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char*
|
|||||||
for (int i = 0; atts[i]; i += 2) {
|
for (int i = 0; atts[i]; i += 2) {
|
||||||
if (strcmp(atts[i], "src") == 0) {
|
if (strcmp(atts[i], "src") == 0) {
|
||||||
src = atts[i + 1];
|
src = atts[i + 1];
|
||||||
|
} else if (src.empty() && (strcmp(atts[i], "href") == 0 || strcmp(atts[i], "xlink:href") == 0)) {
|
||||||
|
src = atts[i + 1];
|
||||||
} else if (strcmp(atts[i], "alt") == 0) {
|
} else if (strcmp(atts[i], "alt") == 0) {
|
||||||
alt = atts[i + 1];
|
alt = atts[i + 1];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const size_t fragmentPos = src.find('#');
|
||||||
|
if (fragmentPos != std::string::npos) {
|
||||||
|
src.resize(fragmentPos);
|
||||||
|
}
|
||||||
|
|
||||||
// imageRendering: 0=display, 1=placeholder (alt text only), 2=suppress entirely
|
// imageRendering: 0=display, 1=placeholder (alt text only), 2=suppress entirely
|
||||||
if (self->imageRendering == 2) {
|
if (self->imageRendering == 2) {
|
||||||
self->skipUntilDepth = self->depth;
|
self->skipUntilDepth = self->depth;
|
||||||
@@ -460,19 +527,6 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char*
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Skip image if CSS display:none
|
|
||||||
if (self->cssParser) {
|
|
||||||
CssStyle imgDisplayStyle = self->cssParser->resolveStyle("img", classAttr);
|
|
||||||
if (!styleAttr.empty()) {
|
|
||||||
imgDisplayStyle.applyOver(CssParser::parseInlineStyle(styleAttr));
|
|
||||||
}
|
|
||||||
if (imgDisplayStyle.hasDisplay() && imgDisplayStyle.display == CssDisplay::None) {
|
|
||||||
self->skipUntilDepth = self->depth;
|
|
||||||
self->depth += 1;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!src.empty() && self->imageRendering != 1) {
|
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());
|
||||||
|
|
||||||
@@ -496,24 +550,28 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char*
|
|||||||
extractSuccess = self->epub->readItemContentsToStream(resolvedPath, cachedImageFile, 4096);
|
extractSuccess = self->epub->readItemContentsToStream(resolvedPath, cachedImageFile, 4096);
|
||||||
cachedImageFile.flush();
|
cachedImageFile.flush();
|
||||||
cachedImageFile.close();
|
cachedImageFile.close();
|
||||||
delay(50); // Give SD card time to sync
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (extractSuccess) {
|
if (extractSuccess) {
|
||||||
// Get image dimensions
|
// Get image dimensions, retrying to absorb SD-card sync latency on slow
|
||||||
|
// cards. Replaces a blanket delay(50) that cost ~50ms on every image, and
|
||||||
|
// closes the silent-drop bug where a single getDimensions failure was fatal.
|
||||||
ImageDimensions dims = {0, 0};
|
ImageDimensions dims = {0, 0};
|
||||||
ImageToFramebufferDecoder* decoder = ImageDecoderFactory::getDecoder(cachedImagePath);
|
ImageToFramebufferDecoder* decoder = ImageDecoderFactory::getDecoder(cachedImagePath);
|
||||||
if (decoder && decoder->getDimensions(cachedImagePath, dims)) {
|
bool gotDimensions = false;
|
||||||
|
for (int attempt = 0; attempt < 3 && !gotDimensions; attempt++) {
|
||||||
|
if (attempt > 0) {
|
||||||
|
delay(50); // Give a slow SD card time to finish syncing before retrying
|
||||||
|
}
|
||||||
|
gotDimensions = decoder && decoder->getDimensions(cachedImagePath, dims);
|
||||||
|
}
|
||||||
|
if (gotDimensions) {
|
||||||
LOG_DBG("EHP", "Image dimensions: %dx%d", dims.width, dims.height);
|
LOG_DBG("EHP", "Image dimensions: %dx%d", dims.width, dims.height);
|
||||||
|
|
||||||
int displayWidth = 0;
|
int displayWidth = 0;
|
||||||
int displayHeight = 0;
|
int displayHeight = 0;
|
||||||
const float emSize = static_cast<float>(self->renderer.getFontAscenderSize(self->fontId));
|
const float emSize = static_cast<float>(self->renderer.getFontAscenderSize(self->fontId));
|
||||||
CssStyle imgStyle = self->cssParser ? self->cssParser->resolveStyle("img", classAttr) : CssStyle{};
|
const CssStyle& imgStyle = cssStyle;
|
||||||
// Merge inline style (e.g. style="height: 2em") so it overrides stylesheet rules
|
|
||||||
if (!styleAttr.empty()) {
|
|
||||||
imgStyle.applyOver(CssParser::parseInlineStyle(styleAttr));
|
|
||||||
}
|
|
||||||
const bool hasCssHeight = imgStyle.hasImageHeight();
|
const bool hasCssHeight = imgStyle.hasImageHeight();
|
||||||
const bool hasCssWidth = imgStyle.hasImageWidth();
|
const bool hasCssWidth = imgStyle.hasImageWidth();
|
||||||
|
|
||||||
@@ -760,12 +818,11 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char*
|
|||||||
self->currentFootnote.number[0] = '\0';
|
self->currentFootnote.number[0] = '\0';
|
||||||
self->currentFootnoteLinkTextLen = 0;
|
self->currentFootnoteLinkTextLen = 0;
|
||||||
|
|
||||||
// Apply underline style to visually indicate the link
|
// Apply underline style to visually indicate the link.
|
||||||
self->underlineUntilDepth = std::min(self->underlineUntilDepth, self->depth);
|
|
||||||
StyleStackEntry entry;
|
StyleStackEntry entry;
|
||||||
entry.depth = self->depth;
|
entry.depth = self->depth;
|
||||||
entry.hasUnderline = true;
|
entry.hasTextDecoration = true;
|
||||||
entry.underline = true;
|
entry.textDecoration = CssTextDecoration::Underline;
|
||||||
applyDirectionToEntry(entry, cssStyle);
|
applyDirectionToEntry(entry, cssStyle);
|
||||||
self->inlineStyleStack.push_back(entry);
|
self->inlineStyleStack.push_back(entry);
|
||||||
self->updateEffectiveInlineStyle();
|
self->updateEffectiveInlineStyle();
|
||||||
@@ -818,7 +875,14 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char*
|
|||||||
// flush word preceding <br/> to currentTextBlock before calling startNewTextBlock
|
// flush word preceding <br/> to currentTextBlock before calling startNewTextBlock
|
||||||
self->flushPartWordBuffer();
|
self->flushPartWordBuffer();
|
||||||
}
|
}
|
||||||
self->startNewTextBlock(self->blockStyleStack.back().withoutBottom());
|
// Tag the new block so startNewTextBlock can inject a full line-height gap if
|
||||||
|
// the block remains empty (i.e. <br> is a section separator between paragraphs).
|
||||||
|
// If the block gets text added before the next block opens it becomes non-empty,
|
||||||
|
// goes through makePages() normally, and the flag has no effect (inline <br> case).
|
||||||
|
BlockStyle brStyle =
|
||||||
|
self->currentTextBlock ? self->currentTextBlock->getBlockStyle() : self->blockStyleStack.back();
|
||||||
|
brStyle.fromBrElement = true;
|
||||||
|
self->startNewTextBlock(brStyle);
|
||||||
} else {
|
} else {
|
||||||
self->currentCssStyle = cssStyle;
|
self->currentCssStyle = cssStyle;
|
||||||
const auto accumulated = self->blockStyleStack.back().getCombinedBlockStyle(userAlignmentBlockStyle,
|
const auto accumulated = self->blockStyleStack.back().getCombinedBlockStyle(userAlignmentBlockStyle,
|
||||||
@@ -829,6 +893,7 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char*
|
|||||||
|
|
||||||
if (strcmp(name, "li") == 0) {
|
if (strcmp(name, "li") == 0) {
|
||||||
self->currentTextBlock->addWord("\xe2\x80\xa2", EpdFontFamily::REGULAR);
|
self->currentTextBlock->addWord("\xe2\x80\xa2", EpdFontFamily::REGULAR);
|
||||||
|
self->listItemBulletOnly = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (matches(name, UNDERLINE_TAGS, std::size(UNDERLINE_TAGS))) {
|
} else if (matches(name, UNDERLINE_TAGS, std::size(UNDERLINE_TAGS))) {
|
||||||
@@ -837,23 +902,14 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char*
|
|||||||
self->flushPartWordBuffer();
|
self->flushPartWordBuffer();
|
||||||
self->nextWordContinues = true;
|
self->nextWordContinues = true;
|
||||||
}
|
}
|
||||||
self->underlineUntilDepth = std::min(self->underlineUntilDepth, self->depth);
|
self->pushDecorationStyleEntry(CssTextDecoration::Underline, cssStyle);
|
||||||
// Push inline style entry for underline tag
|
} else if (matches(name, LINETHROUGH_TAGS, std::size(LINETHROUGH_TAGS))) {
|
||||||
StyleStackEntry entry;
|
// Flush buffer before style change so preceding text gets current style
|
||||||
entry.depth = self->depth; // Track depth for matching pop
|
if (self->partWordBufferIndex > 0) {
|
||||||
entry.hasUnderline = true;
|
self->flushPartWordBuffer();
|
||||||
entry.underline = true;
|
self->nextWordContinues = true;
|
||||||
if (cssStyle.hasFontWeight()) {
|
|
||||||
entry.hasBold = true;
|
|
||||||
entry.bold = cssStyle.fontWeight == CssFontWeight::Bold;
|
|
||||||
}
|
}
|
||||||
if (cssStyle.hasFontStyle()) {
|
self->pushDecorationStyleEntry(CssTextDecoration::LineThrough, cssStyle);
|
||||||
entry.hasItalic = true;
|
|
||||||
entry.italic = cssStyle.fontStyle == CssFontStyle::Italic;
|
|
||||||
}
|
|
||||||
applyDirectionToEntry(entry, cssStyle);
|
|
||||||
self->inlineStyleStack.push_back(entry);
|
|
||||||
self->updateEffectiveInlineStyle();
|
|
||||||
} else if (matches(name, BOLD_TAGS, std::size(BOLD_TAGS))) {
|
} else if (matches(name, BOLD_TAGS, std::size(BOLD_TAGS))) {
|
||||||
// Flush buffer before style change so preceding text gets current style
|
// Flush buffer before style change so preceding text gets current style
|
||||||
if (self->partWordBufferIndex > 0) {
|
if (self->partWordBufferIndex > 0) {
|
||||||
@@ -870,10 +926,7 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char*
|
|||||||
entry.hasItalic = true;
|
entry.hasItalic = true;
|
||||||
entry.italic = cssStyle.fontStyle == CssFontStyle::Italic;
|
entry.italic = cssStyle.fontStyle == CssFontStyle::Italic;
|
||||||
}
|
}
|
||||||
if (cssStyle.hasTextDecoration()) {
|
applyTextDecorationToEntry(entry, cssStyle);
|
||||||
entry.hasUnderline = true;
|
|
||||||
entry.underline = cssStyle.textDecoration == CssTextDecoration::Underline;
|
|
||||||
}
|
|
||||||
applyDirectionToEntry(entry, cssStyle);
|
applyDirectionToEntry(entry, cssStyle);
|
||||||
self->inlineStyleStack.push_back(entry);
|
self->inlineStyleStack.push_back(entry);
|
||||||
self->updateEffectiveInlineStyle();
|
self->updateEffectiveInlineStyle();
|
||||||
@@ -893,10 +946,7 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char*
|
|||||||
entry.hasBold = true;
|
entry.hasBold = true;
|
||||||
entry.bold = cssStyle.fontWeight == CssFontWeight::Bold;
|
entry.bold = cssStyle.fontWeight == CssFontWeight::Bold;
|
||||||
}
|
}
|
||||||
if (cssStyle.hasTextDecoration()) {
|
applyTextDecorationToEntry(entry, cssStyle);
|
||||||
entry.hasUnderline = true;
|
|
||||||
entry.underline = cssStyle.textDecoration == CssTextDecoration::Underline;
|
|
||||||
}
|
|
||||||
applyDirectionToEntry(entry, cssStyle);
|
applyDirectionToEntry(entry, cssStyle);
|
||||||
self->inlineStyleStack.push_back(entry);
|
self->inlineStyleStack.push_back(entry);
|
||||||
self->updateEffectiveInlineStyle();
|
self->updateEffectiveInlineStyle();
|
||||||
@@ -935,10 +985,7 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char*
|
|||||||
entry.hasItalic = true;
|
entry.hasItalic = true;
|
||||||
entry.italic = cssStyle.fontStyle == CssFontStyle::Italic;
|
entry.italic = cssStyle.fontStyle == CssFontStyle::Italic;
|
||||||
}
|
}
|
||||||
if (cssStyle.hasTextDecoration()) {
|
applyTextDecorationToEntry(entry, cssStyle);
|
||||||
entry.hasUnderline = true;
|
|
||||||
entry.underline = cssStyle.textDecoration == CssTextDecoration::Underline;
|
|
||||||
}
|
|
||||||
applyDirectionToEntry(entry, cssStyle);
|
applyDirectionToEntry(entry, cssStyle);
|
||||||
if (cssStyle.hasVerticalAlign()) {
|
if (cssStyle.hasVerticalAlign()) {
|
||||||
if (cssStyle.verticalAlign == CssVerticalAlign::Super) {
|
if (cssStyle.verticalAlign == CssVerticalAlign::Super) {
|
||||||
@@ -1150,9 +1197,8 @@ void XMLCALL ChapterHtmlSlimParser::endElement(void* userData, const XML_Char* n
|
|||||||
!self->inlineStyleStack.empty() && self->inlineStyleStack.back().depth == self->depth - 1;
|
!self->inlineStyleStack.empty() && self->inlineStyleStack.back().depth == self->depth - 1;
|
||||||
const bool willClearBold = self->boldUntilDepth == self->depth - 1;
|
const bool willClearBold = self->boldUntilDepth == self->depth - 1;
|
||||||
const bool willClearItalic = self->italicUntilDepth == self->depth - 1;
|
const bool willClearItalic = self->italicUntilDepth == self->depth - 1;
|
||||||
const bool willClearUnderline = self->underlineUntilDepth == self->depth - 1;
|
|
||||||
|
|
||||||
const bool styleWillChange = willPopStyleStack || willClearBold || willClearItalic || willClearUnderline;
|
const bool styleWillChange = willPopStyleStack || willClearBold || willClearItalic;
|
||||||
const bool headerOrBlockTag = isHeaderOrBlock(name);
|
const bool headerOrBlockTag = isHeaderOrBlock(name);
|
||||||
const bool tableStructuralTag = isTableStructuralTag(name);
|
const bool tableStructuralTag = isTableStructuralTag(name);
|
||||||
|
|
||||||
@@ -1171,7 +1217,8 @@ void XMLCALL ChapterHtmlSlimParser::endElement(void* userData, const XML_Char* n
|
|||||||
!matches(name, IMAGE_TAGS, std::size(IMAGE_TAGS)) && self->depth != 1;
|
!matches(name, IMAGE_TAGS, std::size(IMAGE_TAGS)) && self->depth != 1;
|
||||||
const bool shouldFlush = styleWillChange || headerOrBlockTag || matches(name, BOLD_TAGS, std::size(BOLD_TAGS)) ||
|
const bool shouldFlush = styleWillChange || headerOrBlockTag || matches(name, BOLD_TAGS, std::size(BOLD_TAGS)) ||
|
||||||
matches(name, ITALIC_TAGS, std::size(ITALIC_TAGS)) ||
|
matches(name, ITALIC_TAGS, std::size(ITALIC_TAGS)) ||
|
||||||
matches(name, UNDERLINE_TAGS, std::size(UNDERLINE_TAGS)) || tableStructuralTag ||
|
matches(name, UNDERLINE_TAGS, std::size(UNDERLINE_TAGS)) ||
|
||||||
|
matches(name, LINETHROUGH_TAGS, std::size(LINETHROUGH_TAGS)) || tableStructuralTag ||
|
||||||
matches(name, IMAGE_TAGS, std::size(IMAGE_TAGS)) || self->depth == 1;
|
matches(name, IMAGE_TAGS, std::size(IMAGE_TAGS)) || self->depth == 1;
|
||||||
|
|
||||||
if (shouldFlush) {
|
if (shouldFlush) {
|
||||||
@@ -1230,11 +1277,6 @@ void XMLCALL ChapterHtmlSlimParser::endElement(void* userData, const XML_Char* n
|
|||||||
self->italicUntilDepth = INT_MAX;
|
self->italicUntilDepth = INT_MAX;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Leaving underline tag
|
|
||||||
if (self->underlineUntilDepth == self->depth) {
|
|
||||||
self->underlineUntilDepth = INT_MAX;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Pop from inline style stack if we pushed an entry at this depth
|
// Pop from inline style stack if we pushed an entry at this depth
|
||||||
// This handles all inline elements: b, i, u, span, etc.
|
// This handles all inline elements: b, i, u, span, etc.
|
||||||
if (!self->inlineStyleStack.empty() && self->inlineStyleStack.back().depth == self->depth) {
|
if (!self->inlineStyleStack.empty() && self->inlineStyleStack.back().depth == self->depth) {
|
||||||
@@ -1258,10 +1300,19 @@ void XMLCALL ChapterHtmlSlimParser::endElement(void* userData, const XML_Char* n
|
|||||||
}
|
}
|
||||||
self->blockStyleStack.pop_back();
|
self->blockStyleStack.pop_back();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// </li> closes: if the bullet never got inline text (empty <li> or <li> with only
|
||||||
|
// block children that were flushed), clear the flag so the next sibling doesn't
|
||||||
|
// merge into this block.
|
||||||
|
if (strcmp(name, "li") == 0) {
|
||||||
|
self->listItemBulletOnly = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bool ChapterHtmlSlimParser::parseAndBuildPages() {
|
ChapterHtmlSlimParser::~ChapterHtmlSlimParser() { abortParse(); }
|
||||||
|
|
||||||
|
bool ChapterHtmlSlimParser::beginParse() {
|
||||||
// Initialize block style stack with a root entry representing "no ancestor block elements".
|
// Initialize block style stack with a root entry representing "no ancestor block elements".
|
||||||
// The user's paragraph alignment is set as the default so child elements without explicit
|
// The user's paragraph alignment is set as the default so child elements without explicit
|
||||||
// text-align inherit it correctly through getCombinedBlockStyle.
|
// text-align inherit it correctly through getCombinedBlockStyle.
|
||||||
@@ -1279,67 +1330,78 @@ bool ChapterHtmlSlimParser::parseAndBuildPages() {
|
|||||||
paragraphAlignmentBlockStyle.alignment = align;
|
paragraphAlignmentBlockStyle.alignment = align;
|
||||||
startNewTextBlock(paragraphAlignmentBlockStyle);
|
startNewTextBlock(paragraphAlignmentBlockStyle);
|
||||||
|
|
||||||
XML_Parser parser = XML_ParserCreate(nullptr);
|
xmlParser_ = XML_ParserCreate(nullptr);
|
||||||
int done;
|
if (!xmlParser_) {
|
||||||
|
|
||||||
if (!parser) {
|
|
||||||
LOG_ERR("EHP", "Couldn't allocate memory for parser");
|
LOG_ERR("EHP", "Couldn't allocate memory for parser");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle HTML entities (like ) that aren't in XML spec or DTD
|
// Handle HTML entities (like ) that aren't in XML spec or DTD
|
||||||
// Using DefaultHandlerExpand preserves normal entity expansion from DOCTYPE
|
// Using DefaultHandlerExpand preserves normal entity expansion from DOCTYPE
|
||||||
XML_SetDefaultHandlerExpand(parser, defaultHandlerExpand);
|
XML_SetDefaultHandlerExpand(xmlParser_, defaultHandlerExpand);
|
||||||
|
|
||||||
HalFile file;
|
if (!Storage.openFileForRead("EHP", filepath, parseFile_)) {
|
||||||
if (!Storage.openFileForRead("EHP", filepath, file)) {
|
destroyXmlParser(xmlParser_);
|
||||||
destroyXmlParser(parser);
|
xmlParser_ = nullptr;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get file size to decide whether to show indexing popup.
|
// Get file size to decide whether to show indexing popup.
|
||||||
if (popupFn && file.size() >= MIN_SIZE_FOR_POPUP) {
|
if (popupFn && parseFile_.size() >= MIN_SIZE_FOR_POPUP) {
|
||||||
popupFn();
|
popupFn();
|
||||||
}
|
}
|
||||||
|
|
||||||
XML_SetUserData(parser, this);
|
XML_SetUserData(xmlParser_, this);
|
||||||
XML_SetElementHandler(parser, startElement, endElement);
|
XML_SetElementHandler(xmlParser_, startElement, endElement);
|
||||||
XML_SetCharacterDataHandler(parser, characterData);
|
XML_SetCharacterDataHandler(xmlParser_, characterData);
|
||||||
|
|
||||||
// Compute the time taken to parse and build pages
|
parseStartTime_ = millis();
|
||||||
const uint32_t chapterStartTime = millis();
|
return true;
|
||||||
do {
|
}
|
||||||
void* const buf = XML_GetBuffer(parser, PARSE_BUFFER_SIZE);
|
|
||||||
if (!buf) {
|
|
||||||
LOG_ERR("EHP", "Couldn't allocate memory for buffer");
|
|
||||||
destroyXmlParser(parser);
|
|
||||||
file.close();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const size_t len = file.read(buf, PARSE_BUFFER_SIZE);
|
ChapterHtmlSlimParser::ParseStatus ChapterHtmlSlimParser::parseStep() {
|
||||||
|
void* const buf = XML_GetBuffer(xmlParser_, PARSE_BUFFER_SIZE);
|
||||||
|
if (!buf) {
|
||||||
|
LOG_ERR("EHP", "Couldn't allocate memory for buffer");
|
||||||
|
return ParseStatus::Error;
|
||||||
|
}
|
||||||
|
|
||||||
if (len == 0 && file.available() > 0) {
|
const size_t len = parseFile_.read(buf, PARSE_BUFFER_SIZE);
|
||||||
LOG_ERR("EHP", "File read error");
|
|
||||||
destroyXmlParser(parser);
|
|
||||||
file.close();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
done = file.available() == 0;
|
if (len == 0 && parseFile_.available() > 0) {
|
||||||
|
LOG_ERR("EHP", "File read error");
|
||||||
|
return ParseStatus::Error;
|
||||||
|
}
|
||||||
|
|
||||||
if (XML_ParseBuffer(parser, static_cast<int>(len), done) == XML_STATUS_ERROR) {
|
const int done = parseFile_.available() == 0;
|
||||||
LOG_ERR("EHP", "Parse error at line %lu:\n%s", XML_GetCurrentLineNumber(parser),
|
|
||||||
XML_ErrorString(XML_GetErrorCode(parser)));
|
|
||||||
destroyXmlParser(parser);
|
|
||||||
file.close();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
} while (!done);
|
|
||||||
LOG_DBG("EHP", "Time to parse and build pages: %lu ms", millis() - chapterStartTime);
|
|
||||||
|
|
||||||
destroyXmlParser(parser);
|
if (XML_ParseBuffer(xmlParser_, static_cast<int>(len), done) == XML_STATUS_ERROR) {
|
||||||
file.close();
|
LOG_ERR("EHP", "Parse error at line %lu:\n%s", XML_GetCurrentLineNumber(xmlParser_),
|
||||||
|
XML_ErrorString(XML_GetErrorCode(xmlParser_)));
|
||||||
|
return ParseStatus::Error;
|
||||||
|
}
|
||||||
|
|
||||||
|
return done ? ParseStatus::Done : ParseStatus::More;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChapterHtmlSlimParser::abortParse() {
|
||||||
|
if (xmlParser_) {
|
||||||
|
destroyXmlParser(xmlParser_);
|
||||||
|
xmlParser_ = nullptr;
|
||||||
|
}
|
||||||
|
// Only close the file if it was successfully opened in beginParse()
|
||||||
|
if (parseFile_.isOpen()) {
|
||||||
|
parseFile_.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ChapterHtmlSlimParser::finishParse() {
|
||||||
|
if (xmlParser_) {
|
||||||
|
LOG_DBG("EHP", "Time to parse and build pages: %lu ms", millis() - parseStartTime_);
|
||||||
|
destroyXmlParser(xmlParser_);
|
||||||
|
xmlParser_ = nullptr;
|
||||||
|
}
|
||||||
|
parseFile_.close();
|
||||||
|
|
||||||
// Process last page if there is still text
|
// Process last page if there is still text
|
||||||
if (currentTextBlock) {
|
if (currentTextBlock) {
|
||||||
@@ -1357,6 +1419,23 @@ bool ChapterHtmlSlimParser::parseAndBuildPages() {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool ChapterHtmlSlimParser::parseAndBuildPages() {
|
||||||
|
if (!beginParse()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
for (;;) {
|
||||||
|
const ParseStatus status = parseStep();
|
||||||
|
if (status == ParseStatus::Error) {
|
||||||
|
abortParse();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (status == ParseStatus::Done) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return finishParse();
|
||||||
|
}
|
||||||
|
|
||||||
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;
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include <HalStorage.h>
|
||||||
#include <expat.h>
|
#include <expat.h>
|
||||||
|
|
||||||
#include <climits>
|
#include <climits>
|
||||||
@@ -31,7 +32,6 @@ class ChapterHtmlSlimParser {
|
|||||||
int skipUntilDepth = INT_MAX;
|
int skipUntilDepth = INT_MAX;
|
||||||
int boldUntilDepth = INT_MAX;
|
int boldUntilDepth = INT_MAX;
|
||||||
int italicUntilDepth = INT_MAX;
|
int italicUntilDepth = INT_MAX;
|
||||||
int underlineUntilDepth = INT_MAX;
|
|
||||||
// buffer for building up words from characters, will auto break if longer than this
|
// buffer for building up words from characters, will auto break if longer than this
|
||||||
// leave one char at end for null pointer
|
// leave one char at end for null pointer
|
||||||
char partWordBuffer[MAX_WORD_SIZE + 1] = {};
|
char partWordBuffer[MAX_WORD_SIZE + 1] = {};
|
||||||
@@ -60,7 +60,8 @@ class ChapterHtmlSlimParser {
|
|||||||
int depth = 0;
|
int depth = 0;
|
||||||
bool hasBold = false, bold = false;
|
bool hasBold = false, bold = false;
|
||||||
bool hasItalic = false, italic = false;
|
bool hasItalic = false, italic = false;
|
||||||
bool hasUnderline = false, underline = false;
|
bool hasTextDecoration = false;
|
||||||
|
CssTextDecoration textDecoration = CssTextDecoration::None;
|
||||||
bool hasDirection = false;
|
bool hasDirection = false;
|
||||||
CssTextDirection direction = CssTextDirection::Ltr;
|
CssTextDirection direction = CssTextDirection::Ltr;
|
||||||
bool hasSup = false, sup = false;
|
bool hasSup = false, sup = false;
|
||||||
@@ -71,7 +72,7 @@ class ChapterHtmlSlimParser {
|
|||||||
CssStyle currentCssStyle;
|
CssStyle currentCssStyle;
|
||||||
bool effectiveBold = false;
|
bool effectiveBold = false;
|
||||||
bool effectiveItalic = false;
|
bool effectiveItalic = false;
|
||||||
bool effectiveUnderline = false;
|
CssTextDecoration effectiveTextDecoration = CssTextDecoration::None;
|
||||||
bool effectiveDirectionDefined = false;
|
bool effectiveDirectionDefined = false;
|
||||||
CssTextDirection effectiveDirection = CssTextDirection::Ltr;
|
CssTextDirection effectiveDirection = CssTextDirection::Ltr;
|
||||||
bool effectiveSup = false;
|
bool effectiveSup = false;
|
||||||
@@ -79,6 +80,7 @@ class ChapterHtmlSlimParser {
|
|||||||
int tableDepth = 0;
|
int tableDepth = 0;
|
||||||
int tableRowIndex = 0;
|
int tableRowIndex = 0;
|
||||||
int tableColIndex = 0;
|
int tableColIndex = 0;
|
||||||
|
bool listItemBulletOnly = false; // true when currentTextBlock has only the <li> bullet
|
||||||
|
|
||||||
// Anchor-to-page mapping: tracks which page each HTML id attribute lands on
|
// Anchor-to-page mapping: tracks which page each HTML id attribute lands on
|
||||||
int completedPageCount = 0;
|
int completedPageCount = 0;
|
||||||
@@ -96,12 +98,25 @@ class ChapterHtmlSlimParser {
|
|||||||
std::vector<std::pair<int, FootnoteEntry>> pendingFootnotes; // <wordIndex, entry>
|
std::vector<std::pair<int, FootnoteEntry>> pendingFootnotes; // <wordIndex, entry>
|
||||||
int wordsExtractedInBlock = 0;
|
int wordsExtractedInBlock = 0;
|
||||||
|
|
||||||
|
// Resumable parse state. The one-shot parseAndBuildPages() drives these
|
||||||
|
// internally; the incremental section builder drives them across render ticks
|
||||||
|
// so a large single chapter can yield between pages instead of blocking the UI
|
||||||
|
// until the whole thing is laid out. parseFile_ and the expat parser stay alive
|
||||||
|
// for the lifetime of the parse so it can be paused and resumed at buffer
|
||||||
|
// boundaries.
|
||||||
|
XML_Parser xmlParser_ = nullptr;
|
||||||
|
HalFile parseFile_;
|
||||||
|
uint32_t parseStartTime_ = 0;
|
||||||
|
|
||||||
void updateEffectiveInlineStyle();
|
void updateEffectiveInlineStyle();
|
||||||
void startNewTextBlock(const BlockStyle& blockStyle);
|
void startNewTextBlock(const BlockStyle& blockStyle);
|
||||||
void flushPendingAnchor();
|
void flushPendingAnchor();
|
||||||
void flushPartWordBuffer();
|
void flushPartWordBuffer();
|
||||||
void makePages();
|
void makePages();
|
||||||
|
static EpdFontFamily::Style fontStyleForTextDecoration(CssTextDecoration decoration);
|
||||||
static void applyDirectionToEntry(StyleStackEntry& entry, const CssStyle& css);
|
static void applyDirectionToEntry(StyleStackEntry& entry, const CssStyle& css);
|
||||||
|
static void applyTextDecorationToEntry(StyleStackEntry& entry, const CssStyle& css);
|
||||||
|
void pushDecorationStyleEntry(CssTextDecoration defaultDecoration, const CssStyle& cssStyle);
|
||||||
void emitHorizontalRule(const BlockStyle& blockStyle);
|
void emitHorizontalRule(const BlockStyle& blockStyle);
|
||||||
// XML callbacks
|
// XML callbacks
|
||||||
static void XMLCALL startElement(void* userData, const XML_Char* name, const XML_Char** atts);
|
static void XMLCALL startElement(void* userData, const XML_Char* name, const XML_Char** atts);
|
||||||
@@ -141,8 +156,28 @@ class ChapterHtmlSlimParser {
|
|||||||
imageBasePath(imageBasePath),
|
imageBasePath(imageBasePath),
|
||||||
tocAnchors(std::move(tocAnchors)) {}
|
tocAnchors(std::move(tocAnchors)) {}
|
||||||
|
|
||||||
~ChapterHtmlSlimParser() = default;
|
~ChapterHtmlSlimParser();
|
||||||
|
|
||||||
|
// One-shot parse: builds every page before returning (begin + step* + finish).
|
||||||
bool parseAndBuildPages();
|
bool parseAndBuildPages();
|
||||||
|
|
||||||
|
// Resumable parse, for the incremental section builder. Drive as:
|
||||||
|
// if (!beginParse()) fail;
|
||||||
|
// loop: switch (parseStep()) { More: keep going / yield; Done: finishParse(); Error: abortParse(); }
|
||||||
|
// Pages are emitted via completePageFn as they complete during parseStep(), so
|
||||||
|
// the caller can stop once enough pages are built and resume on a later tick.
|
||||||
|
enum class ParseStatus { More, Done, Error };
|
||||||
|
bool beginParse();
|
||||||
|
ParseStatus parseStep();
|
||||||
|
bool finishParse(); // flush the trailing page and tear down; returns true
|
||||||
|
void abortParse(); // tear down without flushing (error / abandon)
|
||||||
|
|
||||||
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; }
|
const std::vector<std::pair<std::string, uint16_t>>& getAnchors() const { return anchorData; }
|
||||||
|
|
||||||
|
// Byte progress of the in-flight parse, used to estimate a still-building section's total page
|
||||||
|
// count (a giant single-spine book never fully lays out, so its real count is unknown). Valid
|
||||||
|
// between beginParse() and finishParse()/abortParse().
|
||||||
|
size_t parseBytesConsumed() { return parseFile_ ? parseFile_.position() : 0; }
|
||||||
|
size_t parseTotalBytes() { return parseFile_ ? parseFile_.size() : 0; }
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -137,8 +137,10 @@ void XMLCALL ContentOpfParser::startElement(void* userData, const XML_Char* name
|
|||||||
LOG_ERR("COF", "Couldn't open temp items file for reading. This is probably going to be a fatal error.");
|
LOG_ERR("COF", "Couldn't open temp items file for reading. This is probably going to be a fatal error.");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sort item index for binary search if we have enough items
|
// Sort the (unconditionally-built) item index so every idref lookup uses binary
|
||||||
if (self->itemIndex.size() >= LARGE_SPINE_THRESHOLD) {
|
// search. Without this, small/medium manifests fell back to an O(spine × manifest)
|
||||||
|
// linear rescan of .items.bin per itemref (up to ~200ms/item at large scale).
|
||||||
|
if (!self->itemIndex.empty()) {
|
||||||
std::sort(self->itemIndex.begin(), self->itemIndex.end(), [](const ItemIndexEntry& a, const ItemIndexEntry& b) {
|
std::sort(self->itemIndex.begin(), self->itemIndex.end(), [](const ItemIndexEntry& a, const ItemIndexEntry& b) {
|
||||||
return a.idHash < b.idHash || (a.idHash == b.idHash && a.idLen < b.idLen);
|
return a.idHash < b.idHash || (a.idHash == b.idHash && a.idLen < b.idLen);
|
||||||
});
|
});
|
||||||
@@ -284,9 +286,8 @@ void XMLCALL ContentOpfParser::startElement(void* userData, const XML_Char* name
|
|||||||
++it;
|
++it;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Slow path: linear scan (for small manifests, keeps original behavior)
|
// Fallback linear scan, only reached when the index is empty (no manifest
|
||||||
// TODO: This lookup is slow as need to scan through all items each time.
|
// items). The fast binary-search path above is used for all real manifests.
|
||||||
// It can take up to 200ms per item when getting to 1500 items.
|
|
||||||
self->tempItemStore.seek(0);
|
self->tempItemStore.seek(0);
|
||||||
std::string itemId;
|
std::string itemId;
|
||||||
while (self->tempItemStore.available()) {
|
while (self->tempItemStore.available()) {
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ class ContentOpfParser final : public Print {
|
|||||||
HalFile tempItemStore;
|
HalFile tempItemStore;
|
||||||
std::string coverItemId;
|
std::string coverItemId;
|
||||||
|
|
||||||
// Index for fast idref→href lookup (used only for large EPUBs)
|
// Index for fast idref→href lookup (binary search over .items.bin)
|
||||||
struct ItemIndexEntry {
|
struct ItemIndexEntry {
|
||||||
uint32_t idHash; // FNV-1a hash of itemId
|
uint32_t idHash; // FNV-1a hash of itemId
|
||||||
uint16_t idLen; // length for collision reduction
|
uint16_t idLen; // length for collision reduction
|
||||||
@@ -41,8 +41,6 @@ class ContentOpfParser final : public Print {
|
|||||||
std::deque<ItemIndexEntry> itemIndex;
|
std::deque<ItemIndexEntry> itemIndex;
|
||||||
bool useItemIndex = false;
|
bool useItemIndex = false;
|
||||||
|
|
||||||
static constexpr uint16_t LARGE_SPINE_THRESHOLD = 400;
|
|
||||||
|
|
||||||
// FNV-1a hash function
|
// FNV-1a hash function
|
||||||
static uint32_t fnvHash(const std::string& s) {
|
static uint32_t fnvHash(const std::string& s) {
|
||||||
uint32_t hash = 2166136261u;
|
uint32_t hash = 2166136261u;
|
||||||
|
|||||||
+48
-40
@@ -79,6 +79,53 @@ std::string normalisePath(const std::string& path) {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool naturalLess(const std::string& str1, const std::string& str2) {
|
||||||
|
// Naive natural sort: numeric-aware, case-insensitive
|
||||||
|
const char* s1 = str1.c_str();
|
||||||
|
const char* s2 = str2.c_str();
|
||||||
|
|
||||||
|
// ctype functions require unsigned char values: passing a negative char (UTF-8
|
||||||
|
// bytes above 0x7f with signed char) is undefined behavior
|
||||||
|
const auto isDigit = [](const char c) { return isdigit(static_cast<unsigned char>(c)) != 0; };
|
||||||
|
|
||||||
|
// Iterate while both strings have characters
|
||||||
|
while (*s1 && *s2) {
|
||||||
|
// Check if both are at the start of a number
|
||||||
|
if (isDigit(*s1) && isDigit(*s2)) {
|
||||||
|
// Skip leading zeros and track them
|
||||||
|
while (*s1 == '0') s1++;
|
||||||
|
while (*s2 == '0') s2++;
|
||||||
|
|
||||||
|
// Count digits to compare lengths first
|
||||||
|
int len1 = 0, len2 = 0;
|
||||||
|
while (isDigit(s1[len1])) len1++;
|
||||||
|
while (isDigit(s2[len2])) len2++;
|
||||||
|
|
||||||
|
// Different length so return smaller integer value
|
||||||
|
if (len1 != len2) return len1 < len2;
|
||||||
|
|
||||||
|
// Same length so compare digit by digit
|
||||||
|
for (int i = 0; i < len1; i++) {
|
||||||
|
if (s1[i] != s2[i]) return s1[i] < s2[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Numbers equal so advance pointers
|
||||||
|
s1 += len1;
|
||||||
|
s2 += len2;
|
||||||
|
} else {
|
||||||
|
// Regular case-insensitive character comparison
|
||||||
|
const int c1 = tolower(static_cast<unsigned char>(*s1));
|
||||||
|
const int c2 = tolower(static_cast<unsigned char>(*s2));
|
||||||
|
if (c1 != c2) return c1 < c2;
|
||||||
|
s1++;
|
||||||
|
s2++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// One string is prefix of other
|
||||||
|
return *s1 == '\0' && *s2 != '\0';
|
||||||
|
}
|
||||||
|
|
||||||
void sortFileList(std::vector<std::string>& strs) {
|
void sortFileList(std::vector<std::string>& strs) {
|
||||||
std::sort(begin(strs), end(strs), [](const std::string& str1, const std::string& str2) {
|
std::sort(begin(strs), end(strs), [](const std::string& str1, const std::string& str2) {
|
||||||
// Directories first
|
// Directories first
|
||||||
@@ -86,46 +133,7 @@ void sortFileList(std::vector<std::string>& strs) {
|
|||||||
bool isDir2 = str2.back() == '/';
|
bool isDir2 = str2.back() == '/';
|
||||||
if (isDir1 != isDir2) return isDir1;
|
if (isDir1 != isDir2) return isDir1;
|
||||||
|
|
||||||
// Start naive natural sort
|
return naturalLess(str1, str2);
|
||||||
const char* s1 = str1.c_str();
|
|
||||||
const char* s2 = str2.c_str();
|
|
||||||
|
|
||||||
// Iterate while both strings have characters
|
|
||||||
while (*s1 && *s2) {
|
|
||||||
// Check if both are at the start of a number
|
|
||||||
if (isdigit(*s1) && isdigit(*s2)) {
|
|
||||||
// Skip leading zeros and track them
|
|
||||||
while (*s1 == '0') s1++;
|
|
||||||
while (*s2 == '0') s2++;
|
|
||||||
|
|
||||||
// Count digits to compare lengths first
|
|
||||||
int len1 = 0, len2 = 0;
|
|
||||||
while (isdigit(s1[len1])) len1++;
|
|
||||||
while (isdigit(s2[len2])) len2++;
|
|
||||||
|
|
||||||
// Different length so return smaller integer value
|
|
||||||
if (len1 != len2) return len1 < len2;
|
|
||||||
|
|
||||||
// Same length so compare digit by digit
|
|
||||||
for (int i = 0; i < len1; i++) {
|
|
||||||
if (s1[i] != s2[i]) return s1[i] < s2[i];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Numbers equal so advance pointers
|
|
||||||
s1 += len1;
|
|
||||||
s2 += len2;
|
|
||||||
} else {
|
|
||||||
// Regular case-insensitive character comparison
|
|
||||||
char c1 = tolower(*s1);
|
|
||||||
char c2 = tolower(*s2);
|
|
||||||
if (c1 != c2) return c1 < c2;
|
|
||||||
s1++;
|
|
||||||
s2++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// One string is prefix of other
|
|
||||||
return *s1 == '\0' && *s2 != '\0';
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,10 @@ std::string decodeUriEscapes(const std::string& path);
|
|||||||
|
|
||||||
std::string normalisePath(const std::string& path);
|
std::string normalisePath(const std::string& path);
|
||||||
|
|
||||||
|
// Numeric-aware, case-insensitive comparison ("2" < "10"). Returns true when str1 orders
|
||||||
|
// before str2. Same ordering sortFileList applies within the file/directory groups.
|
||||||
|
bool naturalLess(const std::string& str1, const std::string& str2);
|
||||||
|
|
||||||
void sortFileList(std::vector<std::string>& strs);
|
void sortFileList(std::vector<std::string>& strs);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
+141
-23
@@ -1,6 +1,7 @@
|
|||||||
#include "GfxRenderer.h"
|
#include "GfxRenderer.h"
|
||||||
|
|
||||||
#include <BidiUtils.h>
|
#include <BidiUtils.h>
|
||||||
|
#include <BuildScratch.h>
|
||||||
#include <FontDecompressor.h>
|
#include <FontDecompressor.h>
|
||||||
#include <HalGPIO.h>
|
#include <HalGPIO.h>
|
||||||
#include <Logging.h>
|
#include <Logging.h>
|
||||||
@@ -24,6 +25,36 @@ uint8_t resolveSdCardStyle(const SdCardFont& font, const EpdFontFamily::Style st
|
|||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
const char* resolveVisualText(const char* text, std::string& visualBuffer, BidiUtils::BidiBaseDir baseDir);
|
const char* resolveVisualText(const char* text, std::string& visualBuffer, BidiUtils::BidiBaseDir baseDir);
|
||||||
|
|
||||||
|
// Appends the shaped visual form of every RTL token in `text` to `shapedOut`.
|
||||||
|
// getTextAdvanceX() measures the bidi-reordered, Arabic-shaped codepoint stream,
|
||||||
|
// so the SD advance table must be warmed with the presentation forms as well as
|
||||||
|
// the logical codepoints — otherwise every RTL word measurement misses the fast
|
||||||
|
// path and falls through to onGlyphMiss(), which opens the .cpfont and reads
|
||||||
|
// glyph metadata + bitmap into the 8-slot overflow ring, once per glyph.
|
||||||
|
// Tokens without RTL lead bytes (0xD6-0xDB) are skipped with a byte scan, so
|
||||||
|
// pure-LTR text pays almost nothing.
|
||||||
|
void appendShapedRtlTokens(const char* text, std::string& shapedOut) {
|
||||||
|
const auto isBreak = [](const char c) { return c == ' ' || c == '\n' || c == '\r' || c == '\t'; };
|
||||||
|
std::string token;
|
||||||
|
std::string visual;
|
||||||
|
const char* p = text;
|
||||||
|
while (*p) {
|
||||||
|
while (*p && isBreak(*p)) ++p;
|
||||||
|
const char* start = p;
|
||||||
|
bool hasRtlBytes = false;
|
||||||
|
while (*p && !isBreak(*p)) {
|
||||||
|
const auto b = static_cast<unsigned char>(*p);
|
||||||
|
hasRtlBytes = hasRtlBytes || (b >= 0xD6 && b <= 0xDB);
|
||||||
|
++p;
|
||||||
|
}
|
||||||
|
if (!hasRtlBytes) continue;
|
||||||
|
token.assign(start, p - start);
|
||||||
|
if (BidiUtils::applyBidiVisual(token.c_str(), visual, static_cast<int>(BidiUtils::BidiBaseDir::AUTO))) {
|
||||||
|
shapedOut += visual;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
const uint8_t* GfxRenderer::getGlyphBitmap(const EpdFontData* fontData, const EpdGlyph* glyph) const {
|
const uint8_t* GfxRenderer::getGlyphBitmap(const EpdFontData* fontData, const EpdGlyph* glyph) const {
|
||||||
@@ -57,7 +88,9 @@ const uint8_t* GfxRenderer::getGlyphBitmap(const EpdFontData* fontData, const Ep
|
|||||||
void GfxRenderer::ensureSdCardFontReady(int fontId, const char* utf8Text, uint8_t styleMask) const {
|
void GfxRenderer::ensureSdCardFontReady(int fontId, const char* utf8Text, uint8_t styleMask) const {
|
||||||
auto it = sdCardFonts_.find(fontId);
|
auto it = sdCardFonts_.find(fontId);
|
||||||
if (it != sdCardFonts_.end()) {
|
if (it != sdCardFonts_.end()) {
|
||||||
int missed = it->second->buildAdvanceTable(utf8Text, styleMask);
|
std::string shaped;
|
||||||
|
appendShapedRtlTokens(utf8Text, shaped);
|
||||||
|
int missed = it->second->buildAdvanceTable(utf8Text, styleMask, shaped.empty() ? nullptr : shaped.c_str());
|
||||||
if (missed > 0) {
|
if (missed > 0) {
|
||||||
LOG_DBG("GFX", "ensureSdCardFontReady: %d glyph(s) not found", missed);
|
LOG_DBG("GFX", "ensureSdCardFontReady: %d glyph(s) not found", missed);
|
||||||
}
|
}
|
||||||
@@ -71,7 +104,12 @@ void GfxRenderer::ensureSdCardFontReady(int fontId, const std::vector<std::strin
|
|||||||
// Augment the persistent advance-only table for layout measurement.
|
// Augment the persistent advance-only table for layout measurement.
|
||||||
// The table survives across paragraphs/sections (capped per font), so
|
// The table survives across paragraphs/sections (capped per font), so
|
||||||
// repeated indexing of the same SD font amortizes glyph-metric SD reads.
|
// repeated indexing of the same SD font amortizes glyph-metric SD reads.
|
||||||
int missed = it->second->buildAdvanceTable(words, includeHyphen, styleMask);
|
std::string shaped;
|
||||||
|
for (const auto& w : words) {
|
||||||
|
appendShapedRtlTokens(w.c_str(), shaped);
|
||||||
|
}
|
||||||
|
int missed =
|
||||||
|
it->second->buildAdvanceTable(words, includeHyphen, styleMask, shaped.empty() ? nullptr : shaped.c_str());
|
||||||
if (missed > 0) {
|
if (missed > 0) {
|
||||||
LOG_DBG("GFX", "ensureSdCardFontReady: %d glyph(s) not found", missed);
|
LOG_DBG("GFX", "ensureSdCardFontReady: %d glyph(s) not found", missed);
|
||||||
}
|
}
|
||||||
@@ -91,6 +129,47 @@ void GfxRenderer::begin() {
|
|||||||
bwBufferChunks.assign((frameBufferSize + BW_BUFFER_CHUNK_SIZE - 1) / BW_BUFFER_CHUNK_SIZE, nullptr);
|
bwBufferChunks.assign((frameBufferSize + BW_BUFFER_CHUNK_SIZE - 1) / BW_BUFFER_CHUNK_SIZE, nullptr);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void GfxRenderer::releaseFrameBufferForBuild() {
|
||||||
|
// Lend the framebuffer's bytes IN PLACE: the allocation is never freed, so
|
||||||
|
// it cannot move and repeated loans cannot fragment the heap (the previous
|
||||||
|
// free+realloc model measurably decayed the max contiguous block over a
|
||||||
|
// session). The bytes are deposited in the build-scratch registry so
|
||||||
|
// memory-hungry build phases (e.g. InflateStream's tinfl state + window)
|
||||||
|
// can claim them instead of allocating.
|
||||||
|
uint32_t size = 0;
|
||||||
|
uint8_t* scratch = display.lendFrameBufferStorage(&size);
|
||||||
|
frameBuffer = nullptr;
|
||||||
|
if (scratch) {
|
||||||
|
buildscratch::lend(scratch, size);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool GfxRenderer::restoreFrameBufferAfterBuild() {
|
||||||
|
buildscratch::reclaim();
|
||||||
|
display.returnFrameBufferStorage(); // cannot fail: the allocation was never freed
|
||||||
|
frameBuffer = display.getFrameBuffer();
|
||||||
|
return frameBuffer != nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
GfxRenderer::FrameBufferLoan::FrameBufferLoan(GfxRenderer& renderer) : renderer_(renderer) {
|
||||||
|
// Nesting guard: if the framebuffer is already lent out (an outer loan),
|
||||||
|
// stay inert so this end() cannot return storage the outer loan still owns.
|
||||||
|
if (!renderer_.hasFrameBuffer()) return;
|
||||||
|
renderer_.releaseFrameBufferForBuild();
|
||||||
|
active_ = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void GfxRenderer::FrameBufferLoan::end() {
|
||||||
|
if (!active_) return;
|
||||||
|
active_ = false;
|
||||||
|
if (!renderer_.restoreFrameBufferAfterBuild()) {
|
||||||
|
// Only reachable if the framebuffer never existed, which begin() already
|
||||||
|
// asserts against; kept as a backstop since running blind helps nobody.
|
||||||
|
LOG_ERR("GFX", "Framebuffer restore failed - restarting");
|
||||||
|
ESP.restart();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
bool GfxRenderer::isFontCacheScanning() const { return fontCacheManager_ && fontCacheManager_->isScanning(); }
|
bool GfxRenderer::isFontCacheScanning() const { return fontCacheManager_ && fontCacheManager_->isScanning(); }
|
||||||
|
|
||||||
void GfxRenderer::insertFont(const int fontId, EpdFontFamily font) {
|
void GfxRenderer::insertFont(const int fontId, EpdFontFamily font) {
|
||||||
@@ -418,18 +497,21 @@ void GfxRenderer::drawText(const int fontId, const int x, const int y, const cha
|
|||||||
uint32_t cp;
|
uint32_t cp;
|
||||||
uint32_t prevCp = 0;
|
uint32_t prevCp = 0;
|
||||||
while ((cp = utf8NextCodepoint(reinterpret_cast<const uint8_t**>(&textCursor)))) {
|
while ((cp = utf8NextCodepoint(reinterpret_cast<const uint8_t**>(&textCursor)))) {
|
||||||
// Skip Hebrew Niqqud (vowel marks)
|
// RTL vowel marks (Hebrew niqqud, Arabic harakat) ride the combining-mark
|
||||||
// Temporary: avoid adding Niqqud to built-in fonts. Remove when custom fonts are supported.
|
// path: zero-advance overlays on the preceding base glyph (applyBidiVisual
|
||||||
if (cp >= 0x0591 && cp <= 0x05C7) {
|
// emits base-then-marks per UAX#9 L3). anchorFor pins position-sensitive
|
||||||
continue;
|
// niqqud (dagesh, shin/sin dots, holam) to their spot on the base; other
|
||||||
}
|
// marks stay centered, raised above the base or (kasra) at their
|
||||||
|
// font-native position. Fonts without their glyphs — the built-ins — miss
|
||||||
if (utf8IsCombiningMark(cp)) {
|
// the getGlyph lookup and skip them, as before.
|
||||||
|
if (utf8IsCombiningMark(cp) || BidiUtils::isTransparentMark(cp)) {
|
||||||
const EpdGlyph* combiningGlyph = font.getGlyph(cp, style);
|
const EpdGlyph* combiningGlyph = font.getGlyph(cp, style);
|
||||||
if (!combiningGlyph) continue;
|
if (!combiningGlyph) continue;
|
||||||
const int raiseBy = combiningMark::raiseAboveBase(combiningGlyph->top, combiningGlyph->height, lastBaseTop);
|
const auto anchor = combiningMark::anchorFor(cp);
|
||||||
const int combiningX = combiningMark::centerOver(lastBaseX, lastBaseLeft, lastBaseWidth, combiningGlyph->left,
|
const int raiseBy =
|
||||||
combiningGlyph->width);
|
combiningMark::raiseAboveBase(anchor, combiningGlyph->top, combiningGlyph->height, lastBaseTop);
|
||||||
|
const int combiningX = combiningMark::anchorOver(anchor, lastBaseX, lastBaseLeft, lastBaseWidth,
|
||||||
|
combiningGlyph->left, combiningGlyph->width);
|
||||||
renderCharImpl<TextRotation::None>(*this, renderMode, font, cp, combiningX, yPos - raiseBy, black, style);
|
renderCharImpl<TextRotation::None>(*this, renderMode, font, cp, combiningX, yPos - raiseBy, black, style);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -1054,8 +1136,24 @@ void GfxRenderer::drawImage(const uint8_t bitmap[], const int x, const int y, co
|
|||||||
display.drawImage(bitmap, rotatedX, rotatedY, width, height);
|
display.drawImage(bitmap, rotatedX, rotatedY, width, height);
|
||||||
}
|
}
|
||||||
|
|
||||||
void GfxRenderer::drawIcon(const uint8_t bitmap[], const int x, const int y, const int width, const int height) const {
|
void GfxRenderer::drawIcon(const uint8_t bitmap[], const int x, const int y, const int size) const {
|
||||||
display.drawImageTransparent(bitmap, y, getScreenWidth() - width - x, height, width);
|
// Plot the icon pixel-by-pixel through drawPixel (which applies the orientation
|
||||||
|
// transform) instead of the byte-aligned framebuffer blit. The blit snaps the
|
||||||
|
// icon's position to 8px (one byte) along the rotated axis, which prevents it
|
||||||
|
// from aligning with adjacent text; per-pixel plotting is pixel-precise.
|
||||||
|
// Icons are square and 1bpp (MSB-first, bit==0 = ink). The (size-1-row, col)
|
||||||
|
// mapping reproduces the Portrait orientation the blit produced; drawIcon is
|
||||||
|
// only called by the UI themes, which all render in forced Portrait.
|
||||||
|
const int rowBytes = (size + 7) / 8;
|
||||||
|
for (int row = 0; row < size; row++) {
|
||||||
|
for (int col = 0; col < size; col++) {
|
||||||
|
const uint8_t byte = bitmap[row * rowBytes + (col >> 3)];
|
||||||
|
const bool ink = ((byte >> (7 - (col & 7))) & 1) == 0;
|
||||||
|
if (ink) {
|
||||||
|
drawPixel(x + (size - 1 - row), y + col, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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,
|
||||||
@@ -1599,6 +1697,15 @@ int GfxRenderer::getKerning(const int fontId, const uint32_t leftCp, const uint3
|
|||||||
}
|
}
|
||||||
|
|
||||||
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 {
|
||||||
|
// Measure the exact codepoint stream drawText renders: bidi-reordered and
|
||||||
|
// Arabic-shaped (contextual presentation forms, Lam-Alef collapse).
|
||||||
|
// Measuring the raw logical text counts the Alef a ligature absorbs and
|
||||||
|
// uses base-letter advances instead of presentation-form advances, so RTL
|
||||||
|
// lines come out wider than they draw — uneven word gaps and a ragged
|
||||||
|
// right margin.
|
||||||
|
std::string visual;
|
||||||
|
text = resolveVisualText(text, visual, BidiUtils::BidiBaseDir::AUTO);
|
||||||
|
|
||||||
// Advance table fast-path for SD card fonts during layout.
|
// Advance table fast-path for SD card fonts during layout.
|
||||||
// No kerning/ligature lookup — consistent with previous metadataOnly behavior
|
// No kerning/ligature lookup — consistent with previous metadataOnly behavior
|
||||||
// where kern/lig data was not loaded.
|
// where kern/lig data was not loaded.
|
||||||
@@ -1614,6 +1721,10 @@ int GfxRenderer::getTextAdvanceX(const int fontId, const char* text, EpdFontFami
|
|||||||
}
|
}
|
||||||
const auto& font = fontIt->second;
|
const auto& font = fontIt->second;
|
||||||
while (uint32_t cp = utf8NextCodepoint(reinterpret_cast<const uint8_t**>(&text))) {
|
while (uint32_t cp = utf8NextCodepoint(reinterpret_cast<const uint8_t**>(&text))) {
|
||||||
|
// RTL vowel marks (niqqud/harakat) are zero-advance overlays in drawText — no width.
|
||||||
|
if (BidiUtils::isTransparentMark(cp)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
int32_t advFP = sdIt->second->getAdvance(cp, styleIdx);
|
int32_t advFP = sdIt->second->getAdvance(cp, styleIdx);
|
||||||
if (advFP == 0 && !utf8IsCombiningMark(cp)) {
|
if (advFP == 0 && !utf8IsCombiningMark(cp)) {
|
||||||
const EpdGlyph* glyph = font.getGlyph(cp, style);
|
const EpdGlyph* glyph = font.getGlyph(cp, style);
|
||||||
@@ -1636,6 +1747,10 @@ int GfxRenderer::getTextAdvanceX(const int fontId, const char* text, EpdFontFami
|
|||||||
int32_t prevAdvanceFP = 0; // 12.4 fixed-point: prev glyph's advance + next kern for snap
|
int32_t prevAdvanceFP = 0; // 12.4 fixed-point: prev glyph's advance + next kern for snap
|
||||||
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)))) {
|
||||||
|
// RTL vowel marks (niqqud/harakat) are zero-advance overlays in drawText — no width.
|
||||||
|
if (BidiUtils::isTransparentMark(cp)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
if (utf8IsCombiningMark(cp)) {
|
if (utf8IsCombiningMark(cp)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -1712,18 +1827,21 @@ void GfxRenderer::drawTextRotated90CW(const int fontId, const int x, const int y
|
|||||||
uint32_t cp;
|
uint32_t cp;
|
||||||
uint32_t prevCp = 0;
|
uint32_t prevCp = 0;
|
||||||
while ((cp = utf8NextCodepoint(reinterpret_cast<const uint8_t**>(&text)))) {
|
while ((cp = utf8NextCodepoint(reinterpret_cast<const uint8_t**>(&text)))) {
|
||||||
// Skip Hebrew Niqqud (vowel marks)
|
// RTL vowel marks (Hebrew niqqud, Arabic harakat) ride the combining-mark
|
||||||
// Temporary: avoid adding Niqqud to built-in fonts. Remove when custom fonts are supported.
|
// path: zero-advance overlays on the preceding base glyph (applyBidiVisual
|
||||||
if (cp >= 0x0591 && cp <= 0x05C7) {
|
// emits base-then-marks per UAX#9 L3). anchorFor pins position-sensitive
|
||||||
continue;
|
// niqqud (dagesh, shin/sin dots, holam) to their spot on the base; other
|
||||||
}
|
// marks stay centered, raised above the base or (kasra) at their
|
||||||
|
// font-native position. Fonts without their glyphs — the built-ins — miss
|
||||||
if (utf8IsCombiningMark(cp)) {
|
// the getGlyph lookup and skip them, as before.
|
||||||
|
if (utf8IsCombiningMark(cp) || BidiUtils::isTransparentMark(cp)) {
|
||||||
const EpdGlyph* combiningGlyph = font.getGlyph(cp, style);
|
const EpdGlyph* combiningGlyph = font.getGlyph(cp, style);
|
||||||
if (!combiningGlyph) continue;
|
if (!combiningGlyph) continue;
|
||||||
const int raiseBy = combiningMark::raiseAboveBase(combiningGlyph->top, combiningGlyph->height, lastBaseTop);
|
const auto anchor = combiningMark::anchorFor(cp);
|
||||||
|
const int raiseBy =
|
||||||
|
combiningMark::raiseAboveBase(anchor, combiningGlyph->top, combiningGlyph->height, lastBaseTop);
|
||||||
const int combiningX = x - raiseBy;
|
const int combiningX = x - raiseBy;
|
||||||
const int combiningY = combiningMark::centerOverRotated90CW(lastBaseY, lastBaseLeft, lastBaseWidth,
|
const int combiningY = combiningMark::anchorOverRotated90CW(anchor, lastBaseY, lastBaseLeft, lastBaseWidth,
|
||||||
combiningGlyph->left, combiningGlyph->width);
|
combiningGlyph->left, combiningGlyph->width);
|
||||||
renderCharImpl<TextRotation::Rotated90CW>(*this, renderMode, font, cp, combiningX, combiningY, black, style);
|
renderCharImpl<TextRotation::Rotated90CW>(*this, renderMode, font, cp, combiningX, combiningY, black, style);
|
||||||
continue;
|
continue;
|
||||||
|
|||||||
@@ -183,7 +183,7 @@ class GfxRenderer {
|
|||||||
void fillRoundedRect(int x, int y, int width, int height, int cornerRadius, bool roundTopLeft, bool roundTopRight,
|
void fillRoundedRect(int x, int y, int width, int height, int cornerRadius, bool roundTopLeft, bool roundTopRight,
|
||||||
bool roundBottomLeft, bool roundBottomRight, Color color) const;
|
bool roundBottomLeft, bool roundBottomRight, Color color) const;
|
||||||
void drawImage(const uint8_t bitmap[], int x, int y, int width, int height) const;
|
void drawImage(const uint8_t bitmap[], int x, int y, int width, int height) const;
|
||||||
void drawIcon(const uint8_t bitmap[], int x, int y, int width, int height) const;
|
void drawIcon(const uint8_t bitmap[], int x, int y, int size) const;
|
||||||
void drawBitmap(const Bitmap& bitmap, int x, int y, int maxWidth, int maxHeight, float cropX = 0,
|
void drawBitmap(const Bitmap& bitmap, int x, int y, int maxWidth, int maxHeight, float cropX = 0,
|
||||||
float cropY = 0) const;
|
float cropY = 0) const;
|
||||||
void drawBitmap1Bit(const Bitmap& bitmap, int x, int y, int maxWidth, int maxHeight) const;
|
void drawBitmap1Bit(const Bitmap& bitmap, int x, int y, int maxWidth, int maxHeight) const;
|
||||||
@@ -250,6 +250,34 @@ class GfxRenderer {
|
|||||||
// Font helpers
|
// Font helpers
|
||||||
const uint8_t* getGlyphBitmap(const EpdFontData* fontData, const EpdGlyph* glyph) const;
|
const uint8_t* getGlyphBitmap(const EpdFontData* fontData, const EpdGlyph* glyph) const;
|
||||||
|
|
||||||
|
// Lend the 48 KB framebuffer's bytes to a memory-hungry phase (chapter
|
||||||
|
// builds) WITHOUT freeing the allocation, so it never moves and repeated
|
||||||
|
// loans cannot fragment the heap. Between release and restore NOTHING may
|
||||||
|
// draw or display — the panel keeps showing its last refreshed image. The
|
||||||
|
// lent bytes are published via buildscratch::claim() for consumers like
|
||||||
|
// InflateStream. restore returns the buffer white, so the caller must
|
||||||
|
// redraw the full screen; it cannot fail (no allocation involved).
|
||||||
|
void releaseFrameBufferForBuild();
|
||||||
|
bool restoreFrameBufferAfterBuild();
|
||||||
|
bool hasFrameBuffer() const { return frameBuffer != nullptr; }
|
||||||
|
|
||||||
|
// RAII form of the loan above, for blocking build regions with early-return
|
||||||
|
// error paths: restores on scope exit (or explicitly via end()). Display the
|
||||||
|
// popup/screen the panel should hold BEFORE constructing one. Constructing
|
||||||
|
// while the framebuffer is already lent yields an inert loan (nesting-safe).
|
||||||
|
class FrameBufferLoan {
|
||||||
|
public:
|
||||||
|
explicit FrameBufferLoan(GfxRenderer& renderer);
|
||||||
|
~FrameBufferLoan() { end(); }
|
||||||
|
void end();
|
||||||
|
FrameBufferLoan(const FrameBufferLoan&) = delete;
|
||||||
|
FrameBufferLoan& operator=(const FrameBufferLoan&) = delete;
|
||||||
|
|
||||||
|
private:
|
||||||
|
GfxRenderer& renderer_;
|
||||||
|
bool active_ = false;
|
||||||
|
};
|
||||||
|
|
||||||
// Low level functions
|
// Low level functions
|
||||||
uint8_t* getFrameBuffer() const;
|
uint8_t* getFrameBuffer() const;
|
||||||
size_t getBufferSize() const;
|
size_t getBufferSize() const;
|
||||||
|
|||||||
@@ -0,0 +1,394 @@
|
|||||||
|
_language_name: "العربية"
|
||||||
|
_language_code: "AR"
|
||||||
|
_order: "28"
|
||||||
|
|
||||||
|
STR_CROSSPOINT: "CrossPoint"
|
||||||
|
STR_BOOTING: "جاري بدء التشغيل"
|
||||||
|
STR_SLEEPING: "وضع السكون"
|
||||||
|
STR_ENTERING_SLEEP: "جاري الدخول في وضع السكون"
|
||||||
|
STR_BROWSE_FILES: "تصفح الملفات"
|
||||||
|
STR_FILE_TRANSFER: "نقل الملفات"
|
||||||
|
STR_SETTINGS_TITLE: "الإعدادات"
|
||||||
|
STR_CONTINUE_READING: "متابعة القراءة"
|
||||||
|
STR_NO_OPEN_BOOK: "لا يوجد كتاب مفتوح"
|
||||||
|
STR_START_READING: "ابدأ القراءة أدناه"
|
||||||
|
STR_NO_FILES_FOUND: "لم يتم العثور على ملفات"
|
||||||
|
STR_SELECT_CHAPTER: "اختر الفصل"
|
||||||
|
STR_NO_CHAPTERS: "لا توجد فصول"
|
||||||
|
STR_END_OF_BOOK: "نهاية الكتاب"
|
||||||
|
STR_EMPTY_CHAPTER: "فصل فارغ"
|
||||||
|
STR_INDEXING: "جاري الفهرسة"
|
||||||
|
STR_INDEX_FAILED: "فشلت الفهرسة - كتاب غير صالح"
|
||||||
|
STR_MEMORY_ERROR: "خطأ في الذاكرة"
|
||||||
|
STR_PAGE_LOAD_ERROR: "خطأ في تحميل الصفحة"
|
||||||
|
STR_EMPTY_FILE: "ملف فارغ"
|
||||||
|
STR_OUT_OF_BOUNDS: "خارج النطاق"
|
||||||
|
STR_LOADING: "جاري التحميل..."
|
||||||
|
STR_LOADING_POPUP: "جاري التحميل"
|
||||||
|
STR_WIFI_NETWORKS: "شبكات Wi-Fi"
|
||||||
|
STR_NO_NETWORKS: "لم يتم العثور على شبكات"
|
||||||
|
STR_NETWORKS_FOUND: "تم العثور على %zu شبكة"
|
||||||
|
STR_SCANNING: "جاري البحث..."
|
||||||
|
STR_FINDING_SAVED_WIFI: "جاري البحث عن شبكة Wi-Fi محفوظة..."
|
||||||
|
STR_CONNECTING: "جاري الاتصال..."
|
||||||
|
STR_CONNECTING_SAVED_WIFI: "جاري الاتصال بشبكة Wi-Fi محفوظة..."
|
||||||
|
STR_SHOW_NETWORKS: "عرض"
|
||||||
|
STR_CONNECTED: "تم الاتصال!"
|
||||||
|
STR_CONNECTION_FAILED: "فشل الاتصال"
|
||||||
|
STR_FORGET_NETWORK: "نسيان هذه الشبكة؟"
|
||||||
|
STR_SAVE_PASSWORD: "حفظ كلمة المرور للمرة القادمة؟"
|
||||||
|
STR_PRESS_OK_SCAN: "اضغط موافق لإعادة البحث"
|
||||||
|
STR_JOIN_NETWORK: "الانضمام إلى شبكة"
|
||||||
|
STR_CREATE_HOTSPOT: "إنشاء نقطة اتصال"
|
||||||
|
STR_JOIN_DESC: "الاتصال بشبكة Wi-Fi موجودة"
|
||||||
|
STR_HOTSPOT_DESC: "إنشاء شبكة Wi-Fi يمكن للآخرين الانضمام إليها"
|
||||||
|
STR_STARTING_HOTSPOT: "جاري تشغيل نقطة الاتصال..."
|
||||||
|
STR_HOTSPOT_MODE: "وضع نقطة الاتصال"
|
||||||
|
STR_CONNECT_WIFI_HINT: "قم بتوصيل جهازك بشبكة Wi-Fi هذه"
|
||||||
|
STR_OPEN_URL_HINT: "افتح هذا العنوان في المتصفح"
|
||||||
|
STR_OR_HTTP_PREFIX: "أو http://"
|
||||||
|
STR_SCAN_QR_HINT: "أو امسح رمز QR بهاتفك:"
|
||||||
|
STR_CALIBRE_WIRELESS: "اتصال Calibre اللاسلكي"
|
||||||
|
STR_NETWORK_LEGEND: "* = مشفرة | + = محفوظة"
|
||||||
|
STR_MAC_ADDRESS: "عنوان MAC:"
|
||||||
|
STR_CHECKING_WIFI: "جاري فحص Wi-Fi..."
|
||||||
|
STR_ENTER_WIFI_PASSWORD: "أدخل كلمة مرور الشبكة"
|
||||||
|
STR_ADD_HIDDEN_NETWORK: "إضافة شبكة مخفية..."
|
||||||
|
STR_ENTER_WIFI_SSID: "أدخل اسم الشبكة (SSID)"
|
||||||
|
STR_TO_PREFIX: "إلى "
|
||||||
|
STR_CALIBRE_RECEIVING: "جاري الاستلام: "
|
||||||
|
STR_CALIBRE_RECEIVED: "تم الاستلام: "
|
||||||
|
STR_CALIBRE_INSTRUCTION_1: "1) قم بتثبيت إضافة CrossPoint Reader"
|
||||||
|
STR_CALIBRE_INSTRUCTION_2: "2) اتصل بنفس شبكة Wi-Fi"
|
||||||
|
STR_CALIBRE_INSTRUCTION_3: "3) في Calibre: اختر \"إرسال إلى الجهاز\""
|
||||||
|
STR_CALIBRE_INSTRUCTION_4: "اترك هذه الشاشة مفتوحة أثناء الإرسال"
|
||||||
|
STR_CAT_DISPLAY: "الشاشة"
|
||||||
|
STR_CAT_READER: "القراءة"
|
||||||
|
STR_CAT_CONTROLS: "الأزرار"
|
||||||
|
STR_CAT_SYSTEM: "النظام"
|
||||||
|
STR_SLEEP_SCREEN: "شاشة السكون"
|
||||||
|
STR_QUICK_RESUME_TIMEOUT: "استئناف سريع بعد المهلة"
|
||||||
|
STR_SLEEP_COVER_MODE: "عرض الغلاف في وضع السكون"
|
||||||
|
STR_HIDE_BATTERY: "إخفاء نسبة البطارية"
|
||||||
|
STR_EXTRA_SPACING: "تباعد إضافي بين الفقرات"
|
||||||
|
STR_TEXT_AA: "تنعيم حواف النص"
|
||||||
|
STR_IMAGES: "الصور"
|
||||||
|
STR_IMAGES_DISPLAY: "عرض"
|
||||||
|
STR_IMAGES_PLACEHOLDER: "عنصر نائب"
|
||||||
|
STR_IMAGES_SUPPRESS: "إخفاء"
|
||||||
|
STR_EOB_HOME: "الرئيسية"
|
||||||
|
STR_EOB_CONTINUE_WITH: "المتابعة إلى"
|
||||||
|
STR_SHORT_PWR_BTN: "ضغطة قصيرة على زر التشغيل"
|
||||||
|
STR_ORIENTATION: "اتجاه القراءة"
|
||||||
|
STR_SIDE_BTN_LAYOUT: "تخطيط الأزرار الجانبية (أثناء القراءة)"
|
||||||
|
STR_FRONT_BTN_FOLLOW_ORIENTATION: "تدوير الأزرار الأمامية مع الشاشة"
|
||||||
|
STR_LONG_PRESS_BEHAVIOR: "سلوك الضغطة الطويلة"
|
||||||
|
STR_LONG_PRESS_BEHAVIOR_OFF: "إيقاف"
|
||||||
|
STR_LONG_PRESS_BEHAVIOR_SKIP: "تخطي الفصل"
|
||||||
|
STR_LONG_PRESS_BEHAVIOR_ORIENTATION: "تغيير الاتجاه"
|
||||||
|
STR_LONG_PRESS_MENU: "قائمة الضغطة الطويلة"
|
||||||
|
STR_FONT_PREVIEW_TEXT: "نص حكيم له سر قاطع وذو شأن عظيم مكتوب على ثوب أخضر ومغلف بجلد أزرق"
|
||||||
|
STR_FONT_FAMILY: "خط القراءة"
|
||||||
|
STR_FONT_SIZE: "حجم الخط"
|
||||||
|
STR_LINE_SPACING: "تباعد الأسطر"
|
||||||
|
STR_SCREEN_MARGIN: "هوامش شاشة القراءة"
|
||||||
|
STR_PARA_ALIGNMENT: "محاذاة الفقرات"
|
||||||
|
STR_HYPHENATION: "تقسيم الكلمات"
|
||||||
|
STR_TIME_TO_SLEEP: "مهلة الدخول في السكون"
|
||||||
|
STR_SHOW_HIDDEN_FILES: "عرض الملفات المخفية"
|
||||||
|
STR_REMOVE_READ_FROM_RECENTS: "إزالة الكتب المقروءة من قائمة الكتب الأخيرة"
|
||||||
|
STR_MOVE_FINISHED_TO_READ: "نقل الكتب المنتهية إلى مجلد المقروءة"
|
||||||
|
STR_REFRESH_FREQ: "معدل تحديث الشاشة"
|
||||||
|
STR_KOREADER_SYNC: "مزامنة KOReader"
|
||||||
|
STR_CHECK_UPDATES: "التحقق من التحديثات"
|
||||||
|
STR_LANGUAGE: "اللغة"
|
||||||
|
STR_CLEAR_READING_CACHE: "مسح ذاكرة القراءة المؤقتة"
|
||||||
|
STR_USERNAME: "اسم المستخدم"
|
||||||
|
STR_PASSWORD: "كلمة المرور"
|
||||||
|
STR_SYNC_SERVER_URL: "عنوان خادم المزامنة"
|
||||||
|
STR_DOCUMENT_MATCHING: "مطابقة المستندات"
|
||||||
|
STR_SEND_METADATA: "إرسال البيانات الوصفية للمستند"
|
||||||
|
STR_AUTHENTICATE: "تسجيل الدخول"
|
||||||
|
STR_KOREADER_USERNAME: "اسم مستخدم KOReader"
|
||||||
|
STR_KOREADER_PASSWORD: "كلمة مرور KOReader"
|
||||||
|
STR_FILENAME: "اسم الملف"
|
||||||
|
STR_BINARY: "ثنائي"
|
||||||
|
STR_SET_CREDENTIALS_FIRST: "أدخل بيانات الاعتماد أولا"
|
||||||
|
STR_WIFI_CONN_FAILED: "فشل الاتصال بالشبكة"
|
||||||
|
STR_AUTHENTICATING: "جاري التحقق..."
|
||||||
|
STR_AUTH_SUCCESS: "تم تسجيل الدخول بنجاح!"
|
||||||
|
STR_KOREADER_AUTH: "تسجيل الدخول إلى KOReader"
|
||||||
|
STR_SYNC_READY: "مزامنة KOReader جاهزة للاستخدام"
|
||||||
|
STR_AUTH_FAILED: "فشل تسجيل الدخول"
|
||||||
|
STR_DONE: "تم"
|
||||||
|
STR_CLEAR_CACHE_WARNING_1: "سيؤدي هذا إلى مسح جميع بيانات الكتب المخزنة."
|
||||||
|
STR_CLEAR_CACHE_WARNING_2: "سيتم فقدان كل تقدم القراءة!"
|
||||||
|
STR_CLEAR_CACHE_WARNING_3: "ستحتاج الكتب إلى إعادة فهرسة"
|
||||||
|
STR_CLEAR_CACHE_WARNING_4: "عند فتحها مرة أخرى."
|
||||||
|
STR_CLEARING_CACHE: "جاري مسح الذاكرة المؤقتة..."
|
||||||
|
STR_CACHE_CLEARED: "تم مسح الذاكرة المؤقتة"
|
||||||
|
STR_ITEMS_REMOVED: "عناصر تمت إزالتها"
|
||||||
|
STR_FAILED_LOWER: "فشل"
|
||||||
|
STR_CLEAR_CACHE_FAILED: "فشل مسح الذاكرة المؤقتة"
|
||||||
|
STR_CHECK_SERIAL_OUTPUT: "راجع مخرجات المنفذ التسلسلي للتفاصيل"
|
||||||
|
STR_DARK: "داكن"
|
||||||
|
STR_LIGHT: "فاتح"
|
||||||
|
STR_CUSTOM: "مخصص"
|
||||||
|
STR_COVER: "الغلاف"
|
||||||
|
STR_NONE_OPT: "بدون"
|
||||||
|
STR_FIT: "ملاءمة"
|
||||||
|
STR_CROP: "قص"
|
||||||
|
STR_NEVER: "أبدا"
|
||||||
|
STR_IN_READER: "أثناء القراءة"
|
||||||
|
STR_ALWAYS: "دائما"
|
||||||
|
STR_IGNORE: "تجاهل"
|
||||||
|
STR_SLEEP: "سكون"
|
||||||
|
STR_PAGE_TURN: "تقليب الصفحة"
|
||||||
|
STR_FORCE_REFRESH: "تحديث الشاشة"
|
||||||
|
STR_PORTRAIT: "عمودي"
|
||||||
|
STR_LANDSCAPE_CW: "أفقي (يمين)"
|
||||||
|
STR_INVERTED: "عكس الألوان"
|
||||||
|
STR_ORIENTATION_INVERTED: "عمودي 180°"
|
||||||
|
STR_LANDSCAPE_CCW: "أفقي (يسار)"
|
||||||
|
STR_PREV_NEXT: "السابق/التالي"
|
||||||
|
STR_NEXT_PREV: "التالي/السابق"
|
||||||
|
STR_KOSYNC: "KOSync"
|
||||||
|
STR_BOOKMARK_OPTION: "إشارة مرجعية"
|
||||||
|
STR_DISABLED: "معطل"
|
||||||
|
STR_NOTO_SERIF: "Noto Serif"
|
||||||
|
STR_NOTO_SANS: "Noto Sans"
|
||||||
|
STR_SMALL: "صغير"
|
||||||
|
STR_MEDIUM: "متوسط"
|
||||||
|
STR_LARGE: "كبير"
|
||||||
|
STR_X_LARGE: "كبير جدا"
|
||||||
|
STR_TIGHT: "ضيق"
|
||||||
|
STR_NORMAL: "عادي"
|
||||||
|
STR_WIDE: "واسع"
|
||||||
|
STR_JUSTIFY: "ضبط الطرفين"
|
||||||
|
STR_ALIGN_LEFT: "يسار"
|
||||||
|
STR_CENTER: "وسط"
|
||||||
|
STR_ALIGN_RIGHT: "يمين"
|
||||||
|
STR_PAGES_1: "صفحة واحدة"
|
||||||
|
STR_PAGES_5: "5 صفحات"
|
||||||
|
STR_PAGES_10: "10 صفحات"
|
||||||
|
STR_PAGES_15: "15 صفحة"
|
||||||
|
STR_PAGES_30: "30 صفحة"
|
||||||
|
STR_UPDATE: "تحديث"
|
||||||
|
STR_CHECKING_UPDATE: "جاري التحقق من التحديثات..."
|
||||||
|
STR_NEW_UPDATE: "يتوفر تحديث جديد!"
|
||||||
|
STR_CURRENT_VERSION: "الإصدار الحالي: "
|
||||||
|
STR_NEW_VERSION: "الإصدار الجديد: "
|
||||||
|
STR_UPDATING: "جاري التحديث..."
|
||||||
|
STR_NO_UPDATE: "لا يتوفر تحديث"
|
||||||
|
STR_UPDATE_FAILED: "فشل التحديث"
|
||||||
|
STR_UPDATE_COMPLETE: "اكتمل التحديث"
|
||||||
|
STR_POWER_ON_HINT: "اضغط مطولا على زر التشغيل لتشغيل الجهاز من جديد"
|
||||||
|
STR_RESTARTING_HINT: "جاري إعادة التشغيل... إذا لم يعمل الجهاز، اضغط مطولا على زر التشغيل لعدة ثوان."
|
||||||
|
STR_NO_ENTRIES: "لم يتم العثور على عناصر"
|
||||||
|
STR_DOWNLOADING: "جاري التنزيل..."
|
||||||
|
STR_DOWNLOAD_FAILED: "فشل التنزيل"
|
||||||
|
STR_ERROR_MSG: "خطأ:"
|
||||||
|
STR_UNNAMED: "بدون اسم"
|
||||||
|
STR_HOLD_OPEN_TO_DELETE: "اضغط مطولا على فتح للحذف"
|
||||||
|
STR_NO_SERVER_URL: "لم يتم تحديد عنوان الخادم"
|
||||||
|
STR_FETCH_FEED_FAILED: "فشل جلب القائمة"
|
||||||
|
STR_PARSE_FEED_FAILED: "فشل تحليل القائمة"
|
||||||
|
STR_NEXT_PAGE: "الصفحة التالية"
|
||||||
|
STR_PREV_PAGE: "الصفحة السابقة"
|
||||||
|
STR_NETWORK_PREFIX: "الشبكة: "
|
||||||
|
STR_IP_ADDRESS_PREFIX: "عنوان IP: "
|
||||||
|
STR_ERROR_GENERAL_FAILURE: "خطأ: فشل عام"
|
||||||
|
STR_ERROR_NETWORK_NOT_FOUND: "خطأ: الشبكة غير موجودة"
|
||||||
|
STR_ERROR_CONNECTION_TIMEOUT: "خطأ: انتهت مهلة الاتصال"
|
||||||
|
STR_SD_CARD: "بطاقة SD"
|
||||||
|
STR_BACK: "رجوع »"
|
||||||
|
STR_EXIT: "خروج »"
|
||||||
|
STR_HOME: "الرئيسية »"
|
||||||
|
STR_SELECT: "اختيار"
|
||||||
|
STR_SELECTED: "محدد"
|
||||||
|
STR_TOGGLE: "تبديل"
|
||||||
|
STR_TOGGLE_BOOKMARK: "إضافة/إزالة إشارة مرجعية"
|
||||||
|
STR_CONFIRM: "تأكيد"
|
||||||
|
STR_CANCEL: "إلغاء"
|
||||||
|
STR_CONNECT: "اتصال"
|
||||||
|
STR_OPEN: "فتح"
|
||||||
|
STR_DOWNLOAD: "تنزيل"
|
||||||
|
STR_RETRY: "إعادة المحاولة"
|
||||||
|
STR_YES: "نعم"
|
||||||
|
STR_NO: "لا"
|
||||||
|
STR_SHOW: "عرض"
|
||||||
|
STR_HIDE: "إخفاء"
|
||||||
|
STR_STATE_ON: "تشغيل"
|
||||||
|
STR_STATE_OFF: "إيقاف"
|
||||||
|
STR_NOT_SET: "غير محدد"
|
||||||
|
STR_DIR_LEFT: "يسار"
|
||||||
|
STR_DIR_RIGHT: "يمين"
|
||||||
|
STR_DIR_UP: "أعلى"
|
||||||
|
STR_DIR_DOWN: "أسفل"
|
||||||
|
STR_OK_BUTTON: "موافق"
|
||||||
|
STR_SLEEP_COVER_FILTER: "مرشح غلاف شاشة السكون"
|
||||||
|
STR_FILTER_CONTRAST: "التباين"
|
||||||
|
STR_CUSTOMISE_STATUS_BAR: "تخصيص شريط الحالة"
|
||||||
|
STR_CHAPTER_PAGE_COUNT: "عدد صفحات الفصل"
|
||||||
|
STR_BOOK_PROGRESS_PERCENTAGE: "نسبة التقدم في الكتاب"
|
||||||
|
STR_PROGRESS_BAR: "شريط التقدم"
|
||||||
|
STR_PROGRESS_BAR_THICKNESS: "سمك شريط التقدم"
|
||||||
|
STR_PROGRESS_BAR_THIN: "رفيع"
|
||||||
|
STR_PROGRESS_BAR_MEDIUM: "متوسط"
|
||||||
|
STR_PROGRESS_BAR_THICK: "سميك"
|
||||||
|
STR_BOOK: "الكتاب"
|
||||||
|
STR_CHAPTER: "الفصل"
|
||||||
|
STR_EXAMPLE_CHAPTER: "الفصل 21"
|
||||||
|
STR_EXAMPLE_BOOK: "عنوان الكتاب"
|
||||||
|
STR_PREVIEW: "معاينة"
|
||||||
|
STR_TITLE: "العنوان"
|
||||||
|
STR_BATTERY: "البطارية"
|
||||||
|
STR_XTC_STATUS_BAR: "شريط حالة XTC"
|
||||||
|
STR_BOTTOM: "أسفل"
|
||||||
|
STR_TOP: "أعلى"
|
||||||
|
STR_CLOCK: "الساعة"
|
||||||
|
STR_CLOCK_UTC_OFFSET: "فرق التوقيت عن UTC"
|
||||||
|
STR_CLOCK_FORMAT: "تنسيق الساعة"
|
||||||
|
STR_CLOCK_FORMAT_24H: "24 ساعة"
|
||||||
|
STR_CLOCK_FORMAT_12H: "12 ساعة"
|
||||||
|
STR_CURRENT_TIME: "الوقت الحالي:"
|
||||||
|
STR_NEXT_FIELD: "التالي"
|
||||||
|
STR_CLOCK_SYNC: "مزامنة الساعة"
|
||||||
|
STR_CLOCK_SYNC_NOW: "مزامنة الساعة الآن"
|
||||||
|
STR_CLOCK_SYNCING: "جاري المزامنة عبر NTP..."
|
||||||
|
STR_CLOCK_SYNC_OK: "تمت مزامنة الساعة"
|
||||||
|
STR_CLOCK_SYNC_FAIL: "فشلت المزامنة"
|
||||||
|
STR_CLOCK_SYNC_NO_WIFI: "لا يوجد اتصال Wi-Fi"
|
||||||
|
STR_CLOCK_SYNC_NO_WIFI_HINT: "اتصل بشبكة Wi-Fi أولا، ثم حاول مرة أخرى."
|
||||||
|
STR_CLOCK_SYNCED: "تمت مزامنة الساعة"
|
||||||
|
STR_UI_THEME: "مظهر الواجهة"
|
||||||
|
STR_THEME_CLASSIC: "كلاسيكي"
|
||||||
|
STR_THEME_LYRA: "Lyra"
|
||||||
|
STR_THEME_ROUNDEDRAFF: "RoundedRaff"
|
||||||
|
STR_THEME_LYRA_EXTENDED: "Lyra موسع"
|
||||||
|
STR_SUNLIGHT_FADING_FIX: "إصلاح بهتان الشاشة في الشمس"
|
||||||
|
STR_REMAP_FRONT_BUTTONS: "تغيير وظائف الأزرار الأمامية"
|
||||||
|
STR_BOOKMARKS: "الإشارات المرجعية"
|
||||||
|
STR_BOOKMARK_ADDED: "تمت إضافة الإشارة المرجعية."
|
||||||
|
STR_BOOKMARK_REMOVED: "تمت إزالة الإشارة المرجعية."
|
||||||
|
STR_OPDS_BROWSER: "متصفح OPDS"
|
||||||
|
STR_SEARCH: "بحث"
|
||||||
|
STR_COVER_CUSTOM: "الغلاف + مخصص"
|
||||||
|
STR_QUICK_RESUME: "استئناف سريع"
|
||||||
|
STR_MENU_RECENT_BOOKS: "الكتب الأخيرة"
|
||||||
|
STR_REMOVE_FROM_RECENTS: "إزالة من الكتب الأخيرة؟"
|
||||||
|
STR_NO_RECENT_BOOKS: "لا توجد كتب أخيرة"
|
||||||
|
STR_CALIBRE_DESC: "استخدم النقل اللاسلكي من Calibre"
|
||||||
|
STR_FORGET_AND_REMOVE: "نسيان الشبكة وحذف كلمة المرور المحفوظة؟"
|
||||||
|
STR_FORGET_BUTTON: "نسيان"
|
||||||
|
STR_CALIBRE_STARTING: "جاري تشغيل Calibre..."
|
||||||
|
STR_CALIBRE_SETUP: "الإعداد"
|
||||||
|
STR_CALIBRE_STATUS: "الحالة"
|
||||||
|
STR_CLEAR_BUTTON: "مسح"
|
||||||
|
STR_DEFAULT_VALUE: "افتراضي"
|
||||||
|
STR_REMAP_PROMPT: "اضغط زرا أماميا لكل وظيفة"
|
||||||
|
STR_UNASSIGNED: "غير معين"
|
||||||
|
STR_ALREADY_ASSIGNED: "معين مسبقا"
|
||||||
|
STR_REMAP_RESET_HINT: "الزر الجانبي العلوي: استعادة التخطيط الافتراضي"
|
||||||
|
STR_REMAP_CANCEL_HINT: "الزر الجانبي السفلي: إلغاء التغيير"
|
||||||
|
STR_HW_BACK_LABEL: "رجوع (الزر 1)"
|
||||||
|
STR_HW_CONFIRM_LABEL: "تأكيد (الزر 2)"
|
||||||
|
STR_HW_LEFT_LABEL: "يسار (الزر 3)"
|
||||||
|
STR_HW_RIGHT_LABEL: "يمين (الزر 4)"
|
||||||
|
STR_GO_TO_PERCENT: "الانتقال إلى نسبة %"
|
||||||
|
STR_GO_HOME_BUTTON: "العودة إلى الرئيسية"
|
||||||
|
STR_SYNC_PROGRESS: "مزامنة التقدم"
|
||||||
|
STR_DELETE_CACHE: "حذف ذاكرة الكتاب المؤقتة"
|
||||||
|
STR_DELETE: "حذف"
|
||||||
|
STR_CONFIRM_DELETE_BOOKMARK: "حذف هذه الإشارة المرجعية؟"
|
||||||
|
STR_DISPLAY_QR: "عرض الصفحة كرمز QR"
|
||||||
|
STR_CHAPTER_PREFIX: "الفصل: "
|
||||||
|
STR_PAGES_SEPARATOR: " صفحات | "
|
||||||
|
STR_BOOK_PREFIX: "الكتاب: "
|
||||||
|
STR_CALIBRE_URL_HINT: "في Calibre، أضف /opds إلى العنوان"
|
||||||
|
STR_SYNCING_TIME: "جاري مزامنة الوقت..."
|
||||||
|
STR_CALC_HASH: "جاري حساب بصمة المستند..."
|
||||||
|
STR_HASH_FAILED: "فشل حساب بصمة المستند"
|
||||||
|
STR_FETCH_PROGRESS: "جاري جلب التقدم من الخادم..."
|
||||||
|
STR_UPLOAD_PROGRESS: "جاري رفع التقدم..."
|
||||||
|
STR_NO_CREDENTIALS_MSG: "لم يتم إعداد بيانات الاعتماد"
|
||||||
|
STR_KOREADER_SETUP_HINT: "قم بإعداد حساب KOReader في الإعدادات"
|
||||||
|
STR_PROGRESS_FOUND: "تم العثور على تقدم سابق!"
|
||||||
|
STR_REMOTE_LABEL: "الخادم:"
|
||||||
|
STR_LOCAL_LABEL: "الجهاز:"
|
||||||
|
STR_PAGE_OVERALL_FORMAT: "صفحة %d، %.2f%% إجمالا"
|
||||||
|
STR_PAGE_TOTAL_OVERALL_FORMAT: "صفحة %d من %d، %.2f%% إجمالا"
|
||||||
|
STR_DEVICE_FROM_FORMAT: " من: %s"
|
||||||
|
STR_APPLY_REMOTE: "استخدام تقدم الخادم"
|
||||||
|
STR_UPLOAD_LOCAL: "رفع التقدم المحلي"
|
||||||
|
STR_NO_REMOTE_MSG: "لا يوجد تقدم على الخادم"
|
||||||
|
STR_UPLOAD_PROMPT: "رفع الموضع الحالي؟"
|
||||||
|
STR_UPLOAD_SUCCESS: "تم رفع التقدم!"
|
||||||
|
STR_SYNC_FAILED_MSG: "فشلت المزامنة"
|
||||||
|
STR_SAVE_PROGRESS_FAILED: "تعذر حفظ التقدم"
|
||||||
|
STR_SECTION_PREFIX: "القسم "
|
||||||
|
STR_UPLOAD: "رفع"
|
||||||
|
STR_BOOK_S_STYLE: "تنسيق الكتاب الأصلي"
|
||||||
|
STR_EMBEDDED_STYLE: "التنسيق المضمن"
|
||||||
|
STR_FOCUS_READING: "قراءة مركزة"
|
||||||
|
STR_OPDS_SERVER_URL: "عنوان خادم OPDS"
|
||||||
|
STR_PWR_BTN_FOOTNOTE_BACK: "عودة سريعة من الحواشي"
|
||||||
|
STR_SET_SLEEP_COVER: "تعيين الغلاف"
|
||||||
|
STR_FOOTNOTES: "الحواشي"
|
||||||
|
STR_NO_FOOTNOTES: "لا توجد حواش في هذه الصفحة"
|
||||||
|
STR_LINK: "[رابط]"
|
||||||
|
STR_SCREENSHOT_BUTTON: "التقاط لقطة شاشة"
|
||||||
|
STR_SLEEP_TIMER_VALUE_FORMAT: "%u دقيقة"
|
||||||
|
STR_SLEEP_NEVER: "أبدا"
|
||||||
|
STR_STEP_HINT_FRONT: "الأزرار الأمامية:"
|
||||||
|
STR_STEP_HINT_SIDE: "الأزرار الجانبية:"
|
||||||
|
STR_ADD_SERVER: "إضافة خادم"
|
||||||
|
STR_SERVER_NAME: "اسم الخادم"
|
||||||
|
STR_NO_SERVERS: "لم يتم إعداد خوادم OPDS"
|
||||||
|
STR_DELETE_SERVER: "حذف الخادم"
|
||||||
|
STR_OPDS_SERVERS: "خوادم OPDS"
|
||||||
|
STR_AUTO_TURN_ENABLED: "التقليب التلقائي مفعل: "
|
||||||
|
STR_AUTO_TURN_PAGES_PER_MIN: "التقليب التلقائي (صفحات في الدقيقة)"
|
||||||
|
STR_MANAGE_FONTS: "إدارة الخطوط"
|
||||||
|
STR_FONT_BROWSER: "متصفح الخطوط"
|
||||||
|
STR_LOADING_FONT_LIST: "جاري تحميل قائمة الخطوط..."
|
||||||
|
STR_NO_FONTS_AVAILABLE: "لا تتوفر خطوط"
|
||||||
|
STR_FONT_INSTALLED: "تم تثبيت الخط!"
|
||||||
|
STR_FONT_INSTALL_FAILED: "فشل تثبيت الخط"
|
||||||
|
STR_INSTALLED: "مثبت"
|
||||||
|
STR_DOWNLOAD_ALL: "تنزيل الكل"
|
||||||
|
STR_UPDATE_ALL: "تحديث الكل"
|
||||||
|
STR_UPDATE_AVAILABLE: "تحديث"
|
||||||
|
STR_CRASH_TITLE: "تعطل النظام"
|
||||||
|
STR_CRASH_DESCRIPTION: "تم حفظ تقرير مفصل في الملف crash_report.txt. يرجى إرفاق هذا الملف عند الإبلاغ عن المشكلة."
|
||||||
|
STR_CRASH_REASON: "سبب التعطل:"
|
||||||
|
STR_CRASH_NO_REASON: "(لم يتم تسجيل سبب)"
|
||||||
|
STR_TILT_PAGE_TURN: "تقليب الصفحة بالإمالة"
|
||||||
|
STR_KB_HINT_MOVE_CURSOR: "اضغط زر اليمين أو اليسار لتحريك المؤشر"
|
||||||
|
STR_KB_HINT_RETURN_CURSOR: "اضغط زر اليسار للعودة إلى موضع المؤشر"
|
||||||
|
STR_KB_HINT_HIDE_PASSWORD: "اضغط مطولا على زر اليمين ثم اضغط [***] لإخفاء كلمة المرور"
|
||||||
|
STR_KB_HINT_SHOW_PASSWORD: "اضغط مطولا على زر اليمين ثم اضغط [abc] لإظهار كلمة المرور"
|
||||||
|
STR_KB_HINT_TOGGLE_HIDE_PASSWORD: "اضغط [***] لإخفاء كلمة المرور"
|
||||||
|
STR_KB_HINT_TOGGLE_SHOW_PASSWORD: "اضغط [abc] لإظهار كلمة المرور"
|
||||||
|
STR_KB_HINT_EDIT_ENTRY: "اضغط مطولا على زر الأعلى للتعديل"
|
||||||
|
STR_KB_TIPS: "نصائح:"
|
||||||
|
STR_KB_HINT_RETURN_KEYBOARD: "اضغط زر الأسفل للعودة إلى لوحة المفاتيح"
|
||||||
|
STR_KB_HINT_EXIT_URL_MODE: "اضغط ABC للخروج من وضع URL"
|
||||||
|
STR_KB_HINT_CLEAR_TEXT: "اضغط مطولا على DEL لمسح كل النص"
|
||||||
|
STR_KB_HINT_SECONDARY_CHAR: "اضغط مطولا على زر الاختيار للحرف الثانوي"
|
||||||
|
STR_KB_HINT_UPPER_SECONDARY: "اضغط مطولا على زر الاختيار للحرف الكبير أو الثانوي"
|
||||||
|
STR_KB_HINT_LOWER_SECONDARY: "اضغط مطولا على زر الاختيار للحرف الصغير أو الثانوي"
|
||||||
|
STR_KB_HINT_URL_SNIPPETS: "اضغط URL للاختصارات"
|
||||||
|
STR_SD_FIRMWARE_UPDATE: "تحديث البرنامج الثابت من بطاقة SD"
|
||||||
|
STR_SELECT_FIRMWARE_FILE: "اختر ملف البرنامج الثابت (.bin)"
|
||||||
|
STR_NO_BIN_FILES: "لم يتم العثور على ملفات .bin"
|
||||||
|
STR_VALIDATING_FIRMWARE: "جاري التحقق من البرنامج الثابت..."
|
||||||
|
STR_INVALID_FIRMWARE: "ملف برنامج ثابت غير صالح"
|
||||||
|
STR_FIRMWARE_TOO_LARGE: "البرنامج الثابت أكبر من القسم المخصص له"
|
||||||
|
STR_FIRMWARE_TOO_SMALL: "ملف البرنامج الثابت صغير جدا"
|
||||||
|
STR_FIRMWARE_UPDATE_PROMPT: "تحديث البرنامج الثابت؟"
|
||||||
|
STR_FIRMWARE_FILE_OPEN_FAILED: "تعذر فتح الملف"
|
||||||
|
STR_FIRMWARE_WRITE_FAILED: "فشلت كتابة البرنامج الثابت"
|
||||||
|
STR_FIRMWARE_UPDATE_DO_NOT_POWER_OFF: "لا تطفئ الجهاز!"
|
||||||
|
STR_RECOVERY_MODE: "وضع الاستعادة"
|
||||||
|
STR_RECOVERY_MODE_HINT: "ضع الملف firmware.bin في المجلد الرئيسي لبطاقة SD ثم اختره"
|
||||||
@@ -87,6 +87,7 @@ STR_USERNAME: "Імя карыстальніка"
|
|||||||
STR_PASSWORD: "Пароль"
|
STR_PASSWORD: "Пароль"
|
||||||
STR_SYNC_SERVER_URL: "URL сервера сінхранізацыі"
|
STR_SYNC_SERVER_URL: "URL сервера сінхранізацыі"
|
||||||
STR_DOCUMENT_MATCHING: "Супастаўленне дакументаў"
|
STR_DOCUMENT_MATCHING: "Супастаўленне дакументаў"
|
||||||
|
STR_SEND_METADATA: "Адпраўляць метаданыя дакумента"
|
||||||
STR_AUTHENTICATE: "Аўтарызацыя"
|
STR_AUTHENTICATE: "Аўтарызацыя"
|
||||||
STR_KOREADER_USERNAME: "Імя карыстальніка KOReader"
|
STR_KOREADER_USERNAME: "Імя карыстальніка KOReader"
|
||||||
STR_KOREADER_PASSWORD: "Пароль KOReader"
|
STR_KOREADER_PASSWORD: "Пароль KOReader"
|
||||||
@@ -236,7 +237,6 @@ STR_CHAPTER_PREFIX: "Раздзел:"
|
|||||||
STR_PAGES_SEPARATOR: "стар. |"
|
STR_PAGES_SEPARATOR: "стар. |"
|
||||||
STR_BOOK_PREFIX: "Кніга:"
|
STR_BOOK_PREFIX: "Кніга:"
|
||||||
STR_CALIBRE_URL_HINT: "Для Calibre дадайце /opds да URL"
|
STR_CALIBRE_URL_HINT: "Для Calibre дадайце /opds да URL"
|
||||||
STR_PERCENT_STEP_HINT: "Улева/Управа: 1% Уверх/Уніз: 10%"
|
|
||||||
STR_SYNCING_TIME: "Сінхранізацыя часу..."
|
STR_SYNCING_TIME: "Сінхранізацыя часу..."
|
||||||
STR_CALC_HASH: "Вылічэнне хэша дакумента..."
|
STR_CALC_HASH: "Вылічэнне хэша дакумента..."
|
||||||
STR_HASH_FAILED: "Не ўдалося вылічыць хэш дакумента"
|
STR_HASH_FAILED: "Не ўдалося вылічыць хэш дакумента"
|
||||||
@@ -296,7 +296,10 @@ STR_NO_FOOTNOTES: "На гэтай старонцы няма зносак"
|
|||||||
STR_LINK: "[спасылка]"
|
STR_LINK: "[спасылка]"
|
||||||
STR_SLEEP_TIMER_VALUE_FORMAT: "%u хв"
|
STR_SLEEP_TIMER_VALUE_FORMAT: "%u хв"
|
||||||
STR_SLEEP_NEVER: "Ніколі"
|
STR_SLEEP_NEVER: "Ніколі"
|
||||||
STR_SLEEP_TIMER_STEP_HINT: "Улева/Управа: 1 хв Уверх/Уніз: 5 хв"
|
STR_STEP_HINT_FRONT: "Пярэднія кнопкі:"
|
||||||
|
STR_STEP_HINT_SIDE: "Бакавыя кнопкі:"
|
||||||
STR_AUTO_TURN_ENABLED: "Аўтаперагортванне: "
|
STR_AUTO_TURN_ENABLED: "Аўтаперагортванне: "
|
||||||
STR_AUTO_TURN_PAGES_PER_MIN: "Аўтаперагортванне (старонак за хвіліну)"
|
STR_AUTO_TURN_PAGES_PER_MIN: "Аўтаперагортванне (старонак за хвіліну)"
|
||||||
STR_TILT_PAGE_TURN: "Перагортванне нахілам"
|
STR_TILT_PAGE_TURN: "Перагортванне нахілам"
|
||||||
|
STR_ADD_HIDDEN_NETWORK: "Дадаць схаваную сетку..."
|
||||||
|
STR_ENTER_WIFI_SSID: "Увядзіце назву сеткі (SSID)"
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ STR_BOOTING: "ARRENCANT"
|
|||||||
STR_SLEEPING: "ENTRANT EN REPÒS"
|
STR_SLEEPING: "ENTRANT EN REPÒS"
|
||||||
STR_ENTERING_SLEEP: "Entrant en repòs"
|
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 de fitxers"
|
||||||
STR_SETTINGS_TITLE: "Configuració"
|
STR_SETTINGS_TITLE: "Configuració"
|
||||||
STR_CONTINUE_READING: "Continua llegint"
|
STR_CONTINUE_READING: "Continua llegint"
|
||||||
STR_NO_OPEN_BOOK: "Cap llibre obert"
|
STR_NO_OPEN_BOOK: "Cap llibre obert"
|
||||||
@@ -18,6 +18,7 @@ STR_NO_CHAPTERS: "Sense capítols"
|
|||||||
STR_END_OF_BOOK: "Final del llibre"
|
STR_END_OF_BOOK: "Final del llibre"
|
||||||
STR_EMPTY_CHAPTER: "Capítol buit"
|
STR_EMPTY_CHAPTER: "Capítol buit"
|
||||||
STR_INDEXING: "S'està indexant"
|
STR_INDEXING: "S'està indexant"
|
||||||
|
STR_INDEX_FAILED: "No s'ha pogut indexar: llibre no vàlid"
|
||||||
STR_MEMORY_ERROR: "Error de memòria"
|
STR_MEMORY_ERROR: "Error de memòria"
|
||||||
STR_PAGE_LOAD_ERROR: "Error en carregar la pàgina"
|
STR_PAGE_LOAD_ERROR: "Error en carregar la pàgina"
|
||||||
STR_EMPTY_FILE: "Fitxer buit"
|
STR_EMPTY_FILE: "Fitxer buit"
|
||||||
@@ -28,58 +29,63 @@ STR_WIFI_NETWORKS: "Xarxes Wi-Fi"
|
|||||||
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"
|
||||||
STR_SCANNING: "S'està escanejant..."
|
STR_SCANNING: "S'està escanejant..."
|
||||||
|
STR_FINDING_SAVED_WIFI: "S'estan cercant xarxes Wi-Fi desades..."
|
||||||
STR_CONNECTING: "S'està connectant..."
|
STR_CONNECTING: "S'està connectant..."
|
||||||
|
STR_CONNECTING_SAVED_WIFI: "S'està connectant a una xarxa Wi-Fi desada..."
|
||||||
|
STR_SHOW_NETWORKS: "Mostra"
|
||||||
STR_CONNECTED: "S'ha connectat!"
|
STR_CONNECTED: "S'ha connectat!"
|
||||||
STR_CONNECTION_FAILED: "Error de connexió"
|
STR_CONNECTION_FAILED: "Error de connexió"
|
||||||
STR_FORGET_NETWORK: "Voleu oblidar aquesta xarxa?"
|
STR_FORGET_NETWORK: "Vols oblidar aquesta xarxa?"
|
||||||
STR_SAVE_PASSWORD: "Voleu desar la contrasenya per a la propera vegada?"
|
STR_SAVE_PASSWORD: "Vols desar la contrasenya per a la propera vegada?"
|
||||||
STR_PRESS_OK_SCAN: "Premeu OK per tornar a escanejar"
|
STR_PRESS_OK_SCAN: "Prem OK per tornar a escanejar"
|
||||||
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 Wi-Fi existent"
|
STR_JOIN_DESC: "Connecta't a una xarxa Wi-Fi existent"
|
||||||
STR_HOTSPOT_DESC: "Crea una xarxa Wi-Fi per unir-s'hi"
|
STR_HOTSPOT_DESC: "Crea una xarxa Wi-Fi per unir-s'hi"
|
||||||
STR_STARTING_HOTSPOT: "S'està iniciant el punt d'accés..."
|
STR_STARTING_HOTSPOT: "S'està iniciant el punt d'accés..."
|
||||||
STR_HOTSPOT_MODE: "Mode de punt d'accés"
|
STR_HOTSPOT_MODE: "Mode de punt d'accés"
|
||||||
STR_CONNECT_WIFI_HINT: "Connecteu el dispositiu a aquesta xarxa Wi-Fi"
|
STR_CONNECT_WIFI_HINT: "Connecta el dispositiu a aquesta xarxa Wi-Fi"
|
||||||
STR_OPEN_URL_HINT: "Obriu aquest URL al navegador"
|
STR_OPEN_URL_HINT: "Obre aquest URL al navegador"
|
||||||
STR_OR_HTTP_PREFIX: "o http://"
|
STR_OR_HTTP_PREFIX: "o http://"
|
||||||
STR_SCAN_QR_HINT: "o escanegeu el codi QR amb el telèfon:"
|
STR_SCAN_QR_HINT: "o escaneja el codi QR amb el telèfon:"
|
||||||
STR_CALIBRE_WIRELESS: "Calibre sense fils"
|
STR_CALIBRE_WIRELESS: "Calibre 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 Wi-Fi..."
|
STR_CHECKING_WIFI: "S'està comprovant el Wi-Fi..."
|
||||||
STR_ENTER_WIFI_PASSWORD: "Introduïu la contrasenya Wi-Fi"
|
STR_ENTER_WIFI_PASSWORD: "Introdueix la contrasenya Wi-Fi"
|
||||||
STR_TO_PREFIX: "a "
|
STR_TO_PREFIX: "a "
|
||||||
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_INSTRUCTION_1: "1) Instal·leu el connector CrossPoint Reader"
|
STR_CALIBRE_INSTRUCTION_1: "1) Instal·la el connector CrossPoint Reader"
|
||||||
STR_CALIBRE_INSTRUCTION_2: "2) Estigueu a la mateixa xarxa Wi-Fi"
|
STR_CALIBRE_INSTRUCTION_2: "2) Estigues a la mateixa xarxa Wi-Fi"
|
||||||
STR_CALIBRE_INSTRUCTION_3: "3) A Calibre: \"Envia a un dispositiu\""
|
STR_CALIBRE_INSTRUCTION_3: "3) A Calibre: \"Envia a un dispositiu\""
|
||||||
STR_CALIBRE_INSTRUCTION_4: "\"Mantingueu aquesta pantalla oberta mentre s'envia\""
|
STR_CALIBRE_INSTRUCTION_4: "\"Mantén aquesta pantalla oberta mentre s'envia\""
|
||||||
STR_CAT_DISPLAY: "Visualització"
|
STR_CAT_DISPLAY: "Visualització"
|
||||||
STR_CAT_READER: "Lector"
|
STR_CAT_READER: "Lector"
|
||||||
STR_CAT_CONTROLS: "Controls"
|
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: "Ajust de la portada en repòs"
|
||||||
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: "Imatges"
|
||||||
STR_IMAGES_DISPLAY: "Mostrar"
|
STR_IMAGES_DISPLAY: "Mostrar"
|
||||||
STR_IMAGES_PLACEHOLDER: "Text de mostra"
|
STR_IMAGES_PLACEHOLDER: "Text alternatiu"
|
||||||
STR_IMAGES_SUPPRESS: "Suprimir"
|
STR_IMAGES_SUPPRESS: "Suprimir"
|
||||||
STR_SHORT_PWR_BTN: "Clic curt del botó d'engegada"
|
STR_EOB_HOME: "Inici"
|
||||||
|
STR_EOB_CONTINUE_WITH: "Continua amb"
|
||||||
|
STR_SHORT_PWR_BTN: "Pulsació curta del botó d'engegada"
|
||||||
STR_ORIENTATION: "Orientació de lectura"
|
STR_ORIENTATION: "Orientació de lectura"
|
||||||
STR_SIDE_BTN_LAYOUT: "Disposició botons laterals"
|
STR_SIDE_BTN_LAYOUT: "Disposició botons laterals"
|
||||||
STR_FRONT_BTN_FOLLOW_ORIENTATION: "Orientar botons frontals"
|
STR_FRONT_BTN_FOLLOW_ORIENTATION: "Orientar botons frontals"
|
||||||
STR_LONG_PRESS_BEHAVIOR: "Comportament de prémer llargament el botó"
|
STR_LONG_PRESS_BEHAVIOR: "Acció en mantenir premut un botó"
|
||||||
STR_LONG_PRESS_BEHAVIOR_OFF: "Desactivat"
|
STR_LONG_PRESS_BEHAVIOR_OFF: "Desactivat"
|
||||||
STR_LONG_PRESS_BEHAVIOR_SKIP: "Saltar capítols"
|
STR_LONG_PRESS_BEHAVIOR_SKIP: "Saltar capítols"
|
||||||
STR_LONG_PRESS_BEHAVIOR_ORIENTATION: "Canvi d'orientació"
|
STR_LONG_PRESS_BEHAVIOR_ORIENTATION: "Canvi d'orientació"
|
||||||
STR_LONG_PRESS_MENU: "Funció de pulsació llarga"
|
STR_LONG_PRESS_MENU: "Funció de pulsació llarga"
|
||||||
STR_FONT_FAMILY: "Tipus de lletra"
|
STR_FONT_FAMILY: "Font del lector"
|
||||||
STR_FONT_SIZE: "Mida de la lletra (UI)"
|
STR_FONT_SIZE: "Cos de lletra del lector"
|
||||||
STR_LINE_SPACING: "Interlineat del lector"
|
STR_LINE_SPACING: "Interlineat del lector"
|
||||||
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"
|
||||||
@@ -88,15 +94,16 @@ STR_TIME_TO_SLEEP: "Temps per entrar en repòs"
|
|||||||
STR_SHOW_HIDDEN_FILES: "Mostra fitxers ocults"
|
STR_SHOW_HIDDEN_FILES: "Mostra fitxers ocults"
|
||||||
STR_REMOVE_READ_FROM_RECENTS: "Esborra els llibres llegits de la llista de recents"
|
STR_REMOVE_READ_FROM_RECENTS: "Esborra els llibres llegits de la llista de recents"
|
||||||
STR_MOVE_FINISHED_TO_READ: "Mou els llibres acabats a la carpeta Read"
|
STR_MOVE_FINISHED_TO_READ: "Mou els llibres acabats a la carpeta Read"
|
||||||
STR_REFRESH_FREQ: "Freqüència de refresc"
|
STR_REFRESH_FREQ: "Freqüència d'actualització"
|
||||||
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: "Llengua"
|
||||||
STR_CLEAR_READING_CACHE: "Esborra la memòria cau de lectura"
|
STR_CLEAR_READING_CACHE: "Esborra la memòria cau de lectura"
|
||||||
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ó"
|
||||||
STR_DOCUMENT_MATCHING: "Coincidència de documents"
|
STR_DOCUMENT_MATCHING: "Coincidència de documents"
|
||||||
|
STR_SEND_METADATA: "Envia metadades del document"
|
||||||
STR_AUTHENTICATE: "Autentica"
|
STR_AUTHENTICATE: "Autentica"
|
||||||
STR_KOREADER_USERNAME: "Nom d'usuari del KOReader"
|
STR_KOREADER_USERNAME: "Nom d'usuari del KOReader"
|
||||||
STR_KOREADER_PASSWORD: "Contrasenya del KOReader"
|
STR_KOREADER_PASSWORD: "Contrasenya del KOReader"
|
||||||
@@ -142,7 +149,7 @@ STR_PREV_NEXT: "Anterior/Següent"
|
|||||||
STR_NEXT_PREV: "Següent/Anterior"
|
STR_NEXT_PREV: "Següent/Anterior"
|
||||||
STR_KOSYNC: "KOSync"
|
STR_KOSYNC: "KOSync"
|
||||||
STR_BOOKMARK_OPTION: "Punt de llibre"
|
STR_BOOKMARK_OPTION: "Punt de llibre"
|
||||||
STR_DISABLED: "Desactivats"
|
STR_DISABLED: "Sense funció"
|
||||||
STR_NOTO_SERIF: "Noto Serif"
|
STR_NOTO_SERIF: "Noto Serif"
|
||||||
STR_NOTO_SANS: "Noto Sans"
|
STR_NOTO_SANS: "Noto Sans"
|
||||||
STR_SMALL: "Petita"
|
STR_SMALL: "Petita"
|
||||||
@@ -170,16 +177,16 @@ STR_UPDATING: "S'està actualitzant..."
|
|||||||
STR_NO_UPDATE: "No hi ha actualitzacions disponibles"
|
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: "Prem i mantén premut el botó d'encesa per tornar a engegar"
|
||||||
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"
|
||||||
STR_ERROR_MSG: "Error:"
|
STR_ERROR_MSG: "Error:"
|
||||||
STR_UNNAMED: "Sense nom"
|
STR_UNNAMED: "Sense nom"
|
||||||
STR_HOLD_OPEN_TO_DELETE: "Manteniu premut Obre per esborrar"
|
STR_HOLD_OPEN_TO_DELETE: "Mantén premut Obre per esborrar"
|
||||||
STR_NO_SERVER_URL: "No s'ha configurat cap URL de servidor"
|
STR_NO_SERVER_URL: "No s'ha configurat cap URL de servidor"
|
||||||
STR_FETCH_FEED_FAILED: "Ha fallat l'obtenció del feed"
|
STR_FETCH_FEED_FAILED: "Ha fallat l'obtenció del canal de continguts"
|
||||||
STR_PARSE_FEED_FAILED: "Ha fallat l'anàlisi del feed"
|
STR_PARSE_FEED_FAILED: "Ha fallat l'anàlisi del canal de continguts"
|
||||||
STR_NETWORK_PREFIX: "Xarxa: "
|
STR_NETWORK_PREFIX: "Xarxa: "
|
||||||
STR_IP_ADDRESS_PREFIX: "Adreça IP: "
|
STR_IP_ADDRESS_PREFIX: "Adreça IP: "
|
||||||
STR_ERROR_GENERAL_FAILURE: "Error: Fallada general"
|
STR_ERROR_GENERAL_FAILURE: "Error: Fallada general"
|
||||||
@@ -192,7 +199,7 @@ STR_HOME: "« Inici"
|
|||||||
STR_SELECT: "Selecciona"
|
STR_SELECT: "Selecciona"
|
||||||
STR_SELECTED: "Seleccionat"
|
STR_SELECTED: "Seleccionat"
|
||||||
STR_TOGGLE: "Canvia"
|
STR_TOGGLE: "Canvia"
|
||||||
STR_TOGGLE_BOOKMARK: "Commuta punt de llibre"
|
STR_TOGGLE_BOOKMARK: "Afegeix o elimina el punt de llibre"
|
||||||
STR_CONFIRM: "Confirma"
|
STR_CONFIRM: "Confirma"
|
||||||
STR_CANCEL: "Cancel·la"
|
STR_CANCEL: "Cancel·la"
|
||||||
STR_CONNECT: "Connecta"
|
STR_CONNECT: "Connecta"
|
||||||
@@ -211,7 +218,7 @@ STR_DIR_RIGHT: "Dreta"
|
|||||||
STR_DIR_UP: "Amunt"
|
STR_DIR_UP: "Amunt"
|
||||||
STR_DIR_DOWN: "Avall"
|
STR_DIR_DOWN: "Avall"
|
||||||
STR_OK_BUTTON: "OK"
|
STR_OK_BUTTON: "OK"
|
||||||
STR_SLEEP_COVER_FILTER: "Filtre de pantalla de repòs"
|
STR_SLEEP_COVER_FILTER: "Filtre de la portada en repòs"
|
||||||
STR_FILTER_CONTRAST: "Contrast"
|
STR_FILTER_CONTRAST: "Contrast"
|
||||||
STR_CUSTOMISE_STATUS_BAR: "Personalitza la barra d'estat"
|
STR_CUSTOMISE_STATUS_BAR: "Personalitza la barra d'estat"
|
||||||
STR_CHAPTER_PAGE_COUNT: "Comptador de pàgines del capítol"
|
STR_CHAPTER_PAGE_COUNT: "Comptador de pàgines del capítol"
|
||||||
@@ -233,7 +240,7 @@ STR_THEME_CLASSIC: "Clàssic"
|
|||||||
STR_THEME_LYRA: "Lyra"
|
STR_THEME_LYRA: "Lyra"
|
||||||
STR_THEME_LYRA_EXTENDED: "Lyra Ampliat"
|
STR_THEME_LYRA_EXTENDED: "Lyra Ampliat"
|
||||||
STR_SUNLIGHT_FADING_FIX: "Correcció de l'esvaïment pel sol"
|
STR_SUNLIGHT_FADING_FIX: "Correcció de l'esvaïment pel sol"
|
||||||
STR_QUICK_RESUME_TIMEOUT: "Represa ràpida després del temps"
|
STR_QUICK_RESUME_TIMEOUT: "Represa ràpida per inactivitat"
|
||||||
STR_REMAP_FRONT_BUTTONS: "Reassigna els botons frontals"
|
STR_REMAP_FRONT_BUTTONS: "Reassigna els botons frontals"
|
||||||
STR_BOOKMARKS: "Punts de llibre"
|
STR_BOOKMARKS: "Punts de llibre"
|
||||||
STR_BOOKMARK_ADDED: "S'ha afegit el punt de llibre."
|
STR_BOOKMARK_ADDED: "S'ha afegit el punt de llibre."
|
||||||
@@ -242,17 +249,17 @@ 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_MENU_RECENT_BOOKS: "Llibres recents"
|
STR_MENU_RECENT_BOOKS: "Llibres recents"
|
||||||
STR_REMOVE_FROM_RECENTS: "Voleu 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"
|
||||||
STR_CALIBRE_DESC: "Usa les transferències sense fils de Calibre"
|
STR_CALIBRE_DESC: "Usa les transferències sense fils de Calibre"
|
||||||
STR_FORGET_AND_REMOVE: "Voleu suprimir la contrasenya desada?"
|
STR_FORGET_AND_REMOVE: "Vols oblidar la xarxa i suprimir la contrasenya desada?"
|
||||||
STR_FORGET_BUTTON: "Oblida"
|
STR_FORGET_BUTTON: "Oblida"
|
||||||
STR_CALIBRE_STARTING: "S'està iniciant el Calibre..."
|
STR_CALIBRE_STARTING: "S'està iniciant el Calibre..."
|
||||||
STR_CALIBRE_SETUP: "Configura"
|
STR_CALIBRE_SETUP: "Configura"
|
||||||
STR_CALIBRE_STATUS: "Estat"
|
STR_CALIBRE_STATUS: "Estat"
|
||||||
STR_CLEAR_BUTTON: "Esborra"
|
STR_CLEAR_BUTTON: "Esborra"
|
||||||
STR_DEFAULT_VALUE: "Per defecte"
|
STR_DEFAULT_VALUE: "Per defecte"
|
||||||
STR_REMAP_PROMPT: "Premeu un botó frontal per a cada rol"
|
STR_REMAP_PROMPT: "Prem un botó frontal per a cada rol"
|
||||||
STR_UNASSIGNED: "No assignat"
|
STR_UNASSIGNED: "No assignat"
|
||||||
STR_ALREADY_ASSIGNED: "Ja assignat"
|
STR_ALREADY_ASSIGNED: "Ja assignat"
|
||||||
STR_REMAP_RESET_HINT: "Botó lateral Amunt: Restableix la disposició per defecte"
|
STR_REMAP_RESET_HINT: "Botó lateral Amunt: Restableix la disposició per defecte"
|
||||||
@@ -266,20 +273,19 @@ 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_DELETE: "Esborra"
|
||||||
STR_CONFIRM_DELETE_BOOKMARK: "Voleu esborrar aquest punt de llibre?"
|
STR_CONFIRM_DELETE_BOOKMARK: "Vols esborrar aquest punt de llibre?"
|
||||||
STR_DISPLAY_QR: "Mostra la pàgina com a QR"
|
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_CALIBRE_URL_HINT: "Per al Calibre, afegiu /opds a la URL"
|
STR_CALIBRE_URL_HINT: "Per al Calibre, afegeix /opds a la URL"
|
||||||
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..."
|
||||||
STR_CALC_HASH: "S'està calculant el hash del document..."
|
STR_CALC_HASH: "S'està calculant l'empremta electrònica del document..."
|
||||||
STR_HASH_FAILED: "No s'ha pogut calcular el hash del document"
|
STR_HASH_FAILED: "No s'ha pogut calcular l'empremta electrònica del document"
|
||||||
STR_FETCH_PROGRESS: "S'està obtenint el progrés remot..."
|
STR_FETCH_PROGRESS: "S'està obtenint el progrés remot..."
|
||||||
STR_UPLOAD_PROGRESS: "S'està pujant el progrés..."
|
STR_UPLOAD_PROGRESS: "S'està pujant el progrés..."
|
||||||
STR_NO_CREDENTIALS_MSG: "No s'han configurat credencials"
|
STR_NO_CREDENTIALS_MSG: "No s'han configurat credencials"
|
||||||
STR_KOREADER_SETUP_HINT: "Configureu el compte de KOReader a la configuració"
|
STR_KOREADER_SETUP_HINT: "Configura el compte de KOReader a la configuració"
|
||||||
STR_PROGRESS_FOUND: "S'ha trobat progrés!"
|
STR_PROGRESS_FOUND: "S'ha trobat progrés!"
|
||||||
STR_REMOTE_LABEL: "Remot:"
|
STR_REMOTE_LABEL: "Remot:"
|
||||||
STR_LOCAL_LABEL: "Local:"
|
STR_LOCAL_LABEL: "Local:"
|
||||||
@@ -289,7 +295,7 @@ STR_DEVICE_FROM_FORMAT: " De: %s"
|
|||||||
STR_APPLY_REMOTE: "Aplica el progrés remot"
|
STR_APPLY_REMOTE: "Aplica el progrés remot"
|
||||||
STR_UPLOAD_LOCAL: "Puja el progrés local"
|
STR_UPLOAD_LOCAL: "Puja el progrés local"
|
||||||
STR_NO_REMOTE_MSG: "No s'ha trobat progrés remot"
|
STR_NO_REMOTE_MSG: "No s'ha trobat progrés remot"
|
||||||
STR_UPLOAD_PROMPT: "Voleu pujar la posició actual?"
|
STR_UPLOAD_PROMPT: "Vols pujar la posició actual?"
|
||||||
STR_UPLOAD_SUCCESS: "Progrés pujat!"
|
STR_UPLOAD_SUCCESS: "Progrés pujat!"
|
||||||
STR_SYNC_FAILED_MSG: "Sincronització fallida"
|
STR_SYNC_FAILED_MSG: "Sincronització fallida"
|
||||||
STR_SAVE_PROGRESS_FAILED: "No s'ha pogut desar el progrés"
|
STR_SAVE_PROGRESS_FAILED: "No s'ha pogut desar el progrés"
|
||||||
@@ -305,13 +311,14 @@ STR_NO_FOOTNOTES: "No hi ha notes al peu en aquesta pàgina"
|
|||||||
STR_LINK: "[enllaç]"
|
STR_LINK: "[enllaç]"
|
||||||
STR_SLEEP_TIMER_VALUE_FORMAT: "%u min"
|
STR_SLEEP_TIMER_VALUE_FORMAT: "%u min"
|
||||||
STR_SLEEP_NEVER: "Mai"
|
STR_SLEEP_NEVER: "Mai"
|
||||||
STR_SLEEP_TIMER_STEP_HINT: "Esquerra/Dreta: 1 min Amunt/Avall: 5 min"
|
STR_STEP_HINT_FRONT: "Botons frontals:"
|
||||||
|
STR_STEP_HINT_SIDE: "Botons laterals:"
|
||||||
STR_SCREENSHOT_BUTTON: "Fes una captura de pantalla"
|
STR_SCREENSHOT_BUTTON: "Fes una captura de pantalla"
|
||||||
STR_AUTO_TURN_ENABLED: "Passar automàtic activat: "
|
STR_AUTO_TURN_ENABLED: "Pas automàtic de pàgina activat"
|
||||||
STR_AUTO_TURN_PAGES_PER_MIN: "Passar automàtic (pàgines per minut)"
|
STR_AUTO_TURN_PAGES_PER_MIN: "Pas automàtic de pàgina (pàg./min)"
|
||||||
STR_TILT_PAGE_TURN: "Pas de pàgina per inclinació"
|
STR_TILT_PAGE_TURN: "Pas de pàgina per inclinació"
|
||||||
STR_FORCE_REFRESH: "Refresca la pantalla"
|
STR_FORCE_REFRESH: "Refresca la pantalla"
|
||||||
STR_RESTARTING_HINT: "S'està reiniciant... Si el dispositiu no es reinicia, manteniu premut el botó d'encesa durant uns segons."
|
STR_RESTARTING_HINT: "S'està reiniciant... Si el dispositiu no es reinicia, mantén premut el botó d'encesa durant uns segons."
|
||||||
STR_NEXT_PAGE: "Pàgina següent »"
|
STR_NEXT_PAGE: "Pàgina següent »"
|
||||||
STR_PREV_PAGE: "« Pàgina anterior"
|
STR_PREV_PAGE: "« Pàgina anterior"
|
||||||
STR_XTC_STATUS_BAR: "Barra d'estat XTC"
|
STR_XTC_STATUS_BAR: "Barra d'estat XTC"
|
||||||
@@ -340,20 +347,20 @@ STR_SERVER_NAME: "Nom del servidor"
|
|||||||
STR_NO_SERVERS: "No hi ha servidors OPDS configurats"
|
STR_NO_SERVERS: "No hi ha servidors OPDS configurats"
|
||||||
STR_DELETE_SERVER: "Suprimeix el servidor"
|
STR_DELETE_SERVER: "Suprimeix el servidor"
|
||||||
STR_OPDS_SERVERS: "Servidors OPDS"
|
STR_OPDS_SERVERS: "Servidors OPDS"
|
||||||
STR_MANAGE_FONTS: "Gestiona els tipus de lletra"
|
STR_MANAGE_FONTS: "Gestiona les fonts"
|
||||||
STR_FONT_BROWSER: "Navegador de tipus de lletra"
|
STR_FONT_BROWSER: "Navegador de fonts"
|
||||||
STR_LOADING_FONT_LIST: "S'està carregant la llista de tipus de lletra..."
|
STR_LOADING_FONT_LIST: "S'està carregant la llista de fonts..."
|
||||||
STR_NO_FONTS_AVAILABLE: "No hi ha tipus de lletra disponibles"
|
STR_NO_FONTS_AVAILABLE: "No hi ha fonts disponibles"
|
||||||
STR_FONT_PREVIEW_TEXT: "Jove xef, porti whisky amb quinze glaçons d'hidrogen, coi!"
|
STR_FONT_PREVIEW_TEXT: "Jove xef, porti whisky amb quinze glaçons d'hidrogen, coi!"
|
||||||
STR_FONT_INSTALLED: "Tipus de lletra instal·lat!"
|
STR_FONT_INSTALLED: "Font instal·lada!"
|
||||||
STR_FONT_INSTALL_FAILED: "Ha fallat la instal·lació del tipus de lletra"
|
STR_FONT_INSTALL_FAILED: "Ha fallat la instal·lació de la font"
|
||||||
STR_INSTALLED: "Instal·lat"
|
STR_INSTALLED: "Instal·lat"
|
||||||
STR_DOWNLOAD_ALL: "Descarrega-ho tot"
|
STR_DOWNLOAD_ALL: "Descarrega-ho tot"
|
||||||
STR_UPDATE_ALL: "Actualitza-ho tot"
|
STR_UPDATE_ALL: "Actualitza-ho tot"
|
||||||
STR_UPDATE_AVAILABLE: "Actualitza"
|
STR_UPDATE_AVAILABLE: "Actualitza"
|
||||||
STR_CRASH_TITLE: "Bloqueig del sistema"
|
STR_CRASH_TITLE: "Fallada del sistema"
|
||||||
STR_CRASH_DESCRIPTION: "S'ha desat un informe detallat a crash_report.txt. Incloeu aquest fitxer a l'informe d'errors."
|
STR_CRASH_DESCRIPTION: "S'ha desat un informe detallat a crash_report.txt. Inclou aquest fitxer a l'informe d'errors."
|
||||||
STR_CRASH_REASON: "Motiu del bloqueig:"
|
STR_CRASH_REASON: "Motiu de la fallada"
|
||||||
STR_CRASH_NO_REASON: "(No s'ha registrat cap motiu)"
|
STR_CRASH_NO_REASON: "(No s'ha registrat cap motiu)"
|
||||||
STR_KB_HINT_MOVE_CURSOR: "Prem Esquerra o Dreta per moure el cursor"
|
STR_KB_HINT_MOVE_CURSOR: "Prem Esquerra o Dreta per moure el cursor"
|
||||||
STR_KB_HINT_RETURN_CURSOR: "Prem Esquerra per tornar a la posició del cursor"
|
STR_KB_HINT_RETURN_CURSOR: "Prem Esquerra per tornar a la posició del cursor"
|
||||||
@@ -366,20 +373,22 @@ STR_KB_TIPS: "Consells:"
|
|||||||
STR_KB_HINT_RETURN_KEYBOARD: "Prem Avall per tornar al teclat"
|
STR_KB_HINT_RETURN_KEYBOARD: "Prem Avall per tornar al teclat"
|
||||||
STR_KB_HINT_EXIT_URL_MODE: "Prem ABC per sortir del mode URL"
|
STR_KB_HINT_EXIT_URL_MODE: "Prem ABC per sortir del mode URL"
|
||||||
STR_KB_HINT_CLEAR_TEXT: "Mantén premut DEL per esborrar tot el text"
|
STR_KB_HINT_CLEAR_TEXT: "Mantén premut DEL per esborrar tot el text"
|
||||||
STR_KB_HINT_SECONDARY_CHAR: "Mantén premut SELECT per al caràcter secundari"
|
STR_KB_HINT_SECONDARY_CHAR: "Mantén premut SELECT: caràcter secundari"
|
||||||
STR_KB_HINT_UPPER_SECONDARY: "Mantén premut SELECT per MAJÚSCULES o caràcter secundari"
|
STR_KB_HINT_UPPER_SECONDARY: "Mantén premut SELECT: majúscules o caràcter secundari"
|
||||||
STR_KB_HINT_LOWER_SECONDARY: "Mantén premut SELECT per minúscules o caràcter secundari"
|
STR_KB_HINT_LOWER_SECONDARY: "Mantén premut SELECT: minúscules o caràcter secundari"
|
||||||
STR_KB_HINT_URL_SNIPPETS: "Prem URL per inserir fragments"
|
STR_KB_HINT_URL_SNIPPETS: "Prem URL per inserir fragments"
|
||||||
STR_SD_FIRMWARE_UPDATE: "Actualització de firmware des de la targeta SD"
|
STR_SD_FIRMWARE_UPDATE: "Actualització de firmware des de la targeta SD"
|
||||||
STR_SELECT_FIRMWARE_FILE: "Seleccioneu un fitxer de firmware (.bin)"
|
STR_SELECT_FIRMWARE_FILE: "Selecciona un fitxer de firmware (.bin)"
|
||||||
STR_NO_BIN_FILES: "No s'han trobat fitxers .bin"
|
STR_NO_BIN_FILES: "No s'han trobat fitxers .bin"
|
||||||
STR_VALIDATING_FIRMWARE: "S'està validant el firmware..."
|
STR_VALIDATING_FIRMWARE: "S'està validant el firmware..."
|
||||||
STR_INVALID_FIRMWARE: "Fitxer de firmware no vàlid"
|
STR_INVALID_FIRMWARE: "Fitxer de firmware no vàlid"
|
||||||
STR_FIRMWARE_TOO_LARGE: "El firmware és massa gran per a la partició"
|
STR_FIRMWARE_TOO_LARGE: "El firmware és massa gran per a la partició"
|
||||||
STR_FIRMWARE_TOO_SMALL: "El fitxer de firmware és massa petit"
|
STR_FIRMWARE_TOO_SMALL: "El fitxer de firmware és massa petit"
|
||||||
STR_FIRMWARE_UPDATE_PROMPT: "Voleu actualitzar el firmware?"
|
STR_FIRMWARE_UPDATE_PROMPT: "Vols actualitzar el firmware?"
|
||||||
STR_FIRMWARE_FILE_OPEN_FAILED: "No es pot obrir el fitxer"
|
STR_FIRMWARE_FILE_OPEN_FAILED: "No es pot obrir el fitxer"
|
||||||
STR_FIRMWARE_WRITE_FAILED: "Ha fallat l'escriptura del firmware"
|
STR_FIRMWARE_WRITE_FAILED: "Ha fallat l'escriptura del firmware"
|
||||||
STR_FIRMWARE_UPDATE_DO_NOT_POWER_OFF: "No apagueu el dispositiu!"
|
STR_FIRMWARE_UPDATE_DO_NOT_POWER_OFF: "No apaguis el dispositiu!"
|
||||||
STR_RECOVERY_MODE: "Mode de recuperació"
|
STR_RECOVERY_MODE: "Mode de recuperació"
|
||||||
STR_RECOVERY_MODE_HINT: "Poseu firmware.bin a l'arrel de la targeta SD i seleccioneu-lo"
|
STR_RECOVERY_MODE_HINT: "Posa firmware.bin a l'arrel de la targeta SD i selecciona'l"
|
||||||
|
STR_ADD_HIDDEN_NETWORK: "Afegeix una xarxa oculta..."
|
||||||
|
STR_ENTER_WIFI_SSID: "Introdueix el nom de la xarxa (SSID)"
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ STR_NETWORK_LEGEND: "* = Šifrováno | + = Uloženo"
|
|||||||
STR_MAC_ADDRESS: "MAC adresa:"
|
STR_MAC_ADDRESS: "MAC adresa:"
|
||||||
STR_CHECKING_WIFI: "Kontrola Wi-Fi..."
|
STR_CHECKING_WIFI: "Kontrola Wi-Fi..."
|
||||||
STR_ENTER_WIFI_PASSWORD: "Zadejte heslo Wi-Fi"
|
STR_ENTER_WIFI_PASSWORD: "Zadejte heslo Wi-Fi"
|
||||||
STR_TO_PREFIX: "pro"
|
STR_TO_PREFIX: "k "
|
||||||
STR_CALIBRE_RECEIVING: "Příjem:"
|
STR_CALIBRE_RECEIVING: "Příjem:"
|
||||||
STR_CALIBRE_RECEIVED: "Přijato:"
|
STR_CALIBRE_RECEIVED: "Přijato:"
|
||||||
STR_CALIBRE_INSTRUCTION_1: "1) Nainstalujte plugin CrossPoint Reader"
|
STR_CALIBRE_INSTRUCTION_1: "1) Nainstalujte plugin CrossPoint Reader"
|
||||||
@@ -58,7 +58,7 @@ STR_CALIBRE_INSTRUCTION_3: "3) V Calibre: „Odeslat do zařízení“"
|
|||||||
STR_CALIBRE_INSTRUCTION_4: "„Při odesílání ponechat tuto obrazovku otevřenou“"
|
STR_CALIBRE_INSTRUCTION_4: "„Při odesílání ponechat tuto obrazovku otevřenou“"
|
||||||
STR_CAT_DISPLAY: "Displej"
|
STR_CAT_DISPLAY: "Displej"
|
||||||
STR_CAT_READER: "Čtečka"
|
STR_CAT_READER: "Čtečka"
|
||||||
STR_CAT_CONTROLS: "Ovládací prvky"
|
STR_CAT_CONTROLS: "Ovládání"
|
||||||
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"
|
||||||
@@ -92,6 +92,7 @@ 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"
|
||||||
STR_DOCUMENT_MATCHING: "Párování dokumentů"
|
STR_DOCUMENT_MATCHING: "Párování dokumentů"
|
||||||
|
STR_SEND_METADATA: "Odesílat metadata dokumentu"
|
||||||
STR_AUTHENTICATE: "Ověření"
|
STR_AUTHENTICATE: "Ověření"
|
||||||
STR_KOREADER_USERNAME: "Uživ. jméno KOReaderu"
|
STR_KOREADER_USERNAME: "Uživ. jméno KOReaderu"
|
||||||
STR_KOREADER_PASSWORD: "Heslo KOReaderu"
|
STR_KOREADER_PASSWORD: "Heslo KOReaderu"
|
||||||
@@ -243,7 +244,6 @@ STR_CHAPTER_PREFIX: "Kapitola:"
|
|||||||
STR_PAGES_SEPARATOR: "stránek |"
|
STR_PAGES_SEPARATOR: "stránek |"
|
||||||
STR_BOOK_PREFIX: "Kniha:"
|
STR_BOOK_PREFIX: "Kniha:"
|
||||||
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_SYNCING_TIME: "Čas synchronizace..."
|
STR_SYNCING_TIME: "Čas synchronizace..."
|
||||||
STR_CALC_HASH: "Výpočet hashe dokumentu..."
|
STR_CALC_HASH: "Výpočet hashe dokumentu..."
|
||||||
STR_HASH_FAILED: "Nepodařilo se vypočítat hash dokumentu"
|
STR_HASH_FAILED: "Nepodařilo se vypočítat hash dokumentu"
|
||||||
@@ -273,5 +273,8 @@ STR_OPDS_SERVER_URL: "URL serveru OPDS"
|
|||||||
STR_SCREENSHOT_BUTTON: "Udělat snímek obrazovky"
|
STR_SCREENSHOT_BUTTON: "Udělat snímek obrazovky"
|
||||||
STR_SLEEP_TIMER_VALUE_FORMAT: "%u min"
|
STR_SLEEP_TIMER_VALUE_FORMAT: "%u min"
|
||||||
STR_SLEEP_NEVER: "Nikdy"
|
STR_SLEEP_NEVER: "Nikdy"
|
||||||
STR_SLEEP_TIMER_STEP_HINT: "Vlevo/Vpravo: 1 min Nahoru/Dolů: 5 min"
|
STR_STEP_HINT_FRONT: "Přední tlačítka:"
|
||||||
|
STR_STEP_HINT_SIDE: "Boční tlačítka:"
|
||||||
STR_TILT_PAGE_TURN: "Otáčení stránek nakloněním"
|
STR_TILT_PAGE_TURN: "Otáčení stránek nakloněním"
|
||||||
|
STR_ADD_HIDDEN_NETWORK: "Přidat skrytou síť..."
|
||||||
|
STR_ENTER_WIFI_SSID: "Zadejte název sítě (SSID)"
|
||||||
|
|||||||
@@ -97,6 +97,7 @@ STR_USERNAME: "Brugernavn"
|
|||||||
STR_PASSWORD: "Adgangskode"
|
STR_PASSWORD: "Adgangskode"
|
||||||
STR_SYNC_SERVER_URL: "Synkroniseringsserver-URL"
|
STR_SYNC_SERVER_URL: "Synkroniseringsserver-URL"
|
||||||
STR_DOCUMENT_MATCHING: "Dokumentsammenkobling"
|
STR_DOCUMENT_MATCHING: "Dokumentsammenkobling"
|
||||||
|
STR_SEND_METADATA: "Send dokumentmetadata"
|
||||||
STR_AUTHENTICATE: "Godkend"
|
STR_AUTHENTICATE: "Godkend"
|
||||||
STR_KOREADER_USERNAME: "KOReader brugernavn"
|
STR_KOREADER_USERNAME: "KOReader brugernavn"
|
||||||
STR_KOREADER_PASSWORD: "KOReader adgangskode"
|
STR_KOREADER_PASSWORD: "KOReader adgangskode"
|
||||||
@@ -266,7 +267,6 @@ STR_CHAPTER_PREFIX: "Kapitel: "
|
|||||||
STR_PAGES_SEPARATOR: " sider | "
|
STR_PAGES_SEPARATOR: " sider | "
|
||||||
STR_BOOK_PREFIX: "Bog: "
|
STR_BOOK_PREFIX: "Bog: "
|
||||||
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_SYNCING_TIME: "Synkroniserer tid..."
|
STR_SYNCING_TIME: "Synkroniserer tid..."
|
||||||
STR_CALC_HASH: "Beregner dokument-hash..."
|
STR_CALC_HASH: "Beregner dokument-hash..."
|
||||||
STR_HASH_FAILED: "Kunne ikke beregne dokument-hash"
|
STR_HASH_FAILED: "Kunne ikke beregne dokument-hash"
|
||||||
@@ -298,8 +298,11 @@ STR_NO_FOOTNOTES: "Ingen fodnoter på denne side"
|
|||||||
STR_LINK: "[link]"
|
STR_LINK: "[link]"
|
||||||
STR_SLEEP_TIMER_VALUE_FORMAT: "%u min"
|
STR_SLEEP_TIMER_VALUE_FORMAT: "%u min"
|
||||||
STR_SLEEP_NEVER: "Aldrig"
|
STR_SLEEP_NEVER: "Aldrig"
|
||||||
STR_SLEEP_TIMER_STEP_HINT: "Venstre/Højre: 1 min Op/Ned: 5 min"
|
STR_STEP_HINT_FRONT: "Frontknapper:"
|
||||||
|
STR_STEP_HINT_SIDE: "Sideknapper:"
|
||||||
STR_SCREENSHOT_BUTTON: "Tag skærmbillede"
|
STR_SCREENSHOT_BUTTON: "Tag skærmbillede"
|
||||||
STR_AUTO_TURN_ENABLED: "Automatisk sidevendning aktiveret: "
|
STR_AUTO_TURN_ENABLED: "Automatisk sidevendning aktiveret: "
|
||||||
STR_AUTO_TURN_PAGES_PER_MIN: "Automatisk vending (sider per minut)"
|
STR_AUTO_TURN_PAGES_PER_MIN: "Automatisk vending (sider per minut)"
|
||||||
STR_TILT_PAGE_TURN: "Vip for at vende side"
|
STR_TILT_PAGE_TURN: "Vip for at vende side"
|
||||||
|
STR_ADD_HIDDEN_NETWORK: "Tilføj skjult netværk..."
|
||||||
|
STR_ENTER_WIFI_SSID: "Indtast netværksnavn (SSID)"
|
||||||
|
|||||||
@@ -97,6 +97,7 @@ STR_USERNAME: "Gebruikersnaam"
|
|||||||
STR_PASSWORD: "Wachtwoord"
|
STR_PASSWORD: "Wachtwoord"
|
||||||
STR_SYNC_SERVER_URL: "Sync-server URL"
|
STR_SYNC_SERVER_URL: "Sync-server URL"
|
||||||
STR_DOCUMENT_MATCHING: "Documentkoppeling"
|
STR_DOCUMENT_MATCHING: "Documentkoppeling"
|
||||||
|
STR_SEND_METADATA: "Documentmetadata versturen"
|
||||||
STR_AUTHENTICATE: "Authenticatie"
|
STR_AUTHENTICATE: "Authenticatie"
|
||||||
STR_KOREADER_USERNAME: "KOReader gebruikersnaam"
|
STR_KOREADER_USERNAME: "KOReader gebruikersnaam"
|
||||||
STR_KOREADER_PASSWORD: "KOReader wachtwoord"
|
STR_KOREADER_PASSWORD: "KOReader wachtwoord"
|
||||||
@@ -266,7 +267,6 @@ STR_CHAPTER_PREFIX: "Hoofdstuk: "
|
|||||||
STR_PAGES_SEPARATOR: " pagina's | "
|
STR_PAGES_SEPARATOR: " pagina's | "
|
||||||
STR_BOOK_PREFIX: "Boek: "
|
STR_BOOK_PREFIX: "Boek: "
|
||||||
STR_CALIBRE_URL_HINT: "Voeg voor Calibre /opds toe aan de URL"
|
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_SYNCING_TIME: "Tijd synchroniseren..."
|
||||||
STR_CALC_HASH: "Document-hash berekenen..."
|
STR_CALC_HASH: "Document-hash berekenen..."
|
||||||
STR_HASH_FAILED: "Document-hash berekenen mislukt"
|
STR_HASH_FAILED: "Document-hash berekenen mislukt"
|
||||||
@@ -298,8 +298,11 @@ STR_NO_FOOTNOTES: "Geen voetnoten op deze pagina"
|
|||||||
STR_LINK: "[link]"
|
STR_LINK: "[link]"
|
||||||
STR_SLEEP_TIMER_VALUE_FORMAT: "%u min"
|
STR_SLEEP_TIMER_VALUE_FORMAT: "%u min"
|
||||||
STR_SLEEP_NEVER: "Nooit"
|
STR_SLEEP_NEVER: "Nooit"
|
||||||
STR_SLEEP_TIMER_STEP_HINT: "Links/Rechts: 1 min Omhoog/Omlaag: 5 min"
|
STR_STEP_HINT_FRONT: "Voorknoppen:"
|
||||||
|
STR_STEP_HINT_SIDE: "Zijknoppen:"
|
||||||
STR_SCREENSHOT_BUTTON: "Screenshot maken"
|
STR_SCREENSHOT_BUTTON: "Screenshot maken"
|
||||||
STR_AUTO_TURN_ENABLED: "Automatisch omslaan ingeschakeld: "
|
STR_AUTO_TURN_ENABLED: "Automatisch omslaan ingeschakeld: "
|
||||||
STR_AUTO_TURN_PAGES_PER_MIN: "Autom. omslaan (pagina's per minuut)"
|
STR_AUTO_TURN_PAGES_PER_MIN: "Autom. omslaan (pagina's per minuut)"
|
||||||
STR_TILT_PAGE_TURN: "Kantel om te bladeren"
|
STR_TILT_PAGE_TURN: "Kantel om te bladeren"
|
||||||
|
STR_ADD_HIDDEN_NETWORK: "Verborgen netwerk toevoegen..."
|
||||||
|
STR_ENTER_WIFI_SSID: "Voer netwerknaam in (SSID)"
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ STR_NO_CHAPTERS: "No chapters"
|
|||||||
STR_END_OF_BOOK: "End of book"
|
STR_END_OF_BOOK: "End of book"
|
||||||
STR_EMPTY_CHAPTER: "Empty chapter"
|
STR_EMPTY_CHAPTER: "Empty chapter"
|
||||||
STR_INDEXING: "Indexing"
|
STR_INDEXING: "Indexing"
|
||||||
|
STR_INDEX_FAILED: "Failed to index - invalid book"
|
||||||
STR_MEMORY_ERROR: "Memory error"
|
STR_MEMORY_ERROR: "Memory error"
|
||||||
STR_PAGE_LOAD_ERROR: "Page load error"
|
STR_PAGE_LOAD_ERROR: "Page load error"
|
||||||
STR_EMPTY_FILE: "Empty file"
|
STR_EMPTY_FILE: "Empty file"
|
||||||
@@ -28,7 +29,10 @@ STR_WIFI_NETWORKS: "Wi-Fi 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"
|
||||||
STR_SCANNING: "Scanning..."
|
STR_SCANNING: "Scanning..."
|
||||||
|
STR_FINDING_SAVED_WIFI: "Finding saved Wi-Fi..."
|
||||||
STR_CONNECTING: "Connecting..."
|
STR_CONNECTING: "Connecting..."
|
||||||
|
STR_CONNECTING_SAVED_WIFI: "Connecting to saved Wi-Fi..."
|
||||||
|
STR_SHOW_NETWORKS: "Show"
|
||||||
STR_CONNECTED: "Connected!"
|
STR_CONNECTED: "Connected!"
|
||||||
STR_CONNECTION_FAILED: "Connection Failed"
|
STR_CONNECTION_FAILED: "Connection Failed"
|
||||||
STR_FORGET_NETWORK: "Forget Network?"
|
STR_FORGET_NETWORK: "Forget Network?"
|
||||||
@@ -49,6 +53,8 @@ STR_NETWORK_LEGEND: "* = Encrypted | + = Saved"
|
|||||||
STR_MAC_ADDRESS: "MAC address:"
|
STR_MAC_ADDRESS: "MAC address:"
|
||||||
STR_CHECKING_WIFI: "Checking Wi-Fi..."
|
STR_CHECKING_WIFI: "Checking Wi-Fi..."
|
||||||
STR_ENTER_WIFI_PASSWORD: "Enter Wi-Fi password"
|
STR_ENTER_WIFI_PASSWORD: "Enter Wi-Fi password"
|
||||||
|
STR_ADD_HIDDEN_NETWORK: "Add hidden network..."
|
||||||
|
STR_ENTER_WIFI_SSID: "Enter network name (SSID)"
|
||||||
STR_TO_PREFIX: "to "
|
STR_TO_PREFIX: "to "
|
||||||
STR_CALIBRE_RECEIVING: "Receiving: "
|
STR_CALIBRE_RECEIVING: "Receiving: "
|
||||||
STR_CALIBRE_RECEIVED: "Received: "
|
STR_CALIBRE_RECEIVED: "Received: "
|
||||||
@@ -70,6 +76,8 @@ STR_IMAGES: "Images"
|
|||||||
STR_IMAGES_DISPLAY: "Display"
|
STR_IMAGES_DISPLAY: "Display"
|
||||||
STR_IMAGES_PLACEHOLDER: "Placeholder"
|
STR_IMAGES_PLACEHOLDER: "Placeholder"
|
||||||
STR_IMAGES_SUPPRESS: "Suppress"
|
STR_IMAGES_SUPPRESS: "Suppress"
|
||||||
|
STR_EOB_HOME: "Home"
|
||||||
|
STR_EOB_CONTINUE_WITH: "Continue with"
|
||||||
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_SIDE_BTN_LAYOUT: "Side Button Layout (reader)"
|
STR_SIDE_BTN_LAYOUT: "Side Button Layout (reader)"
|
||||||
@@ -99,6 +107,7 @@ STR_USERNAME: "Username"
|
|||||||
STR_PASSWORD: "Password"
|
STR_PASSWORD: "Password"
|
||||||
STR_SYNC_SERVER_URL: "Sync Server URL"
|
STR_SYNC_SERVER_URL: "Sync Server URL"
|
||||||
STR_DOCUMENT_MATCHING: "Document Matching"
|
STR_DOCUMENT_MATCHING: "Document Matching"
|
||||||
|
STR_SEND_METADATA: "Send Document Metadata"
|
||||||
STR_AUTHENTICATE: "Authenticate"
|
STR_AUTHENTICATE: "Authenticate"
|
||||||
STR_KOREADER_USERNAME: "KOReader Username"
|
STR_KOREADER_USERNAME: "KOReader Username"
|
||||||
STR_KOREADER_PASSWORD: "KOReader Password"
|
STR_KOREADER_PASSWORD: "KOReader Password"
|
||||||
@@ -220,7 +229,7 @@ STR_OK_BUTTON: "OK"
|
|||||||
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"
|
||||||
STR_CHAPTER_PAGE_COUNT: "Chapter Page Count"
|
STR_CHAPTER_PAGE_COUNT: "Page Count"
|
||||||
STR_BOOK_PROGRESS_PERCENTAGE: "Book Progress Percentage"
|
STR_BOOK_PROGRESS_PERCENTAGE: "Book Progress Percentage"
|
||||||
STR_PROGRESS_BAR: "Progress Bar"
|
STR_PROGRESS_BAR: "Progress Bar"
|
||||||
STR_PROGRESS_BAR_THICKNESS: "Progress Bar Thickness"
|
STR_PROGRESS_BAR_THICKNESS: "Progress Bar Thickness"
|
||||||
@@ -297,7 +306,6 @@ STR_CHAPTER_PREFIX: "Chapter: "
|
|||||||
STR_PAGES_SEPARATOR: " pages | "
|
STR_PAGES_SEPARATOR: " pages | "
|
||||||
STR_BOOK_PREFIX: "Book: "
|
STR_BOOK_PREFIX: "Book: "
|
||||||
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_SYNCING_TIME: "Syncing time..."
|
STR_SYNCING_TIME: "Syncing time..."
|
||||||
STR_CALC_HASH: "Calculating document hash..."
|
STR_CALC_HASH: "Calculating document hash..."
|
||||||
STR_HASH_FAILED: "Failed to calculate document hash"
|
STR_HASH_FAILED: "Failed to calculate document hash"
|
||||||
@@ -332,7 +340,8 @@ STR_LINK: "[link]"
|
|||||||
STR_SCREENSHOT_BUTTON: "Take screenshot"
|
STR_SCREENSHOT_BUTTON: "Take screenshot"
|
||||||
STR_SLEEP_TIMER_VALUE_FORMAT: "%u min"
|
STR_SLEEP_TIMER_VALUE_FORMAT: "%u min"
|
||||||
STR_SLEEP_NEVER: "Never"
|
STR_SLEEP_NEVER: "Never"
|
||||||
STR_SLEEP_TIMER_STEP_HINT: "Left/Right: 1 min Up/Down: 5 min"
|
STR_STEP_HINT_FRONT: "Front buttons:"
|
||||||
|
STR_STEP_HINT_SIDE: "Side buttons:"
|
||||||
STR_ADD_SERVER: "Add Server"
|
STR_ADD_SERVER: "Add Server"
|
||||||
STR_SERVER_NAME: "Server Name"
|
STR_SERVER_NAME: "Server Name"
|
||||||
STR_NO_SERVERS: "No OPDS servers configured"
|
STR_NO_SERVERS: "No OPDS servers configured"
|
||||||
|
|||||||
@@ -92,6 +92,7 @@ STR_USERNAME: "Käyttäjänimi"
|
|||||||
STR_PASSWORD: "Salasana"
|
STR_PASSWORD: "Salasana"
|
||||||
STR_SYNC_SERVER_URL: "Synkronointipalvelimen osoite"
|
STR_SYNC_SERVER_URL: "Synkronointipalvelimen osoite"
|
||||||
STR_DOCUMENT_MATCHING: "Dokumenttien tunnistus"
|
STR_DOCUMENT_MATCHING: "Dokumenttien tunnistus"
|
||||||
|
STR_SEND_METADATA: "Lähetä asiakirjan metatiedot"
|
||||||
STR_AUTHENTICATE: "Tunnistaudu"
|
STR_AUTHENTICATE: "Tunnistaudu"
|
||||||
STR_KOREADER_USERNAME: "KOReader-käyttäjänimi"
|
STR_KOREADER_USERNAME: "KOReader-käyttäjänimi"
|
||||||
STR_KOREADER_PASSWORD: "KOReader-salasana"
|
STR_KOREADER_PASSWORD: "KOReader-salasana"
|
||||||
@@ -241,7 +242,6 @@ STR_CHAPTER_PREFIX: "Luku: "
|
|||||||
STR_PAGES_SEPARATOR: " sivua | "
|
STR_PAGES_SEPARATOR: " sivua | "
|
||||||
STR_BOOK_PREFIX: "Kirja: "
|
STR_BOOK_PREFIX: "Kirja: "
|
||||||
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_SYNCING_TIME: "Synkronoidaan aikaa..."
|
STR_SYNCING_TIME: "Synkronoidaan aikaa..."
|
||||||
STR_CALC_HASH: "Lasketaan dokumentin tarkistussummaa..."
|
STR_CALC_HASH: "Lasketaan dokumentin tarkistussummaa..."
|
||||||
STR_HASH_FAILED: "Dokumentin tiivisteen laskenta epäonnistui"
|
STR_HASH_FAILED: "Dokumentin tiivisteen laskenta epäonnistui"
|
||||||
@@ -271,5 +271,8 @@ STR_OPDS_SERVER_URL: "OPDS-palvelimen osoite"
|
|||||||
STR_SCREENSHOT_BUTTON: "Ota kuvakaappaus"
|
STR_SCREENSHOT_BUTTON: "Ota kuvakaappaus"
|
||||||
STR_SLEEP_TIMER_VALUE_FORMAT: "%u min"
|
STR_SLEEP_TIMER_VALUE_FORMAT: "%u min"
|
||||||
STR_SLEEP_NEVER: "Ei koskaan"
|
STR_SLEEP_NEVER: "Ei koskaan"
|
||||||
STR_SLEEP_TIMER_STEP_HINT: "Vasen/Oikea: 1 min Ylös/Alas: 5 min"
|
STR_STEP_HINT_FRONT: "Etupainikkeet:"
|
||||||
|
STR_STEP_HINT_SIDE: "Sivupainikkeet:"
|
||||||
STR_TILT_PAGE_TURN: "Sivunkääntö kallistamalla"
|
STR_TILT_PAGE_TURN: "Sivunkääntö kallistamalla"
|
||||||
|
STR_ADD_HIDDEN_NETWORK: "Lisää piilotettu verkko..."
|
||||||
|
STR_ENTER_WIFI_SSID: "Anna verkon nimi (SSID)"
|
||||||
|
|||||||
@@ -97,6 +97,7 @@ STR_USERNAME: "Nom d’utilisateur"
|
|||||||
STR_PASSWORD: "Mot de passe"
|
STR_PASSWORD: "Mot de passe"
|
||||||
STR_SYNC_SERVER_URL: "URL du serveur"
|
STR_SYNC_SERVER_URL: "URL du serveur"
|
||||||
STR_DOCUMENT_MATCHING: "Correspondance"
|
STR_DOCUMENT_MATCHING: "Correspondance"
|
||||||
|
STR_SEND_METADATA: "Envoyer métadonnées"
|
||||||
STR_AUTHENTICATE: "Connexion"
|
STR_AUTHENTICATE: "Connexion"
|
||||||
STR_KOREADER_USERNAME: "Utilisateur"
|
STR_KOREADER_USERNAME: "Utilisateur"
|
||||||
STR_KOREADER_PASSWORD: "Mot de passe"
|
STR_KOREADER_PASSWORD: "Mot de passe"
|
||||||
@@ -267,7 +268,6 @@ STR_CHAPTER_PREFIX: "Chapitre : "
|
|||||||
STR_PAGES_SEPARATOR: " pages | "
|
STR_PAGES_SEPARATOR: " pages | "
|
||||||
STR_BOOK_PREFIX: "Livre : "
|
STR_BOOK_PREFIX: "Livre : "
|
||||||
STR_CALIBRE_URL_HINT: "Pour Calibre, ajoutez /opds à l’URL"
|
STR_CALIBRE_URL_HINT: "Pour Calibre, ajoutez /opds à l’URL"
|
||||||
STR_PERCENT_STEP_HINT: "Gauche/Droite : 1% Haut/Bas : 10%"
|
|
||||||
STR_SYNCING_TIME: "Synchro de l’heure…"
|
STR_SYNCING_TIME: "Synchro de l’heure…"
|
||||||
STR_CALC_HASH: "Calcul hash doc…"
|
STR_CALC_HASH: "Calcul hash doc…"
|
||||||
STR_HASH_FAILED: "Échec calcul hash"
|
STR_HASH_FAILED: "Échec calcul hash"
|
||||||
@@ -299,8 +299,11 @@ STR_NO_FOOTNOTES: "Aucune note sur cette page"
|
|||||||
STR_LINK: "[lien]"
|
STR_LINK: "[lien]"
|
||||||
STR_SLEEP_TIMER_VALUE_FORMAT: "%u min"
|
STR_SLEEP_TIMER_VALUE_FORMAT: "%u min"
|
||||||
STR_SLEEP_NEVER: "Jamais"
|
STR_SLEEP_NEVER: "Jamais"
|
||||||
STR_SLEEP_TIMER_STEP_HINT: "Gauche/Droite : 1 min Haut/Bas : 5 min"
|
STR_STEP_HINT_FRONT: "Boutons avant :"
|
||||||
|
STR_STEP_HINT_SIDE: "Boutons latéraux :"
|
||||||
STR_SCREENSHOT_BUTTON: "Capture d'écran"
|
STR_SCREENSHOT_BUTTON: "Capture d'écran"
|
||||||
STR_AUTO_TURN_ENABLED: "Tourne-page auto : "
|
STR_AUTO_TURN_ENABLED: "Tourne-page auto : "
|
||||||
STR_AUTO_TURN_PAGES_PER_MIN: "Tourne-page auto (pages par minute)"
|
STR_AUTO_TURN_PAGES_PER_MIN: "Tourne-page auto (pages par minute)"
|
||||||
STR_TILT_PAGE_TURN: "Tourner par inclinaison"
|
STR_TILT_PAGE_TURN: "Tourner par inclinaison"
|
||||||
|
STR_ADD_HIDDEN_NETWORK: "Ajouter un réseau masqué..."
|
||||||
|
STR_ENTER_WIFI_SSID: "Saisir le nom du réseau (SSID)"
|
||||||
|
|||||||
@@ -91,6 +91,7 @@ STR_USERNAME: "Benutzername"
|
|||||||
STR_PASSWORD: "Passwort"
|
STR_PASSWORD: "Passwort"
|
||||||
STR_SYNC_SERVER_URL: "Sync-Server-URL"
|
STR_SYNC_SERVER_URL: "Sync-Server-URL"
|
||||||
STR_DOCUMENT_MATCHING: "Dateizuordnung"
|
STR_DOCUMENT_MATCHING: "Dateizuordnung"
|
||||||
|
STR_SEND_METADATA: "Metadaten senden"
|
||||||
STR_AUTHENTICATE: "Authentifizieren"
|
STR_AUTHENTICATE: "Authentifizieren"
|
||||||
STR_KOREADER_USERNAME: "KOReader-Benutzername"
|
STR_KOREADER_USERNAME: "KOReader-Benutzername"
|
||||||
STR_KOREADER_PASSWORD: "KOReader-Passwort"
|
STR_KOREADER_PASSWORD: "KOReader-Passwort"
|
||||||
@@ -288,7 +289,6 @@ STR_CHAPTER_PREFIX: "Kapitel:"
|
|||||||
STR_PAGES_SEPARATOR: " Seiten | "
|
STR_PAGES_SEPARATOR: " Seiten | "
|
||||||
STR_BOOK_PREFIX: "Buch: "
|
STR_BOOK_PREFIX: "Buch: "
|
||||||
STR_CALIBRE_URL_HINT: "Calibre: URL um /opds ergänzen"
|
STR_CALIBRE_URL_HINT: "Calibre: URL um /opds ergänzen"
|
||||||
STR_PERCENT_STEP_HINT: "links/rechts: 1% hoch/runter: 10%"
|
|
||||||
STR_SYNCING_TIME: "Zeit synchronisieren…"
|
STR_SYNCING_TIME: "Zeit synchronisieren…"
|
||||||
STR_CALC_HASH: "Dokument-Hash berechnen…"
|
STR_CALC_HASH: "Dokument-Hash berechnen…"
|
||||||
STR_HASH_FAILED: "Dokument-Hash fehlgeschlagen"
|
STR_HASH_FAILED: "Dokument-Hash fehlgeschlagen"
|
||||||
@@ -322,7 +322,8 @@ STR_NO_FOOTNOTES: "Keine Fußnoten auf dieser Seite"
|
|||||||
STR_LINK: "[Link]"
|
STR_LINK: "[Link]"
|
||||||
STR_SLEEP_TIMER_VALUE_FORMAT: "%u Min."
|
STR_SLEEP_TIMER_VALUE_FORMAT: "%u Min."
|
||||||
STR_SLEEP_NEVER: "Nie"
|
STR_SLEEP_NEVER: "Nie"
|
||||||
STR_SLEEP_TIMER_STEP_HINT: "Links/Rechts: 1 Min. Hoch/Runter: 5 Min."
|
STR_STEP_HINT_FRONT: "Vordere Tasten:"
|
||||||
|
STR_STEP_HINT_SIDE: "Seitentasten:"
|
||||||
STR_ADD_SERVER: "Server hinzufügen"
|
STR_ADD_SERVER: "Server hinzufügen"
|
||||||
STR_SERVER_NAME: "Servername"
|
STR_SERVER_NAME: "Servername"
|
||||||
STR_NO_SERVERS: "Keine OPDS-Server konfiguriert"
|
STR_NO_SERVERS: "Keine OPDS-Server konfiguriert"
|
||||||
@@ -380,3 +381,5 @@ STR_FIRMWARE_WRITE_FAILED: "Schreiben der Firmware-Datei ist fehlgeschlagen"
|
|||||||
STR_FIRMWARE_UPDATE_DO_NOT_POWER_OFF: "Nicht ausschalten!"
|
STR_FIRMWARE_UPDATE_DO_NOT_POWER_OFF: "Nicht ausschalten!"
|
||||||
STR_RECOVERY_MODE: "Wiederherstellungsmodus"
|
STR_RECOVERY_MODE: "Wiederherstellungsmodus"
|
||||||
STR_RECOVERY_MODE_HINT: "Lege firmware.bin im SD-Kartenwurzelverzeichnis ab und wähle es aus"
|
STR_RECOVERY_MODE_HINT: "Lege firmware.bin im SD-Kartenwurzelverzeichnis ab und wähle es aus"
|
||||||
|
STR_ADD_HIDDEN_NETWORK: "Verstecktes Netzwerk hinzufügen..."
|
||||||
|
STR_ENTER_WIFI_SSID: "Netzwerknamen eingeben (SSID)"
|
||||||
|
|||||||
@@ -95,6 +95,7 @@ STR_USERNAME: "שם משתמש"
|
|||||||
STR_PASSWORD: "סיסמה"
|
STR_PASSWORD: "סיסמה"
|
||||||
STR_SYNC_SERVER_URL: "כתובת שרת סנכרון"
|
STR_SYNC_SERVER_URL: "כתובת שרת סנכרון"
|
||||||
STR_DOCUMENT_MATCHING: "התאמת מסמכים"
|
STR_DOCUMENT_MATCHING: "התאמת מסמכים"
|
||||||
|
STR_SEND_METADATA: "שלח מטא-נתוני מסמך"
|
||||||
STR_AUTHENTICATE: "התחבר"
|
STR_AUTHENTICATE: "התחבר"
|
||||||
STR_KOREADER_USERNAME: "שם משתמש ב-KOReader"
|
STR_KOREADER_USERNAME: "שם משתמש ב-KOReader"
|
||||||
STR_KOREADER_PASSWORD: "סיסמת KOReader"
|
STR_KOREADER_PASSWORD: "סיסמת KOReader"
|
||||||
@@ -268,7 +269,6 @@ STR_CHAPTER_PREFIX: "פרק: "
|
|||||||
STR_PAGES_SEPARATOR: " דפים | "
|
STR_PAGES_SEPARATOR: " דפים | "
|
||||||
STR_BOOK_PREFIX: "ספר: "
|
STR_BOOK_PREFIX: "ספר: "
|
||||||
STR_CALIBRE_URL_HINT: "עבור Calibre, הוסף /opds לכתובת ה-URL"
|
STR_CALIBRE_URL_HINT: "עבור Calibre, הוסף /opds לכתובת ה-URL"
|
||||||
STR_PERCENT_STEP_HINT: "שמאל/ימין: 1% למעלה/למטה: 10%"
|
|
||||||
STR_SYNCING_TIME: "מסנכרן שעון..."
|
STR_SYNCING_TIME: "מסנכרן שעון..."
|
||||||
STR_CALC_HASH: "מחשב האש..."
|
STR_CALC_HASH: "מחשב האש..."
|
||||||
STR_HASH_FAILED: "חישוב האש נכשל"
|
STR_HASH_FAILED: "חישוב האש נכשל"
|
||||||
@@ -390,4 +390,7 @@ STR_BOOKMARK_OPTION: "סימנייה"
|
|||||||
STR_PWR_BTN_FOOTNOTE_BACK: "חזרה מהירה מהערות שוליים"
|
STR_PWR_BTN_FOOTNOTE_BACK: "חזרה מהירה מהערות שוליים"
|
||||||
STR_SLEEP_TIMER_VALUE_FORMAT: "%u דקות"
|
STR_SLEEP_TIMER_VALUE_FORMAT: "%u דקות"
|
||||||
STR_SLEEP_NEVER: "אף פעם"
|
STR_SLEEP_NEVER: "אף פעם"
|
||||||
STR_SLEEP_TIMER_STEP_HINT: "שמאל/ימין: 1 דק' למעלה/למטה: 5 דק'"
|
STR_STEP_HINT_FRONT: "לחצנים קדמיים:"
|
||||||
|
STR_STEP_HINT_SIDE: "לחצני צד:"
|
||||||
|
STR_ADD_HIDDEN_NETWORK: "הוסף רשת מוסתרת..."
|
||||||
|
STR_ENTER_WIFI_SSID: "הזן שם רשת (SSID)"
|
||||||
|
|||||||
@@ -49,6 +49,8 @@ STR_NETWORK_LEGEND: "* = Titkosított | + = Mentett"
|
|||||||
STR_MAC_ADDRESS: "MAC-cím:"
|
STR_MAC_ADDRESS: "MAC-cím:"
|
||||||
STR_CHECKING_WIFI: "Wi-Fi ellenőrzése..."
|
STR_CHECKING_WIFI: "Wi-Fi ellenőrzése..."
|
||||||
STR_ENTER_WIFI_PASSWORD: "Add meg a Wi-Fi jelszót"
|
STR_ENTER_WIFI_PASSWORD: "Add meg a Wi-Fi jelszót"
|
||||||
|
STR_ADD_HIDDEN_NETWORK: "Rejtett hálózat hozzáadása..."
|
||||||
|
STR_ENTER_WIFI_SSID: "Add meg a hálózat nevét (SSID)"
|
||||||
STR_TO_PREFIX: "- "
|
STR_TO_PREFIX: "- "
|
||||||
STR_CALIBRE_RECEIVING: "Fogadás: "
|
STR_CALIBRE_RECEIVING: "Fogadás: "
|
||||||
STR_CALIBRE_RECEIVED: "Fogadva: "
|
STR_CALIBRE_RECEIVED: "Fogadva: "
|
||||||
@@ -94,6 +96,7 @@ STR_USERNAME: "Felhasználónév"
|
|||||||
STR_PASSWORD: "Jelszó"
|
STR_PASSWORD: "Jelszó"
|
||||||
STR_SYNC_SERVER_URL: "Szinkronizálás szerver URL"
|
STR_SYNC_SERVER_URL: "Szinkronizálás szerver URL"
|
||||||
STR_DOCUMENT_MATCHING: "Dokumentum egyeztetés"
|
STR_DOCUMENT_MATCHING: "Dokumentum egyeztetés"
|
||||||
|
STR_SEND_METADATA: "Dokumentum metaadatok küldése"
|
||||||
STR_AUTHENTICATE: "Hitelesítés"
|
STR_AUTHENTICATE: "Hitelesítés"
|
||||||
STR_KOREADER_USERNAME: "KOReader felhasználónév"
|
STR_KOREADER_USERNAME: "KOReader felhasználónév"
|
||||||
STR_KOREADER_PASSWORD: "KOReader jelszó"
|
STR_KOREADER_PASSWORD: "KOReader jelszó"
|
||||||
@@ -263,7 +266,6 @@ STR_CHAPTER_PREFIX: "Fejezet: "
|
|||||||
STR_PAGES_SEPARATOR: " oldal | "
|
STR_PAGES_SEPARATOR: " oldal | "
|
||||||
STR_BOOK_PREFIX: "Könyv: "
|
STR_BOOK_PREFIX: "Könyv: "
|
||||||
STR_CALIBRE_URL_HINT: "Calibre esetén adj /opds-t az URL-hez"
|
STR_CALIBRE_URL_HINT: "Calibre esetén adj /opds-t az URL-hez"
|
||||||
STR_PERCENT_STEP_HINT: "Bal/Jobb: 1% Fel/Le: 10%"
|
|
||||||
STR_SYNCING_TIME: "Idő szinkronizálása..."
|
STR_SYNCING_TIME: "Idő szinkronizálása..."
|
||||||
STR_CALC_HASH: "Dokumentum hash kiszámítása..."
|
STR_CALC_HASH: "Dokumentum hash kiszámítása..."
|
||||||
STR_HASH_FAILED: "Dokumentum hash kiszámítása sikertelen"
|
STR_HASH_FAILED: "Dokumentum hash kiszámítása sikertelen"
|
||||||
@@ -295,8 +297,99 @@ STR_NO_FOOTNOTES: "Nincsenek lábjegyzetek ezen az oldalon"
|
|||||||
STR_LINK: "[link]"
|
STR_LINK: "[link]"
|
||||||
STR_SLEEP_TIMER_VALUE_FORMAT: "%u perc"
|
STR_SLEEP_TIMER_VALUE_FORMAT: "%u perc"
|
||||||
STR_SLEEP_NEVER: "Soha"
|
STR_SLEEP_NEVER: "Soha"
|
||||||
STR_SLEEP_TIMER_STEP_HINT: "Bal/Jobb: 1 perc Fel/Le: 5 perc"
|
STR_STEP_HINT_FRONT: "Elülső gombok:"
|
||||||
|
STR_STEP_HINT_SIDE: "Oldalsó gombok:"
|
||||||
STR_SCREENSHOT_BUTTON: "Képernyőkép készítése"
|
STR_SCREENSHOT_BUTTON: "Képernyőkép készítése"
|
||||||
STR_AUTO_TURN_ENABLED: "Automatikus lapozás bekapcsolva: "
|
STR_AUTO_TURN_ENABLED: "Automatikus lapozás bekapcsolva: "
|
||||||
STR_AUTO_TURN_PAGES_PER_MIN: "Automatikus lapozás (oldal/perc)"
|
STR_AUTO_TURN_PAGES_PER_MIN: "Automatikus lapozás (oldal/perc)"
|
||||||
STR_TILT_PAGE_TURN: "Döntéses lapozás"
|
STR_TILT_PAGE_TURN: "Döntéses lapozás"
|
||||||
|
STR_ADD_SERVER: "Szerver hozzáadása"
|
||||||
|
STR_BOOKMARKS: "Könyvjelzők"
|
||||||
|
STR_BOOKMARK_ADDED: "Könyvjelző hozzáadva."
|
||||||
|
STR_BOOKMARK_OPTION: "Könyvjelző"
|
||||||
|
STR_BOTTOM: "Alul"
|
||||||
|
STR_CLOCK: "Óra"
|
||||||
|
STR_CLOCK_FORMAT: "Óraformátum"
|
||||||
|
STR_CLOCK_FORMAT_12H: "12 órás"
|
||||||
|
STR_CLOCK_FORMAT_24H: "24 órás"
|
||||||
|
STR_CLOCK_SYNC: "Óra szinkronizálása"
|
||||||
|
STR_CLOCK_SYNCED: "Óra szinkronizálva"
|
||||||
|
STR_CLOCK_SYNCING: "Szinkronizálás NTP-ről..."
|
||||||
|
STR_CLOCK_SYNC_FAIL: "Szinkronizálás sikertelen"
|
||||||
|
STR_CLOCK_SYNC_NOW: "Óra szinkronizálása most"
|
||||||
|
STR_CLOCK_SYNC_NO_WIFI: "Wi-Fi nincs csatlakoztatva"
|
||||||
|
STR_CLOCK_SYNC_NO_WIFI_HINT: "Először csatlakozz Wi-Fi-hez, majd próbáld újra."
|
||||||
|
STR_CLOCK_SYNC_OK: "Óra szinkronizálva"
|
||||||
|
STR_CLOCK_UTC_OFFSET: "Óra UTC eltolás"
|
||||||
|
STR_CONFIRM_DELETE_BOOKMARK: "Törlöd ezt a könyvjelzőt?"
|
||||||
|
STR_CONNECTING_SAVED_WIFI: "Csatlakozás mentett Wi-Fi-hez..."
|
||||||
|
STR_CRASH_DESCRIPTION: "A részletes jelentés a crash_report.txt fájlba lett mentve. Kérjük, csatold ezt a fájlt a hibajelentéshez."
|
||||||
|
STR_CRASH_NO_REASON: "(Nem került rögzítésre ok)"
|
||||||
|
STR_CRASH_REASON: "Összeomlás oka:"
|
||||||
|
STR_CRASH_TITLE: "Rendszerösszeomlás"
|
||||||
|
STR_CURRENT_TIME: "Aktuális idő:"
|
||||||
|
STR_DELETE_SERVER: "Szerver törlése"
|
||||||
|
STR_DISABLED: "Letiltva"
|
||||||
|
STR_DOWNLOAD_ALL: "Összes letöltése"
|
||||||
|
STR_EOB_CONTINUE_WITH: "Folytatás ezzel"
|
||||||
|
STR_EOB_HOME: "Kezdőképernyő"
|
||||||
|
STR_FINDING_SAVED_WIFI: "Mentett Wi-Fi keresése..."
|
||||||
|
STR_FIRMWARE_FILE_OPEN_FAILED: "A fájl nem nyitható meg"
|
||||||
|
STR_FIRMWARE_TOO_LARGE: "A firmware túl nagy a partícióhoz"
|
||||||
|
STR_FIRMWARE_TOO_SMALL: "A firmware fájl túl kicsi"
|
||||||
|
STR_FIRMWARE_UPDATE_DO_NOT_POWER_OFF: "Ne kapcsold ki!"
|
||||||
|
STR_FIRMWARE_UPDATE_PROMPT: "Frissíted a firmware-t?"
|
||||||
|
STR_FIRMWARE_WRITE_FAILED: "A firmware írása sikertelen"
|
||||||
|
STR_FONT_BROWSER: "Betűtípus-böngésző"
|
||||||
|
STR_FONT_INSTALLED: "Betűtípus telepítve!"
|
||||||
|
STR_FONT_INSTALL_FAILED: "A betűtípus telepítése sikertelen"
|
||||||
|
STR_FORCE_REFRESH: "Képernyő frissítése"
|
||||||
|
STR_INDEX_FAILED: "Indexelés sikertelen – érvénytelen könyv"
|
||||||
|
STR_INSTALLED: "Telepítve"
|
||||||
|
STR_INVALID_FIRMWARE: "Érvénytelen firmware fájl"
|
||||||
|
STR_KB_HINT_CLEAR_TEXT: "Tartsd nyomva a DEL-t az összes szöveg törléséhez"
|
||||||
|
STR_KB_HINT_EDIT_ENTRY: "Tartsd nyomva a FEL-t a bejegyzés szerkesztéséhez"
|
||||||
|
STR_KB_HINT_EXIT_URL_MODE: "Nyomd meg az ABC-t az URL mód elhagyásához"
|
||||||
|
STR_KB_HINT_HIDE_PASSWORD: "Tartsd nyomva a JOBBRA-t, majd nyomd meg a [***]-ot a jelszó elrejtéséhez"
|
||||||
|
STR_KB_HINT_LOWER_SECONDARY: "Tartsd nyomva a SELECT-et kisbetűért vagy másodlagos karakterért"
|
||||||
|
STR_KB_HINT_MOVE_CURSOR: "Nyomd meg a BALRA vagy JOBBRA gombot a kurzor mozgatásához"
|
||||||
|
STR_KB_HINT_RETURN_CURSOR: "Nyomd meg a BALRA-t a kurzorpozícióhoz való visszatéréshez"
|
||||||
|
STR_KB_HINT_RETURN_KEYBOARD: "Nyomd meg a LE-t a billentyűzethez való visszatéréshez"
|
||||||
|
STR_KB_HINT_SECONDARY_CHAR: "Tartsd nyomva a SELECT-et másodlagos karakterért"
|
||||||
|
STR_KB_HINT_SHOW_PASSWORD: "Tartsd nyomva a JOBBRA-t, majd nyomd meg az [abc]-t a jelszó megjelenítéséhez"
|
||||||
|
STR_KB_HINT_TOGGLE_HIDE_PASSWORD: "Nyomd meg a [***]-ot a jelszó elrejtéséhez"
|
||||||
|
STR_KB_HINT_TOGGLE_SHOW_PASSWORD: "Nyomd meg az [abc]-t a jelszó megjelenítéséhez"
|
||||||
|
STR_KB_HINT_UPPER_SECONDARY: "Tartsd nyomva a SELECT-et NAGYBETŰÉRT vagy másodlagos karakterért"
|
||||||
|
STR_KB_HINT_URL_SNIPPETS: "Nyomd meg az URL-t az URL-részletekért"
|
||||||
|
STR_KB_TIPS: "Tippek:"
|
||||||
|
STR_KOSYNC: "KOSync"
|
||||||
|
STR_LOADING_FONT_LIST: "Betűtípuslista betöltése..."
|
||||||
|
STR_LONG_PRESS_BEHAVIOR: "Hosszú gombnyomás viselkedése"
|
||||||
|
STR_LONG_PRESS_BEHAVIOR_OFF: "KI"
|
||||||
|
STR_LONG_PRESS_BEHAVIOR_ORIENTATION: "Tájolásváltás"
|
||||||
|
STR_LONG_PRESS_BEHAVIOR_SKIP: "Fejezetugrás"
|
||||||
|
STR_LONG_PRESS_MENU: "Hosszú nyomás – Menü"
|
||||||
|
STR_MANAGE_FONTS: "Betűtípusok kezelése"
|
||||||
|
STR_NEXT_FIELD: "Következő"
|
||||||
|
STR_NEXT_PAGE: "Következő oldal »"
|
||||||
|
STR_NO_BIN_FILES: "Nincs .bin fájl"
|
||||||
|
STR_NO_FONTS_AVAILABLE: "Nincs elérhető betűtípus"
|
||||||
|
STR_NO_SERVERS: "Nincs beállított OPDS szerver"
|
||||||
|
STR_OPDS_SERVERS: "OPDS szerverek"
|
||||||
|
STR_PREV_PAGE: "« Előző oldal"
|
||||||
|
STR_PWR_BTN_FOOTNOTE_BACK: "Gyors visszatérés lábjegyzetből"
|
||||||
|
STR_RECOVERY_MODE: "Helyreállítási mód"
|
||||||
|
STR_RECOVERY_MODE_HINT: "Helyezd a firmware.bin fájlt az SD-kártya gyökerébe, majd válaszd ki"
|
||||||
|
STR_RESTARTING_HINT: "Újraindítás... Ha az eszköz nem indul újra, tartsd nyomva a bekapcsológombot néhány másodpercig."
|
||||||
|
STR_SD_FIRMWARE_UPDATE: "Firmware frissítés SD-kártyáról"
|
||||||
|
STR_SEARCH: "Keresés"
|
||||||
|
STR_SELECT_FIRMWARE_FILE: "Válassz firmware fájlt (.bin)"
|
||||||
|
STR_SERVER_NAME: "Szerver neve"
|
||||||
|
STR_SET_SLEEP_COVER: "Borító beállítása"
|
||||||
|
STR_SHOW_NETWORKS: "Megjelenítés"
|
||||||
|
STR_THEME_ROUNDEDRAFF: "RoundedRaff"
|
||||||
|
STR_TOP: "Felül"
|
||||||
|
STR_UPDATE_ALL: "Összes frissítése"
|
||||||
|
STR_UPDATE_AVAILABLE: "Frissítés"
|
||||||
|
STR_VALIDATING_FIRMWARE: "Firmware ellenőrzése..."
|
||||||
|
STR_XTC_STATUS_BAR: "XTC állapotsáv"
|
||||||
|
|||||||
@@ -98,6 +98,7 @@ STR_USERNAME: "Nome utente"
|
|||||||
STR_PASSWORD: "Password"
|
STR_PASSWORD: "Password"
|
||||||
STR_SYNC_SERVER_URL: "URL server di sincronizzazione"
|
STR_SYNC_SERVER_URL: "URL server di sincronizzazione"
|
||||||
STR_DOCUMENT_MATCHING: "Corrispondenza documenti"
|
STR_DOCUMENT_MATCHING: "Corrispondenza documenti"
|
||||||
|
STR_SEND_METADATA: "Invia metadati documento"
|
||||||
STR_AUTHENTICATE: "Autentica"
|
STR_AUTHENTICATE: "Autentica"
|
||||||
STR_KOREADER_USERNAME: "Nome utente KOReader"
|
STR_KOREADER_USERNAME: "Nome utente KOReader"
|
||||||
STR_KOREADER_PASSWORD: "Password KOReader"
|
STR_KOREADER_PASSWORD: "Password KOReader"
|
||||||
@@ -274,7 +275,6 @@ STR_CHAPTER_PREFIX: "Capitolo: "
|
|||||||
STR_PAGES_SEPARATOR: " pagine | "
|
STR_PAGES_SEPARATOR: " pagine | "
|
||||||
STR_BOOK_PREFIX: "Libro: "
|
STR_BOOK_PREFIX: "Libro: "
|
||||||
STR_CALIBRE_URL_HINT: "Per Calibre, aggiungere /opds all'URL"
|
STR_CALIBRE_URL_HINT: "Per Calibre, aggiungere /opds all'URL"
|
||||||
STR_PERCENT_STEP_HINT: "Sinistra/Destra: 1% Su/Giù: 10%"
|
|
||||||
STR_SYNCING_TIME: "Sincronizzazione orario..."
|
STR_SYNCING_TIME: "Sincronizzazione orario..."
|
||||||
STR_CALC_HASH: "Calcolo hash documento..."
|
STR_CALC_HASH: "Calcolo hash documento..."
|
||||||
STR_HASH_FAILED: "Impossibile calcolare l'hash del documento"
|
STR_HASH_FAILED: "Impossibile calcolare l'hash del documento"
|
||||||
@@ -307,7 +307,8 @@ STR_NO_FOOTNOTES: "Nessuna nota in questa pagina"
|
|||||||
STR_LINK: "[link]"
|
STR_LINK: "[link]"
|
||||||
STR_SLEEP_TIMER_VALUE_FORMAT: "%u min"
|
STR_SLEEP_TIMER_VALUE_FORMAT: "%u min"
|
||||||
STR_SLEEP_NEVER: "Mai"
|
STR_SLEEP_NEVER: "Mai"
|
||||||
STR_SLEEP_TIMER_STEP_HINT: "Sinistra/Destra: 1 min Su/Giù: 5 min"
|
STR_STEP_HINT_FRONT: "Pulsanti frontali:"
|
||||||
|
STR_STEP_HINT_SIDE: "Pulsanti laterali:"
|
||||||
STR_SCREENSHOT_BUTTON: "Screenshot"
|
STR_SCREENSHOT_BUTTON: "Screenshot"
|
||||||
STR_ADD_SERVER: "Aggiungi server"
|
STR_ADD_SERVER: "Aggiungi server"
|
||||||
STR_SERVER_NAME: "Nome server"
|
STR_SERVER_NAME: "Nome server"
|
||||||
@@ -383,3 +384,8 @@ STR_DISABLED: "Disattivato"
|
|||||||
STR_BOOKMARK_OPTION: "Segnalibro"
|
STR_BOOKMARK_OPTION: "Segnalibro"
|
||||||
STR_KOSYNC: "KOSync"
|
STR_KOSYNC: "KOSync"
|
||||||
STR_PWR_BTN_FOOTNOTE_BACK: "Rientro rapido dalle note"
|
STR_PWR_BTN_FOOTNOTE_BACK: "Rientro rapido dalle note"
|
||||||
|
STR_EOB_CONTINUE_WITH: "Continua con"
|
||||||
|
STR_EOB_HOME: "Home"
|
||||||
|
STR_INDEX_FAILED: "Indicizzazione fallita - libro non valido"
|
||||||
|
STR_ADD_HIDDEN_NETWORK: "Aggiungi rete nascosta..."
|
||||||
|
STR_ENTER_WIFI_SSID: "Inserisci il nome della rete (SSID)"
|
||||||
|
|||||||
@@ -88,6 +88,7 @@ STR_USERNAME: "Пайдаланушы аты"
|
|||||||
STR_PASSWORD: "Құпия сөз"
|
STR_PASSWORD: "Құпия сөз"
|
||||||
STR_SYNC_SERVER_URL: "Синхрондау сервері URL"
|
STR_SYNC_SERVER_URL: "Синхрондау сервері URL"
|
||||||
STR_DOCUMENT_MATCHING: "Құжат сәйкестендіру"
|
STR_DOCUMENT_MATCHING: "Құжат сәйкестендіру"
|
||||||
|
STR_SEND_METADATA: "Құжат метадеректерін жіберу"
|
||||||
STR_AUTHENTICATE: "Аутентификация"
|
STR_AUTHENTICATE: "Аутентификация"
|
||||||
STR_KOREADER_USERNAME: "KOReader пайдаланушы аты"
|
STR_KOREADER_USERNAME: "KOReader пайдаланушы аты"
|
||||||
STR_KOREADER_PASSWORD: "KOReader құпия сөзі"
|
STR_KOREADER_PASSWORD: "KOReader құпия сөзі"
|
||||||
@@ -237,7 +238,6 @@ STR_CHAPTER_PREFIX: "Тарау: "
|
|||||||
STR_PAGES_SEPARATOR: " бет | "
|
STR_PAGES_SEPARATOR: " бет | "
|
||||||
STR_BOOK_PREFIX: "Кітап: "
|
STR_BOOK_PREFIX: "Кітап: "
|
||||||
STR_CALIBRE_URL_HINT: "Calibre үшін URL-ге /opds қосыңыз"
|
STR_CALIBRE_URL_HINT: "Calibre үшін URL-ге /opds қосыңыз"
|
||||||
STR_PERCENT_STEP_HINT: "Сол/Оң: 1% Жоғары/Төмен: 10%"
|
|
||||||
STR_SYNCING_TIME: "Уақыт синхрондалуда..."
|
STR_SYNCING_TIME: "Уақыт синхрондалуда..."
|
||||||
STR_CALC_HASH: "Құжат хэші есептелуде..."
|
STR_CALC_HASH: "Құжат хэші есептелуде..."
|
||||||
STR_HASH_FAILED: "Құжат хэшін есептеу сәтсіз"
|
STR_HASH_FAILED: "Құжат хэшін есептеу сәтсіз"
|
||||||
@@ -294,8 +294,11 @@ STR_NO_FOOTNOTES: "Бұл бетте түсіндірме жазбалар жо
|
|||||||
STR_LINK: "[сілтеме]"
|
STR_LINK: "[сілтеме]"
|
||||||
STR_SLEEP_TIMER_VALUE_FORMAT: "%u мин"
|
STR_SLEEP_TIMER_VALUE_FORMAT: "%u мин"
|
||||||
STR_SLEEP_NEVER: "Ешқашан"
|
STR_SLEEP_NEVER: "Ешқашан"
|
||||||
STR_SLEEP_TIMER_STEP_HINT: "Сол/Оң: 1 мин Жоғары/Төмен: 5 мин"
|
STR_STEP_HINT_FRONT: "Алдыңғы түймелер:"
|
||||||
|
STR_STEP_HINT_SIDE: "Бүйір түймелері:"
|
||||||
STR_SCREENSHOT_BUTTON: "Скриншот түсіру"
|
STR_SCREENSHOT_BUTTON: "Скриншот түсіру"
|
||||||
STR_AUTO_TURN_ENABLED: "Автоматты бет аудару қосулы: "
|
STR_AUTO_TURN_ENABLED: "Автоматты бет аудару қосулы: "
|
||||||
STR_AUTO_TURN_PAGES_PER_MIN: "Автоматты бет аудару (минутына бет саны)"
|
STR_AUTO_TURN_PAGES_PER_MIN: "Автоматты бет аудару (минутына бет саны)"
|
||||||
STR_TILT_PAGE_TURN: "Еңкейту арқылы бет аудару"
|
STR_TILT_PAGE_TURN: "Еңкейту арқылы бет аудару"
|
||||||
|
STR_ADD_HIDDEN_NETWORK: "Жасырын желіні қосу..."
|
||||||
|
STR_ENTER_WIFI_SSID: "Желі атауын енгізіңіз (SSID)"
|
||||||
|
|||||||
@@ -94,6 +94,7 @@ STR_USERNAME: "Vartotojas"
|
|||||||
STR_PASSWORD: "Slaptažodis"
|
STR_PASSWORD: "Slaptažodis"
|
||||||
STR_SYNC_SERVER_URL: "Serverio URL"
|
STR_SYNC_SERVER_URL: "Serverio URL"
|
||||||
STR_DOCUMENT_MATCHING: "Atpažinimas"
|
STR_DOCUMENT_MATCHING: "Atpažinimas"
|
||||||
|
STR_SEND_METADATA: "Siųsti dokumento metaduomenis"
|
||||||
STR_AUTHENTICATE: "Prisijungti"
|
STR_AUTHENTICATE: "Prisijungti"
|
||||||
STR_KOREADER_USERNAME: "KOReader vartotojas"
|
STR_KOREADER_USERNAME: "KOReader vartotojas"
|
||||||
STR_KOREADER_PASSWORD: "KOReader slaptažodis"
|
STR_KOREADER_PASSWORD: "KOReader slaptažodis"
|
||||||
@@ -263,7 +264,6 @@ STR_CHAPTER_PREFIX: "Sk: "
|
|||||||
STR_PAGES_SEPARATOR: " psl. | "
|
STR_PAGES_SEPARATOR: " psl. | "
|
||||||
STR_BOOK_PREFIX: "Kn: "
|
STR_BOOK_PREFIX: "Kn: "
|
||||||
STR_CALIBRE_URL_HINT: "Pridėkite /opds prie URL"
|
STR_CALIBRE_URL_HINT: "Pridėkite /opds prie URL"
|
||||||
STR_PERCENT_STEP_HINT: "K/D: 1% | V/A: 10%"
|
|
||||||
STR_SYNCING_TIME: "Sinchr. laikas..."
|
STR_SYNCING_TIME: "Sinchr. laikas..."
|
||||||
STR_CALC_HASH: "Skaičiuojama maiša..."
|
STR_CALC_HASH: "Skaičiuojama maiša..."
|
||||||
STR_HASH_FAILED: "Maišos klaida"
|
STR_HASH_FAILED: "Maišos klaida"
|
||||||
@@ -295,8 +295,11 @@ STR_NO_FOOTNOTES: "Šiame psl. išnašų nėra"
|
|||||||
STR_LINK: "[nuoroda]"
|
STR_LINK: "[nuoroda]"
|
||||||
STR_SLEEP_TIMER_VALUE_FORMAT: "%u min."
|
STR_SLEEP_TIMER_VALUE_FORMAT: "%u min."
|
||||||
STR_SLEEP_NEVER: "Niekada"
|
STR_SLEEP_NEVER: "Niekada"
|
||||||
STR_SLEEP_TIMER_STEP_HINT: "Kairė/Dešinė: 1 min. Viršus/Apačia: 5 min."
|
STR_STEP_HINT_FRONT: "Priekiniai mygtukai:"
|
||||||
|
STR_STEP_HINT_SIDE: "Šoniniai mygtukai:"
|
||||||
STR_SCREENSHOT_BUTTON: "Ekrano nuotrauka"
|
STR_SCREENSHOT_BUTTON: "Ekrano nuotrauka"
|
||||||
STR_AUTO_TURN_ENABLED: "Auto-vertimas: "
|
STR_AUTO_TURN_ENABLED: "Auto-vertimas: "
|
||||||
STR_AUTO_TURN_PAGES_PER_MIN: "Auto-vertimas (psl/min)"
|
STR_AUTO_TURN_PAGES_PER_MIN: "Auto-vertimas (psl/min)"
|
||||||
STR_TILT_PAGE_TURN: "Puslapio vertimas pakreipiant"
|
STR_TILT_PAGE_TURN: "Puslapio vertimas pakreipiant"
|
||||||
|
STR_ADD_HIDDEN_NETWORK: "Pridėti paslėptą tinklą..."
|
||||||
|
STR_ENTER_WIFI_SSID: "Įveskite tinklo pavadinimą (SSID)"
|
||||||
|
|||||||
@@ -0,0 +1,385 @@
|
|||||||
|
_language_name: "Norsk bokmål"
|
||||||
|
_language_code: "NB"
|
||||||
|
_order: "26"
|
||||||
|
|
||||||
|
STR_CROSSPOINT: "CrossPoint"
|
||||||
|
STR_BOOTING: "STARTER"
|
||||||
|
STR_SLEEPING: "HVILER"
|
||||||
|
STR_ENTERING_SLEEP: "Går i hvilemodus"
|
||||||
|
STR_BROWSE_FILES: "Bla i filer"
|
||||||
|
STR_FILE_TRANSFER: "Filoverføring"
|
||||||
|
STR_SETTINGS_TITLE: "Innstillinger"
|
||||||
|
STR_CONTINUE_READING: "Fortsett å lese"
|
||||||
|
STR_NO_OPEN_BOOK: "Ingen åpen bok"
|
||||||
|
STR_START_READING: "Start å lese nedenfor"
|
||||||
|
STR_NO_FILES_FOUND: "Ingen filer funnet"
|
||||||
|
STR_SELECT_CHAPTER: "Velg kapittel"
|
||||||
|
STR_NO_CHAPTERS: "Ingen kapitler"
|
||||||
|
STR_END_OF_BOOK: "Slutten av boken"
|
||||||
|
STR_EMPTY_CHAPTER: "Tomt kapittel"
|
||||||
|
STR_INDEXING: "Indekserer"
|
||||||
|
STR_MEMORY_ERROR: "Minnefeil"
|
||||||
|
STR_PAGE_LOAD_ERROR: "Feil ved sidelasting"
|
||||||
|
STR_EMPTY_FILE: "Tom fil"
|
||||||
|
STR_OUT_OF_BOUNDS: "Utenfor område"
|
||||||
|
STR_LOADING: "Laster..."
|
||||||
|
STR_LOADING_POPUP: "Laster"
|
||||||
|
STR_WIFI_NETWORKS: "WiFi-nettverk"
|
||||||
|
STR_NO_NETWORKS: "Ingen nettverk funnet"
|
||||||
|
STR_NETWORKS_FOUND: "%zu nettverk funnet"
|
||||||
|
STR_SCANNING: "Skanner..."
|
||||||
|
STR_CONNECTING: "Kobler til..."
|
||||||
|
STR_CONNECTED: "Tilkoblet!"
|
||||||
|
STR_CONNECTION_FAILED: "Tilkobling mislyktes"
|
||||||
|
STR_FORGET_NETWORK: "Glem nettverk?"
|
||||||
|
STR_SAVE_PASSWORD: "Lagre passord til neste gang?"
|
||||||
|
STR_PRESS_OK_SCAN: "Trykk OK for å skanne på nytt"
|
||||||
|
STR_JOIN_NETWORK: "Koble til et nettverk"
|
||||||
|
STR_CREATE_HOTSPOT: "Opprett hotspot"
|
||||||
|
STR_JOIN_DESC: "Koble til et eksisterende WiFi-nettverk"
|
||||||
|
STR_HOTSPOT_DESC: "Opprett et WiFi-nettverk andre kan koble seg til"
|
||||||
|
STR_STARTING_HOTSPOT: "Starter hotspot..."
|
||||||
|
STR_HOTSPOT_MODE: "Hotspot-modus"
|
||||||
|
STR_CONNECT_WIFI_HINT: "Koble enheten din til dette WiFi-nettverket"
|
||||||
|
STR_OPEN_URL_HINT: "Åpne denne URL-en i nettleseren din"
|
||||||
|
STR_OR_HTTP_PREFIX: "eller http://"
|
||||||
|
STR_SCAN_QR_HINT: "eller skann QR-kode med telefonen din:"
|
||||||
|
STR_CALIBRE_WIRELESS: "Calibre Wireless"
|
||||||
|
STR_NETWORK_LEGEND: "* = Kryptert | + = Lagret"
|
||||||
|
STR_MAC_ADDRESS: "MAC-adresse:"
|
||||||
|
STR_CHECKING_WIFI: "Sjekker WiFi..."
|
||||||
|
STR_ENTER_WIFI_PASSWORD: "Skriv inn WiFi-passord"
|
||||||
|
STR_TO_PREFIX: "til "
|
||||||
|
STR_CALIBRE_RECEIVING: "Mottar: "
|
||||||
|
STR_CALIBRE_RECEIVED: "Mottatt: "
|
||||||
|
STR_CALIBRE_INSTRUCTION_1: "1) Installer CrossPoint Reader-plugin"
|
||||||
|
STR_CALIBRE_INSTRUCTION_2: "2) Vær på samme WiFi-nettverk"
|
||||||
|
STR_CALIBRE_INSTRUCTION_3: "3) I Calibre: \"Send til enhet\""
|
||||||
|
STR_CALIBRE_INSTRUCTION_4: "\"Hold denne skjermen åpen mens du sender\""
|
||||||
|
STR_CAT_DISPLAY: "Skjerm"
|
||||||
|
STR_CAT_READER: "Leser"
|
||||||
|
STR_CAT_CONTROLS: "Betjening"
|
||||||
|
STR_CAT_SYSTEM: "System"
|
||||||
|
STR_SLEEP_SCREEN: "Hvileskjerm"
|
||||||
|
STR_QUICK_RESUME_TIMEOUT: "Hurtig gjenopptak ved tidsavbrudd"
|
||||||
|
STR_SLEEP_COVER_MODE: "Omslagsmodus for hvileskjerm"
|
||||||
|
STR_HIDE_BATTERY: "Skjul batteri %"
|
||||||
|
STR_EXTRA_SPACING: "Ekstra avsnittsavstand"
|
||||||
|
STR_TEXT_AA: "Tekstutjevning"
|
||||||
|
STR_IMAGES: "Bilder"
|
||||||
|
STR_IMAGES_DISPLAY: "Vis"
|
||||||
|
STR_IMAGES_PLACEHOLDER: "Plassholder"
|
||||||
|
STR_IMAGES_SUPPRESS: "Skjul"
|
||||||
|
STR_SHORT_PWR_BTN: "Kort trykk på av/på-knapp"
|
||||||
|
STR_ORIENTATION: "Leseretning"
|
||||||
|
STR_SIDE_BTN_LAYOUT: "Sideknapp-oppsett (leser)"
|
||||||
|
STR_FRONT_BTN_FOLLOW_ORIENTATION: "Orienter frontknapper"
|
||||||
|
STR_LONG_PRESS_BEHAVIOR: "Atferd ved langt trykk"
|
||||||
|
STR_LONG_PRESS_BEHAVIOR_OFF: "AV"
|
||||||
|
STR_LONG_PRESS_BEHAVIOR_SKIP: "Kapittelhopp"
|
||||||
|
STR_LONG_PRESS_BEHAVIOR_ORIENTATION: "Endre orientering"
|
||||||
|
STR_LONG_PRESS_MENU: "Langt trykk på Meny"
|
||||||
|
STR_FONT_PREVIEW_TEXT: "Høvdingens kjære squaw får litt pizza i Mexico by"
|
||||||
|
STR_FONT_FAMILY: "Skrifttype i leser"
|
||||||
|
STR_FONT_SIZE: "Skriftstørrelse i leser"
|
||||||
|
STR_LINE_SPACING: "Linjeavstand i leser"
|
||||||
|
STR_SCREEN_MARGIN: "Skjermmarg i leser"
|
||||||
|
STR_PARA_ALIGNMENT: "Avsnittsjustering i leser"
|
||||||
|
STR_HYPHENATION: "Orddeling"
|
||||||
|
STR_TIME_TO_SLEEP: "Tid før hvile"
|
||||||
|
STR_SHOW_HIDDEN_FILES: "Vis skjulte filer"
|
||||||
|
STR_REMOVE_READ_FROM_RECENTS: "Fjern leste bøker fra Nylig-listen"
|
||||||
|
STR_MOVE_FINISHED_TO_READ: "Flytt fullførte bøker til Read-mappen"
|
||||||
|
STR_REFRESH_FREQ: "Oppdateringsfrekvens"
|
||||||
|
STR_KOREADER_SYNC: "KOReader Sync"
|
||||||
|
STR_CHECK_UPDATES: "Se etter oppdateringer"
|
||||||
|
STR_LANGUAGE: "Språk"
|
||||||
|
STR_CLEAR_READING_CACHE: "Tøm lesebuffer"
|
||||||
|
STR_USERNAME: "Brukernavn"
|
||||||
|
STR_PASSWORD: "Passord"
|
||||||
|
STR_SYNC_SERVER_URL: "URL til synk-server"
|
||||||
|
STR_DOCUMENT_MATCHING: "Dokumentgjenkjenning"
|
||||||
|
STR_AUTHENTICATE: "Godkjenn"
|
||||||
|
STR_KOREADER_USERNAME: "KOReader-brukernavn"
|
||||||
|
STR_KOREADER_PASSWORD: "KOReader-passord"
|
||||||
|
STR_FILENAME: "Filnavn"
|
||||||
|
STR_BINARY: "Binær"
|
||||||
|
STR_SET_CREDENTIALS_FIRST: "Angi påloggingsinfo først"
|
||||||
|
STR_WIFI_CONN_FAILED: "WiFi-tilkobling mislyktes"
|
||||||
|
STR_AUTHENTICATING: "Godkjenner..."
|
||||||
|
STR_AUTH_SUCCESS: "Godkjenning vellykket!"
|
||||||
|
STR_KOREADER_AUTH: "KOReader-godkjenning"
|
||||||
|
STR_SYNC_READY: "KOReader-synk er klar til bruk"
|
||||||
|
STR_AUTH_FAILED: "Godkjenning mislyktes"
|
||||||
|
STR_DONE: "Ferdig"
|
||||||
|
STR_CLEAR_CACHE_WARNING_1: "Dette tømmer alle bufrede bokdata."
|
||||||
|
STR_CLEAR_CACHE_WARNING_2: "All lesefremdrift går tapt!"
|
||||||
|
STR_CLEAR_CACHE_WARNING_3: "Bøker må indekseres på nytt"
|
||||||
|
STR_CLEAR_CACHE_WARNING_4: "når de åpnes igjen."
|
||||||
|
STR_CLEARING_CACHE: "Tømmer buffer..."
|
||||||
|
STR_CACHE_CLEARED: "Buffer tømt"
|
||||||
|
STR_ITEMS_REMOVED: "elementer fjernet"
|
||||||
|
STR_FAILED_LOWER: "mislyktes"
|
||||||
|
STR_CLEAR_CACHE_FAILED: "Kunne ikke tømme buffer"
|
||||||
|
STR_CHECK_SERIAL_OUTPUT: "Sjekk seriell utdata for detaljer"
|
||||||
|
STR_DARK: "Mørk"
|
||||||
|
STR_LIGHT: "Lys"
|
||||||
|
STR_CUSTOM: "Egendefinert"
|
||||||
|
STR_COVER: "Omslag"
|
||||||
|
STR_NONE_OPT: "Ingen"
|
||||||
|
STR_FIT: "Tilpass"
|
||||||
|
STR_CROP: "Beskjær"
|
||||||
|
STR_NEVER: "Aldri"
|
||||||
|
STR_IN_READER: "I leseren"
|
||||||
|
STR_ALWAYS: "Alltid"
|
||||||
|
STR_IGNORE: "Ignorer"
|
||||||
|
STR_SLEEP: "Hvile"
|
||||||
|
STR_PAGE_TURN: "Bla om"
|
||||||
|
STR_FORCE_REFRESH: "Oppdater skjerm"
|
||||||
|
STR_PORTRAIT: "Stående"
|
||||||
|
STR_LANDSCAPE_CW: "Liggende med klokken"
|
||||||
|
STR_INVERTED: "Opp ned"
|
||||||
|
STR_ORIENTATION_INVERTED: "Portrett 180°"
|
||||||
|
STR_LANDSCAPE_CCW: "Liggende mot klokken"
|
||||||
|
STR_PREV_NEXT: "Forrige/Neste"
|
||||||
|
STR_NEXT_PREV: "Neste/Forrige"
|
||||||
|
STR_KOSYNC: "KOSync"
|
||||||
|
STR_BOOKMARK_OPTION: "Bokmerke"
|
||||||
|
STR_DISABLED: "Deaktivert"
|
||||||
|
STR_NOTO_SERIF: "Noto Serif"
|
||||||
|
STR_NOTO_SANS: "Noto Sans"
|
||||||
|
STR_SMALL: "Liten"
|
||||||
|
STR_MEDIUM: "Middels"
|
||||||
|
STR_LARGE: "Stor"
|
||||||
|
STR_X_LARGE: "Ekstra stor"
|
||||||
|
STR_TIGHT: "Tett"
|
||||||
|
STR_NORMAL: "Normal"
|
||||||
|
STR_WIDE: "Bred"
|
||||||
|
STR_JUSTIFY: "Blokkjustert"
|
||||||
|
STR_ALIGN_LEFT: "Venstre"
|
||||||
|
STR_CENTER: "Midtstilt"
|
||||||
|
STR_ALIGN_RIGHT: "Høyre"
|
||||||
|
STR_PAGES_1: "1 side"
|
||||||
|
STR_PAGES_5: "5 sider"
|
||||||
|
STR_PAGES_10: "10 sider"
|
||||||
|
STR_PAGES_15: "15 sider"
|
||||||
|
STR_PAGES_30: "30 sider"
|
||||||
|
STR_UPDATE: "Oppdater"
|
||||||
|
STR_CHECKING_UPDATE: "Ser etter oppdatering..."
|
||||||
|
STR_NEW_UPDATE: "Ny oppdatering tilgjengelig!"
|
||||||
|
STR_CURRENT_VERSION: "Nåværende versjon: "
|
||||||
|
STR_NEW_VERSION: "Ny versjon: "
|
||||||
|
STR_UPDATING: "Oppdaterer..."
|
||||||
|
STR_NO_UPDATE: "Ingen oppdatering tilgjengelig"
|
||||||
|
STR_UPDATE_FAILED: "Oppdatering mislyktes"
|
||||||
|
STR_UPDATE_COMPLETE: "Oppdatering fullført"
|
||||||
|
STR_POWER_ON_HINT: "Trykk og hold av/på-knappen for å slå på igjen"
|
||||||
|
STR_RESTARTING_HINT: "Starter på nytt... Hvis enheten ikke starter, hold av/på-knappen i noen sekunder."
|
||||||
|
STR_NO_ENTRIES: "Ingen oppføringer funnet"
|
||||||
|
STR_DOWNLOADING: "Laster ned..."
|
||||||
|
STR_DOWNLOAD_FAILED: "Nedlasting mislyktes"
|
||||||
|
STR_ERROR_MSG: "Feil:"
|
||||||
|
STR_UNNAMED: "Uten navn"
|
||||||
|
STR_HOLD_OPEN_TO_DELETE: "Hold Åpne for å slette"
|
||||||
|
STR_NO_SERVER_URL: "Ingen server-URL konfigurert"
|
||||||
|
STR_FETCH_FEED_FAILED: "Kunne ikke hente feed"
|
||||||
|
STR_PARSE_FEED_FAILED: "Kunne ikke tolke feed"
|
||||||
|
STR_NEXT_PAGE: "Neste side »"
|
||||||
|
STR_PREV_PAGE: "« Forrige side"
|
||||||
|
STR_NETWORK_PREFIX: "Nettverk: "
|
||||||
|
STR_IP_ADDRESS_PREFIX: "IP-adresse: "
|
||||||
|
STR_ERROR_GENERAL_FAILURE: "Feil: Generell feil"
|
||||||
|
STR_ERROR_NETWORK_NOT_FOUND: "Feil: Nettverk ikke funnet"
|
||||||
|
STR_ERROR_CONNECTION_TIMEOUT: "Feil: Tidsavbrudd for tilkobling"
|
||||||
|
STR_SD_CARD: "SD-kort"
|
||||||
|
STR_BACK: "« Tilbake"
|
||||||
|
STR_EXIT: "« Avslutt"
|
||||||
|
STR_HOME: "« Hjem"
|
||||||
|
STR_SELECT: "Velg"
|
||||||
|
STR_SELECTED: "Valgt"
|
||||||
|
STR_TOGGLE: "Veksle"
|
||||||
|
STR_TOGGLE_BOOKMARK: "Veksle bokmerke"
|
||||||
|
STR_CONFIRM: "Bekreft"
|
||||||
|
STR_CANCEL: "Avbryt"
|
||||||
|
STR_CONNECT: "Koble til"
|
||||||
|
STR_OPEN: "Åpne"
|
||||||
|
STR_DOWNLOAD: "Last ned"
|
||||||
|
STR_RETRY: "Prøv igjen"
|
||||||
|
STR_YES: "Ja"
|
||||||
|
STR_NO: "Nei"
|
||||||
|
STR_SHOW: "Vis"
|
||||||
|
STR_HIDE: "Skjul"
|
||||||
|
STR_STATE_ON: "PÅ"
|
||||||
|
STR_STATE_OFF: "AV"
|
||||||
|
STR_NOT_SET: "Ikke angitt"
|
||||||
|
STR_DIR_LEFT: "Venstre"
|
||||||
|
STR_DIR_RIGHT: "Høyre"
|
||||||
|
STR_DIR_UP: "Opp"
|
||||||
|
STR_DIR_DOWN: "Ned"
|
||||||
|
STR_OK_BUTTON: "OK"
|
||||||
|
STR_SLEEP_COVER_FILTER: "Omslagsfilter for hvileskjerm"
|
||||||
|
STR_FILTER_CONTRAST: "Kontrast"
|
||||||
|
STR_CUSTOMISE_STATUS_BAR: "Tilpass statuslinje"
|
||||||
|
STR_CHAPTER_PAGE_COUNT: "Sideantall i kapittel"
|
||||||
|
STR_BOOK_PROGRESS_PERCENTAGE: "Bokfremdrift i prosent"
|
||||||
|
STR_PROGRESS_BAR: "Fremdriftslinje"
|
||||||
|
STR_PROGRESS_BAR_THICKNESS: "Tykkelse på fremdriftslinje"
|
||||||
|
STR_PROGRESS_BAR_THIN: "Tynn"
|
||||||
|
STR_PROGRESS_BAR_MEDIUM: "Middels"
|
||||||
|
STR_PROGRESS_BAR_THICK: "Tykk"
|
||||||
|
STR_BOOK: "Bok"
|
||||||
|
STR_CHAPTER: "Kapittel"
|
||||||
|
STR_EXAMPLE_CHAPTER: "Kapittel 21"
|
||||||
|
STR_EXAMPLE_BOOK: "Boktittel"
|
||||||
|
STR_PREVIEW: "Forhåndsvisning"
|
||||||
|
STR_TITLE: "Tittel"
|
||||||
|
STR_BATTERY: "Batteri"
|
||||||
|
STR_XTC_STATUS_BAR: "XTC-statuslinje"
|
||||||
|
STR_BOTTOM: "Nederst"
|
||||||
|
STR_TOP: "Øverst"
|
||||||
|
STR_CLOCK: "Klokke"
|
||||||
|
STR_CLOCK_UTC_OFFSET: "UTC-forskyvning for klokke"
|
||||||
|
STR_CLOCK_FORMAT: "Klokkeformat"
|
||||||
|
STR_CLOCK_FORMAT_24H: "24-timers"
|
||||||
|
STR_CLOCK_FORMAT_12H: "12-timers"
|
||||||
|
STR_CURRENT_TIME: "Nåværende tid:"
|
||||||
|
STR_NEXT_FIELD: "Neste"
|
||||||
|
STR_CLOCK_SYNC: "Synkroniser klokke"
|
||||||
|
STR_CLOCK_SYNC_NOW: "Synkroniser klokke nå"
|
||||||
|
STR_CLOCK_SYNCING: "Synkroniserer fra NTP..."
|
||||||
|
STR_CLOCK_SYNC_OK: "Klokke synkronisert"
|
||||||
|
STR_CLOCK_SYNC_FAIL: "Synkronisering mislyktes"
|
||||||
|
STR_CLOCK_SYNC_NO_WIFI: "WiFi ikke tilkoblet"
|
||||||
|
STR_CLOCK_SYNC_NO_WIFI_HINT: "Koble til WiFi først, og prøv igjen."
|
||||||
|
STR_CLOCK_SYNCED: "Klokke synkronisert"
|
||||||
|
STR_UI_THEME: "Grensesnittstema"
|
||||||
|
STR_THEME_CLASSIC: "Klassisk"
|
||||||
|
STR_THEME_LYRA: "Lyra"
|
||||||
|
STR_THEME_ROUNDEDRAFF: "RoundedRaff"
|
||||||
|
STR_THEME_LYRA_EXTENDED: "Lyra Extended"
|
||||||
|
STR_SUNLIGHT_FADING_FIX: "Retting for soluttoning"
|
||||||
|
STR_REMAP_FRONT_BUTTONS: "Tilordne frontknapper på nytt"
|
||||||
|
STR_BOOKMARKS: "Bokmerker"
|
||||||
|
STR_BOOKMARK_ADDED: "Bokmerke lagt til."
|
||||||
|
STR_BOOKMARK_REMOVED: "Bokmerke fjernet."
|
||||||
|
STR_OPDS_BROWSER: "OPDS-leser"
|
||||||
|
STR_SEARCH: "Søk"
|
||||||
|
STR_COVER_CUSTOM: "Omslag + Egendefinert"
|
||||||
|
STR_QUICK_RESUME: "Hurtig gjenopptak"
|
||||||
|
STR_MENU_RECENT_BOOKS: "Nylige bøker"
|
||||||
|
STR_REMOVE_FROM_RECENTS: "Fjern fra Nylige bøker?"
|
||||||
|
STR_NO_RECENT_BOOKS: "Ingen nylige bøker"
|
||||||
|
STR_CALIBRE_DESC: "Bruk trådløs enhetsoverføring med Calibre"
|
||||||
|
STR_FORGET_AND_REMOVE: "Glem nettverk og fjern lagret passord?"
|
||||||
|
STR_FORGET_BUTTON: "Glem"
|
||||||
|
STR_CALIBRE_STARTING: "Starter Calibre..."
|
||||||
|
STR_CALIBRE_SETUP: "Oppsett"
|
||||||
|
STR_CALIBRE_STATUS: "Status"
|
||||||
|
STR_CLEAR_BUTTON: "Tøm"
|
||||||
|
STR_DEFAULT_VALUE: "Standard"
|
||||||
|
STR_REMAP_PROMPT: "Trykk en frontknapp for hver rolle"
|
||||||
|
STR_UNASSIGNED: "Ikke tilordnet"
|
||||||
|
STR_ALREADY_ASSIGNED: "Allerede tilordnet"
|
||||||
|
STR_REMAP_RESET_HINT: "Sideknapp opp: Nullstill til standardoppsett"
|
||||||
|
STR_REMAP_CANCEL_HINT: "Sideknapp ned: Avbryt omtilordning"
|
||||||
|
STR_HW_BACK_LABEL: "Tilbake (1. knapp)"
|
||||||
|
STR_HW_CONFIRM_LABEL: "Bekreft (2. knapp)"
|
||||||
|
STR_HW_LEFT_LABEL: "Venstre (3. knapp)"
|
||||||
|
STR_HW_RIGHT_LABEL: "Høyre (4. knapp)"
|
||||||
|
STR_GO_TO_PERCENT: "Gå til %"
|
||||||
|
STR_GO_HOME_BUTTON: "Hjem"
|
||||||
|
STR_SYNC_PROGRESS: "Synkroniser fremdrift"
|
||||||
|
STR_DELETE_CACHE: "Slett bokbuffer"
|
||||||
|
STR_DELETE: "Slett"
|
||||||
|
STR_CONFIRM_DELETE_BOOKMARK: "Slette dette bokmerket?"
|
||||||
|
STR_DISPLAY_QR: "Vis side som QR"
|
||||||
|
STR_CHAPTER_PREFIX: "Kapittel: "
|
||||||
|
STR_PAGES_SEPARATOR: " sider | "
|
||||||
|
STR_BOOK_PREFIX: "Bok: "
|
||||||
|
STR_CALIBRE_URL_HINT: "For Calibre, legg til /opds i URL-en din"
|
||||||
|
STR_PERCENT_STEP_HINT: "Venstre/Høyre: 1% Opp/Ned: 10%"
|
||||||
|
STR_SYNCING_TIME: "Synkroniserer tid..."
|
||||||
|
STR_CALC_HASH: "Beregner dokument-hash..."
|
||||||
|
STR_HASH_FAILED: "Kunne ikke beregne dokument-hash"
|
||||||
|
STR_FETCH_PROGRESS: "Henter ekstern fremdrift..."
|
||||||
|
STR_UPLOAD_PROGRESS: "Laster opp fremdrift..."
|
||||||
|
STR_NO_CREDENTIALS_MSG: "Ingen påloggingsinfo angitt"
|
||||||
|
STR_KOREADER_SETUP_HINT: "Sett opp KOReader-konto i Innstillinger"
|
||||||
|
STR_PROGRESS_FOUND: "Fremdrift funnet!"
|
||||||
|
STR_REMOTE_LABEL: "Ekstern:"
|
||||||
|
STR_LOCAL_LABEL: "Lokal:"
|
||||||
|
STR_PAGE_OVERALL_FORMAT: "Side %d, %.2f%% totalt"
|
||||||
|
STR_PAGE_TOTAL_OVERALL_FORMAT: "Side %d/%d, %.2f%% totalt"
|
||||||
|
STR_DEVICE_FROM_FORMAT: " Fra: %s"
|
||||||
|
STR_APPLY_REMOTE: "Bruk ekstern fremdrift"
|
||||||
|
STR_UPLOAD_LOCAL: "Last opp lokal fremdrift"
|
||||||
|
STR_NO_REMOTE_MSG: "Ingen ekstern fremdrift funnet"
|
||||||
|
STR_UPLOAD_PROMPT: "Last opp nåværende posisjon?"
|
||||||
|
STR_UPLOAD_SUCCESS: "Fremdrift lastet opp!"
|
||||||
|
STR_SYNC_FAILED_MSG: "Synkronisering mislyktes"
|
||||||
|
STR_SAVE_PROGRESS_FAILED: "Kunne ikke lagre fremdrift"
|
||||||
|
STR_SECTION_PREFIX: "Del "
|
||||||
|
STR_UPLOAD: "Last opp"
|
||||||
|
STR_BOOK_S_STYLE: "Bokens stil"
|
||||||
|
STR_EMBEDDED_STYLE: "Innebygd stil"
|
||||||
|
STR_FOCUS_READING: "Fokuslesing"
|
||||||
|
STR_OPDS_SERVER_URL: "OPDS-server-URL"
|
||||||
|
STR_PWR_BTN_FOOTNOTE_BACK: "Hurtig retur fra fotnoter"
|
||||||
|
STR_SET_SLEEP_COVER: "Velg omslag"
|
||||||
|
STR_FOOTNOTES: "Fotnoter"
|
||||||
|
STR_NO_FOOTNOTES: "Ingen fotnoter på denne siden"
|
||||||
|
STR_LINK: "[lenke]"
|
||||||
|
STR_SCREENSHOT_BUTTON: "Ta skjermbilde"
|
||||||
|
STR_SLEEP_TIMER_VALUE_FORMAT: "%u min"
|
||||||
|
STR_SLEEP_NEVER: "Aldri"
|
||||||
|
STR_SLEEP_TIMER_STEP_HINT: "Venstre/Høyre: 1 min Opp/Ned: 5 min"
|
||||||
|
STR_ADD_SERVER: "Legg til server"
|
||||||
|
STR_SERVER_NAME: "Servernavn"
|
||||||
|
STR_NO_SERVERS: "Ingen OPDS-servere konfigurert"
|
||||||
|
STR_DELETE_SERVER: "Slett server"
|
||||||
|
STR_OPDS_SERVERS: "OPDS-servere"
|
||||||
|
STR_AUTO_TURN_ENABLED: "Automatisk bla aktivert: "
|
||||||
|
STR_AUTO_TURN_PAGES_PER_MIN: "Automatisk bla (sider per minutt)"
|
||||||
|
STR_MANAGE_FONTS: "Administrer skrifttyper"
|
||||||
|
STR_FONT_BROWSER: "Bla i skrifttyper"
|
||||||
|
STR_LOADING_FONT_LIST: "Laster skrifttypeliste..."
|
||||||
|
STR_NO_FONTS_AVAILABLE: "Ingen skrifttyper tilgjengelig"
|
||||||
|
STR_FONT_INSTALLED: "Skrifttype installert!"
|
||||||
|
STR_FONT_INSTALL_FAILED: "Installasjon av skrifttype mislyktes"
|
||||||
|
STR_INSTALLED: "Installert"
|
||||||
|
STR_DOWNLOAD_ALL: "Last ned alle"
|
||||||
|
STR_UPDATE_ALL: "Oppdater alle"
|
||||||
|
STR_UPDATE_AVAILABLE: "Oppdater"
|
||||||
|
STR_CRASH_TITLE: "Systemkrasj"
|
||||||
|
STR_CRASH_DESCRIPTION: "En detaljert rapport ble lagret i crash_report.txt. Legg ved denne filen i feilrapporten din."
|
||||||
|
STR_CRASH_REASON: "Krasjårsak:"
|
||||||
|
STR_CRASH_NO_REASON: "(Ingen årsak ble registrert)"
|
||||||
|
STR_TILT_PAGE_TURN: "Vipp for å bla"
|
||||||
|
STR_KB_HINT_MOVE_CURSOR: "Trykk VENSTRE eller HØYRE for å flytte markøren"
|
||||||
|
STR_KB_HINT_RETURN_CURSOR: "Trykk VENSTRE for å gå tilbake til markørposisjonen"
|
||||||
|
STR_KB_HINT_HIDE_PASSWORD: "Hold HØYRE og trykk [***] for å skjule passord"
|
||||||
|
STR_KB_HINT_SHOW_PASSWORD: "Hold HØYRE og trykk [abc] for å vise passord"
|
||||||
|
STR_KB_HINT_TOGGLE_HIDE_PASSWORD: "Trykk [***] for å skjule passord"
|
||||||
|
STR_KB_HINT_TOGGLE_SHOW_PASSWORD: "Trykk [abc] for å vise passord"
|
||||||
|
STR_KB_HINT_EDIT_ENTRY: "Hold OPP for å redigere oppføring"
|
||||||
|
STR_KB_TIPS: "Tips:"
|
||||||
|
STR_KB_HINT_RETURN_KEYBOARD: "Trykk NED for å gå tilbake til tastaturet"
|
||||||
|
STR_KB_HINT_EXIT_URL_MODE: "Trykk ABC for å avslutte URL-modus"
|
||||||
|
STR_KB_HINT_CLEAR_TEXT: "Hold DEL for å slette all tekst"
|
||||||
|
STR_KB_HINT_SECONDARY_CHAR: "Hold VELG for sekundærtegn"
|
||||||
|
STR_KB_HINT_UPPER_SECONDARY: "Hold VELG for STORE BOKSTAVER eller sekundærtegn"
|
||||||
|
STR_KB_HINT_LOWER_SECONDARY: "Hold VELG for små bokstaver eller sekundærtegn"
|
||||||
|
STR_KB_HINT_URL_SNIPPETS: "Trykk URL for tekstbiter"
|
||||||
|
STR_SD_FIRMWARE_UPDATE: "Fastvareoppdatering fra SD-kort"
|
||||||
|
STR_SELECT_FIRMWARE_FILE: "Velg fastvarefil (.bin)"
|
||||||
|
STR_NO_BIN_FILES: "Ingen .bin-filer funnet"
|
||||||
|
STR_VALIDATING_FIRMWARE: "Validerer fastvare..."
|
||||||
|
STR_INVALID_FIRMWARE: "Ugyldig fastvarefil"
|
||||||
|
STR_FIRMWARE_TOO_LARGE: "Fastvaren er for stor for partisjonen"
|
||||||
|
STR_FIRMWARE_TOO_SMALL: "Fastvarefilen er for liten"
|
||||||
|
STR_FIRMWARE_UPDATE_PROMPT: "Oppdatere fastvare?"
|
||||||
|
STR_FIRMWARE_FILE_OPEN_FAILED: "Kan ikke åpne filen"
|
||||||
|
STR_FIRMWARE_WRITE_FAILED: "Skriving av fastvare mislyktes"
|
||||||
|
STR_FIRMWARE_UPDATE_DO_NOT_POWER_OFF: "Ikke slå av!"
|
||||||
|
STR_RECOVERY_MODE: "Gjenopprettingsmodus"
|
||||||
|
STR_RECOVERY_MODE_HINT: "Legg firmware.bin i roten av SD-kortet og velg den"
|
||||||
@@ -97,6 +97,7 @@ STR_USERNAME: "Użytkownik"
|
|||||||
STR_PASSWORD: "Hasło"
|
STR_PASSWORD: "Hasło"
|
||||||
STR_SYNC_SERVER_URL: "Serwer URL synchronizacji"
|
STR_SYNC_SERVER_URL: "Serwer URL synchronizacji"
|
||||||
STR_DOCUMENT_MATCHING: "Dopasowanie dokumentów"
|
STR_DOCUMENT_MATCHING: "Dopasowanie dokumentów"
|
||||||
|
STR_SEND_METADATA: "Wyślij metadane dokumentu"
|
||||||
STR_AUTHENTICATE: "Uwierzytelnianie"
|
STR_AUTHENTICATE: "Uwierzytelnianie"
|
||||||
STR_KOREADER_USERNAME: "Użytkownik KOReader"
|
STR_KOREADER_USERNAME: "Użytkownik KOReader"
|
||||||
STR_KOREADER_PASSWORD: "Hasło KOReader"
|
STR_KOREADER_PASSWORD: "Hasło KOReader"
|
||||||
@@ -275,7 +276,6 @@ STR_CHAPTER_PREFIX: "Rozdział: "
|
|||||||
STR_PAGES_SEPARATOR: " stron | "
|
STR_PAGES_SEPARATOR: " stron | "
|
||||||
STR_BOOK_PREFIX: "Książka: "
|
STR_BOOK_PREFIX: "Książka: "
|
||||||
STR_CALIBRE_URL_HINT: "Dla Calibre, dodaj /opds do adresu URL"
|
STR_CALIBRE_URL_HINT: "Dla Calibre, dodaj /opds do adresu URL"
|
||||||
STR_PERCENT_STEP_HINT: "Lewo/Prawo: 1% Góra/Dół: 10%"
|
|
||||||
STR_SYNCING_TIME: "Synchronizacja czasu..."
|
STR_SYNCING_TIME: "Synchronizacja czasu..."
|
||||||
STR_CALC_HASH: "Obliczanie sumy kontrolnej..."
|
STR_CALC_HASH: "Obliczanie sumy kontrolnej..."
|
||||||
STR_HASH_FAILED: "Błąd obliczania sumy kontrolnej"
|
STR_HASH_FAILED: "Błąd obliczania sumy kontrolnej"
|
||||||
@@ -308,7 +308,8 @@ STR_NO_FOOTNOTES: "Brak przypisów na tej stronie"
|
|||||||
STR_LINK: "[link]"
|
STR_LINK: "[link]"
|
||||||
STR_SLEEP_TIMER_VALUE_FORMAT: "%u min"
|
STR_SLEEP_TIMER_VALUE_FORMAT: "%u min"
|
||||||
STR_SLEEP_NEVER: "Nigdy"
|
STR_SLEEP_NEVER: "Nigdy"
|
||||||
STR_SLEEP_TIMER_STEP_HINT: "Lewo/Prawo: 1 min Góra/Dół: 5 min"
|
STR_STEP_HINT_FRONT: "Przyciski przednie:"
|
||||||
|
STR_STEP_HINT_SIDE: "Przyciski boczne:"
|
||||||
STR_SCREENSHOT_BUTTON: "Zrób zrzut ekranu"
|
STR_SCREENSHOT_BUTTON: "Zrób zrzut ekranu"
|
||||||
STR_ADD_SERVER: "Dodaj serwer"
|
STR_ADD_SERVER: "Dodaj serwer"
|
||||||
STR_SERVER_NAME: "Nazwa serwera"
|
STR_SERVER_NAME: "Nazwa serwera"
|
||||||
@@ -360,3 +361,5 @@ STR_FIRMWARE_WRITE_FAILED: "Zapis oprogramowania nieudany"
|
|||||||
STR_FIRMWARE_UPDATE_DO_NOT_POWER_OFF: "Nie wyłączać!"
|
STR_FIRMWARE_UPDATE_DO_NOT_POWER_OFF: "Nie wyłączać!"
|
||||||
STR_RECOVERY_MODE: "Tryb przywracania"
|
STR_RECOVERY_MODE: "Tryb przywracania"
|
||||||
STR_RECOVERY_MODE_HINT: "Umieść firmware.bin w głównym katalogu karty SD i wybierz go"
|
STR_RECOVERY_MODE_HINT: "Umieść firmware.bin w głównym katalogu karty SD i wybierz go"
|
||||||
|
STR_ADD_HIDDEN_NETWORK: "Dodaj ukrytą sieć..."
|
||||||
|
STR_ENTER_WIFI_SSID: "Wprowadź nazwę sieci (SSID)"
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ STR_END_OF_BOOK: "Fim do livro"
|
|||||||
STR_EMPTY_CHAPTER: "Capítulo vazio"
|
STR_EMPTY_CHAPTER: "Capítulo vazio"
|
||||||
STR_INDEXING: "Indexando"
|
STR_INDEXING: "Indexando"
|
||||||
STR_MEMORY_ERROR: "Erro de memória"
|
STR_MEMORY_ERROR: "Erro de memória"
|
||||||
STR_PAGE_LOAD_ERROR: "Erro página"
|
STR_PAGE_LOAD_ERROR: "Erro ao carregar pág."
|
||||||
STR_EMPTY_FILE: "Arquivo vazio"
|
STR_EMPTY_FILE: "Arquivo vazio"
|
||||||
STR_OUT_OF_BOUNDS: "Fora dos limites"
|
STR_OUT_OF_BOUNDS: "Fora dos limites"
|
||||||
STR_LOADING: "Carregando..."
|
STR_LOADING: "Carregando..."
|
||||||
@@ -32,8 +32,8 @@ STR_CONNECTING: "Conectando..."
|
|||||||
STR_CONNECTED: "Conectado!"
|
STR_CONNECTED: "Conectado!"
|
||||||
STR_CONNECTION_FAILED: "Falha na conexão"
|
STR_CONNECTION_FAILED: "Falha na conexão"
|
||||||
STR_FORGET_NETWORK: "Esquecer rede?"
|
STR_FORGET_NETWORK: "Esquecer rede?"
|
||||||
STR_SAVE_PASSWORD: "Salvar senha a próxima vez?"
|
STR_SAVE_PASSWORD: "Salvar senha na próxima vez?"
|
||||||
STR_PRESS_OK_SCAN: "Pressione OK procurar novamente"
|
STR_PRESS_OK_SCAN: "Pressione OK para buscar novamente"
|
||||||
STR_JOIN_NETWORK: "Entrar em uma rede"
|
STR_JOIN_NETWORK: "Entrar em uma rede"
|
||||||
STR_CREATE_HOTSPOT: "Criar hotspot"
|
STR_CREATE_HOTSPOT: "Criar hotspot"
|
||||||
STR_JOIN_DESC: "Conecte-se a uma rede Wi-Fi existente"
|
STR_JOIN_DESC: "Conecte-se a uma rede Wi-Fi existente"
|
||||||
@@ -41,7 +41,7 @@ STR_HOTSPOT_DESC: "Crie uma rede Wi-Fi para outras pessoas entrarem"
|
|||||||
STR_STARTING_HOTSPOT: "Iniciando hotspot..."
|
STR_STARTING_HOTSPOT: "Iniciando hotspot..."
|
||||||
STR_HOTSPOT_MODE: "Modo hotspot"
|
STR_HOTSPOT_MODE: "Modo hotspot"
|
||||||
STR_CONNECT_WIFI_HINT: "Conecte seu dispositivo a esta rede Wi-Fi"
|
STR_CONNECT_WIFI_HINT: "Conecte seu dispositivo a esta rede Wi-Fi"
|
||||||
STR_OPEN_URL_HINT: "Abra este URL seu navegador"
|
STR_OPEN_URL_HINT: "Abra esta URL no seu navegador"
|
||||||
STR_OR_HTTP_PREFIX: "ou http://"
|
STR_OR_HTTP_PREFIX: "ou http://"
|
||||||
STR_SCAN_QR_HINT: "ou escaneie o QR code com seu celular:"
|
STR_SCAN_QR_HINT: "ou escaneie o QR code com seu celular:"
|
||||||
STR_CALIBRE_WIRELESS: "Calibre sem fio"
|
STR_CALIBRE_WIRELESS: "Calibre sem fio"
|
||||||
@@ -50,30 +50,36 @@ STR_MAC_ADDRESS: "Endereço MAC:"
|
|||||||
STR_CHECKING_WIFI: "Verificando Wi-Fi..."
|
STR_CHECKING_WIFI: "Verificando Wi-Fi..."
|
||||||
STR_ENTER_WIFI_PASSWORD: "Digite a senha Wi-Fi"
|
STR_ENTER_WIFI_PASSWORD: "Digite a senha Wi-Fi"
|
||||||
STR_TO_PREFIX: "para"
|
STR_TO_PREFIX: "para"
|
||||||
STR_CALIBRE_RECEIVING: "Recebendo:"
|
STR_CALIBRE_RECEIVING: "Recebendo: "
|
||||||
STR_CALIBRE_RECEIVED: "Recebido:"
|
STR_CALIBRE_RECEIVED: "Recebido: "
|
||||||
STR_CALIBRE_INSTRUCTION_1: "1) Instale o plugin CrossPoint Reader"
|
STR_CALIBRE_INSTRUCTION_1: "1) Instale o plugin CrossPoint Reader"
|
||||||
STR_CALIBRE_INSTRUCTION_2: "2) Esteja na mesma rede Wi-Fi"
|
STR_CALIBRE_INSTRUCTION_2: "2) Esteja na mesma rede Wi-Fi"
|
||||||
STR_CALIBRE_INSTRUCTION_3: "3) No Calibre: \"Enviar o dispositivo\""
|
STR_CALIBRE_INSTRUCTION_3: "3) No Calibre: \"Enviar ao dispositivo\""
|
||||||
STR_CALIBRE_INSTRUCTION_4: "\"Mantenha esta tela aberta durante o envio\""
|
STR_CALIBRE_INSTRUCTION_4: "\"Mantenha esta tela aberta durante o envio\""
|
||||||
STR_CAT_DISPLAY: "Tela"
|
STR_CAT_DISPLAY: "Tela"
|
||||||
STR_CAT_READER: "Leitor"
|
STR_CAT_READER: "Leitor"
|
||||||
STR_CAT_CONTROLS: "Controles"
|
STR_CAT_CONTROLS: "Controles"
|
||||||
STR_CAT_SYSTEM: "Sistema"
|
STR_CAT_SYSTEM: "Sistema"
|
||||||
STR_SLEEP_SCREEN: "Tela de repouso"
|
STR_SLEEP_SCREEN: "Tela de repouso"
|
||||||
|
STR_QUICK_RESUME_TIMEOUT: "Retomada rápida após tempo limite"
|
||||||
STR_SLEEP_COVER_MODE: "Modo capa tela repouso"
|
STR_SLEEP_COVER_MODE: "Modo capa tela repouso"
|
||||||
STR_HIDE_BATTERY: "Ocultar % da bateria"
|
STR_HIDE_BATTERY: "Ocultar % da bateria"
|
||||||
STR_EXTRA_SPACING: "Espaço de parágrafos extra"
|
STR_EXTRA_SPACING: "Espaço de parágrafos extra"
|
||||||
STR_TEXT_AA: "Suavização de texto"
|
STR_TEXT_AA: "Suavização de texto"
|
||||||
|
STR_IMAGES: "Imagens"
|
||||||
|
STR_IMAGES_DISPLAY: "Exibição"
|
||||||
|
STR_IMAGES_PLACEHOLDER: "Espaço reservado"
|
||||||
|
STR_IMAGES_SUPPRESS: "Ocultar"
|
||||||
STR_SHORT_PWR_BTN: "Clique curto botão ligar"
|
STR_SHORT_PWR_BTN: "Clique curto botão ligar"
|
||||||
STR_ORIENTATION: "Orientação de leitura"
|
STR_ORIENTATION: "Orientação de leitura"
|
||||||
STR_SIDE_BTN_LAYOUT: "Disposição botões laterais"
|
STR_SIDE_BTN_LAYOUT: "Disposição botões laterais"
|
||||||
STR_FRONT_BTN_FOLLOW_ORIENTATION: "Orientar botões frontais"
|
STR_FRONT_BTN_FOLLOW_ORIENTATION: "Orientar botões frontais"
|
||||||
STR_LONG_PRESS_BEHAVIOR: "Comportamento do botão de premir e segurar"
|
STR_LONG_PRESS_BEHAVIOR: "Comportamento de Pressionar e segurar"
|
||||||
STR_LONG_PRESS_BEHAVIOR_OFF: "Desligado"
|
STR_LONG_PRESS_BEHAVIOR_OFF: "DESL."
|
||||||
STR_LONG_PRESS_BEHAVIOR_SKIP: "Saltar capítulo"
|
STR_LONG_PRESS_BEHAVIOR_SKIP: "Pular capítulo"
|
||||||
STR_LONG_PRESS_BEHAVIOR_ORIENTATION: "Alterar orientação"
|
STR_LONG_PRESS_BEHAVIOR_ORIENTATION: "Alterar orientação"
|
||||||
STR_FONT_PREVIEW_TEXT: "Vejo galã sexy pôr quinze kiwis à força em baú achatado"
|
STR_LONG_PRESS_MENU: "Pressionar e segurar Menu"
|
||||||
|
STR_FONT_PREVIEW_TEXT: "Um pequeno jabuti xereta viu dez cegonhas felizes. "
|
||||||
STR_FONT_FAMILY: "Fonte do leitor"
|
STR_FONT_FAMILY: "Fonte do leitor"
|
||||||
STR_FONT_SIZE: "Tam. fonte UI"
|
STR_FONT_SIZE: "Tam. fonte UI"
|
||||||
STR_LINE_SPACING: "Espaçamento entre linhas"
|
STR_LINE_SPACING: "Espaçamento entre linhas"
|
||||||
@@ -81,6 +87,7 @@ STR_SCREEN_MARGIN: "Margens da tela"
|
|||||||
STR_PARA_ALIGNMENT: "Alinhamento parágrafo"
|
STR_PARA_ALIGNMENT: "Alinhamento parágrafo"
|
||||||
STR_HYPHENATION: "Hifenização"
|
STR_HYPHENATION: "Hifenização"
|
||||||
STR_TIME_TO_SLEEP: "Tempo para repousar"
|
STR_TIME_TO_SLEEP: "Tempo para repousar"
|
||||||
|
STR_SHOW_HIDDEN_FILES: "Mostrar arquivos ocultos"
|
||||||
STR_REMOVE_READ_FROM_RECENTS: "Limpar livros lidos da lista de recentes"
|
STR_REMOVE_READ_FROM_RECENTS: "Limpar livros lidos da lista de recentes"
|
||||||
STR_MOVE_FINISHED_TO_READ: "Mover livros lidos para a pasta Read"
|
STR_MOVE_FINISHED_TO_READ: "Mover livros lidos para a pasta Read"
|
||||||
STR_REFRESH_FREQ: "Frequência atualização"
|
STR_REFRESH_FREQ: "Frequência atualização"
|
||||||
@@ -92,6 +99,7 @@ STR_USERNAME: "Nome de usuário"
|
|||||||
STR_PASSWORD: "Senha"
|
STR_PASSWORD: "Senha"
|
||||||
STR_SYNC_SERVER_URL: "URL servidor sincronização"
|
STR_SYNC_SERVER_URL: "URL servidor sincronização"
|
||||||
STR_DOCUMENT_MATCHING: "Documento correspondente"
|
STR_DOCUMENT_MATCHING: "Documento correspondente"
|
||||||
|
STR_SEND_METADATA: "Enviar metadados do doc."
|
||||||
STR_AUTHENTICATE: "Autenticar"
|
STR_AUTHENTICATE: "Autenticar"
|
||||||
STR_KOREADER_USERNAME: "Usuário do KOReader"
|
STR_KOREADER_USERNAME: "Usuário do KOReader"
|
||||||
STR_KOREADER_PASSWORD: "Senha do KOReader"
|
STR_KOREADER_PASSWORD: "Senha do KOReader"
|
||||||
@@ -128,6 +136,7 @@ STR_ALWAYS: "Sempre"
|
|||||||
STR_IGNORE: "Ignorar"
|
STR_IGNORE: "Ignorar"
|
||||||
STR_SLEEP: "Repouso"
|
STR_SLEEP: "Repouso"
|
||||||
STR_PAGE_TURN: "Virar página"
|
STR_PAGE_TURN: "Virar página"
|
||||||
|
STR_FORCE_REFRESH: "Atualizar tela"
|
||||||
STR_PORTRAIT: "Retrato"
|
STR_PORTRAIT: "Retrato"
|
||||||
STR_LANDSCAPE_CW: "Paisagem H"
|
STR_LANDSCAPE_CW: "Paisagem H"
|
||||||
STR_INVERTED: "Invertido"
|
STR_INVERTED: "Invertido"
|
||||||
@@ -135,6 +144,9 @@ STR_ORIENTATION_INVERTED: "Retrato 180°"
|
|||||||
STR_LANDSCAPE_CCW: "Paisagem AH"
|
STR_LANDSCAPE_CCW: "Paisagem AH"
|
||||||
STR_PREV_NEXT: "Ant/Próx"
|
STR_PREV_NEXT: "Ant/Próx"
|
||||||
STR_NEXT_PREV: "Próx/Ant"
|
STR_NEXT_PREV: "Próx/Ant"
|
||||||
|
STR_KOSYNC: "KOSync"
|
||||||
|
STR_BOOKMARK_OPTION: "Marcador"
|
||||||
|
STR_DISABLED: "Desabilitado"
|
||||||
STR_NOTO_SERIF: "Noto Serif"
|
STR_NOTO_SERIF: "Noto Serif"
|
||||||
STR_NOTO_SANS: "Noto Sans"
|
STR_NOTO_SANS: "Noto Sans"
|
||||||
STR_SMALL: "Pequeno"
|
STR_SMALL: "Pequeno"
|
||||||
@@ -162,17 +174,21 @@ STR_UPDATING: "Atualizando..."
|
|||||||
STR_NO_UPDATE: "Nenhuma atualização disponível"
|
STR_NO_UPDATE: "Nenhuma atualização disponível"
|
||||||
STR_UPDATE_FAILED: "Falha na atualização"
|
STR_UPDATE_FAILED: "Falha na atualização"
|
||||||
STR_UPDATE_COMPLETE: "Atualização concluída"
|
STR_UPDATE_COMPLETE: "Atualização concluída"
|
||||||
STR_POWER_ON_HINT: "Pressione e segure o botão energia ligar novamente"
|
STR_POWER_ON_HINT: "Pressione e segure o botão de energia para ligar novamente"
|
||||||
STR_NO_ENTRIES: "Nenhum entries encontrado"
|
STR_RESTARTING_HINT: "Reiniciando... Se não reiniciar, pressione e segure o botão de energia por alguns segundos."
|
||||||
|
STR_NO_ENTRIES: "Nenhuma entrada encontrada"
|
||||||
STR_DOWNLOADING: "Baixando..."
|
STR_DOWNLOADING: "Baixando..."
|
||||||
STR_DOWNLOAD_FAILED: "Falha no download"
|
STR_DOWNLOAD_FAILED: "Falha no download"
|
||||||
STR_ERROR_MSG: "Erro:"
|
STR_ERROR_MSG: "Erro:"
|
||||||
STR_UNNAMED: "Sem nome"
|
STR_UNNAMED: "Sem nome"
|
||||||
|
STR_HOLD_OPEN_TO_DELETE: "Segure 'Abrir' para excluir"
|
||||||
STR_NO_SERVER_URL: "Nenhum URL servidor configurado"
|
STR_NO_SERVER_URL: "Nenhum URL servidor configurado"
|
||||||
STR_FETCH_FEED_FAILED: "Falha ao buscar o feed"
|
STR_FETCH_FEED_FAILED: "Falha ao buscar o feed"
|
||||||
STR_PARSE_FEED_FAILED: "Falha ao interpretar o feed"
|
STR_PARSE_FEED_FAILED: "Falha ao interpretar o feed"
|
||||||
STR_NETWORK_PREFIX: "Rede:"
|
STR_NEXT_PAGE: "Próxima Página »"
|
||||||
STR_IP_ADDRESS_PREFIX: "Endereço IP:"
|
STR_PREV_PAGE: "« Página Anterior"
|
||||||
|
STR_NETWORK_PREFIX: "Rede: "
|
||||||
|
STR_IP_ADDRESS_PREFIX: "Endereço IP: "
|
||||||
STR_ERROR_GENERAL_FAILURE: "Erro: falha geral"
|
STR_ERROR_GENERAL_FAILURE: "Erro: falha geral"
|
||||||
STR_ERROR_NETWORK_NOT_FOUND: "Erro: rede não encontrada"
|
STR_ERROR_NETWORK_NOT_FOUND: "Erro: rede não encontrada"
|
||||||
STR_ERROR_CONNECTION_TIMEOUT: "Erro: tempo limite conexão"
|
STR_ERROR_CONNECTION_TIMEOUT: "Erro: tempo limite conexão"
|
||||||
@@ -181,10 +197,9 @@ STR_BACK: "« Voltar"
|
|||||||
STR_EXIT: "« Sair"
|
STR_EXIT: "« Sair"
|
||||||
STR_HOME: "« Início"
|
STR_HOME: "« Início"
|
||||||
STR_SELECT: "Escolher"
|
STR_SELECT: "Escolher"
|
||||||
|
STR_SELECTED: "Selecionado"
|
||||||
STR_TOGGLE: "Alternar"
|
STR_TOGGLE: "Alternar"
|
||||||
STR_TOGGLE_BOOKMARK: "Alternar marcador"
|
STR_TOGGLE_BOOKMARK: "Alternar marcador"
|
||||||
STR_BOOKMARK_REMOVED: "Marcador removido."
|
|
||||||
STR_HOLD_OPEN_TO_DELETE: "Mantenha Abrir pressionado para excluir"
|
|
||||||
STR_CONFIRM: "Confirmar"
|
STR_CONFIRM: "Confirmar"
|
||||||
STR_CANCEL: "Cancelar"
|
STR_CANCEL: "Cancelar"
|
||||||
STR_CONNECT: "Conectar"
|
STR_CONNECT: "Conectar"
|
||||||
@@ -193,6 +208,8 @@ STR_DOWNLOAD: "Baixar"
|
|||||||
STR_RETRY: "Tentar novamente"
|
STR_RETRY: "Tentar novamente"
|
||||||
STR_YES: "Sim"
|
STR_YES: "Sim"
|
||||||
STR_NO: "Não"
|
STR_NO: "Não"
|
||||||
|
STR_SHOW: "Mostrar"
|
||||||
|
STR_HIDE: "Ocultar"
|
||||||
STR_STATE_ON: "LIG."
|
STR_STATE_ON: "LIG."
|
||||||
STR_STATE_OFF: "DESL."
|
STR_STATE_OFF: "DESL."
|
||||||
STR_NOT_SET: "Não definido"
|
STR_NOT_SET: "Não definido"
|
||||||
@@ -203,15 +220,51 @@ STR_DIR_DOWN: "Baixo"
|
|||||||
STR_OK_BUTTON: "OK"
|
STR_OK_BUTTON: "OK"
|
||||||
STR_SLEEP_COVER_FILTER: "Filtro capa tela repouso"
|
STR_SLEEP_COVER_FILTER: "Filtro capa tela repouso"
|
||||||
STR_FILTER_CONTRAST: "Contraste"
|
STR_FILTER_CONTRAST: "Contraste"
|
||||||
STR_UI_THEME: "Tema da interface"
|
STR_CUSTOMISE_STATUS_BAR: "Customizar Barra de Status"
|
||||||
|
STR_CHAPTER_PAGE_COUNT: "Páginas do Capítulo"
|
||||||
|
STR_BOOK_PROGRESS_PERCENTAGE: "Porcentagem de Progresso"
|
||||||
|
STR_PROGRESS_BAR: "Barra de Progresso"
|
||||||
|
STR_PROGRESS_BAR_THICKNESS: "Espessura da Barra de Progresso"
|
||||||
|
STR_PROGRESS_BAR_THIN: "Fino"
|
||||||
|
STR_PROGRESS_BAR_MEDIUM: "Médio"
|
||||||
|
STR_PROGRESS_BAR_THICK: "Grosso"
|
||||||
|
STR_BOOK: "Livro"
|
||||||
|
STR_CHAPTER: "Capítulo"
|
||||||
|
STR_EXAMPLE_CHAPTER: "Capítulo 21"
|
||||||
|
STR_EXAMPLE_BOOK: "Título do Livro"
|
||||||
|
STR_PREVIEW: "Prévia"
|
||||||
|
STR_TITLE: "Título"
|
||||||
|
STR_BATTERY: "Bateria"
|
||||||
|
STR_XTC_STATUS_BAR: "Barra de Status XTC"
|
||||||
|
STR_BOTTOM: "Baixo"
|
||||||
|
STR_TOP: "Topo"
|
||||||
|
STR_CLOCK: "Relógio"
|
||||||
|
STR_CLOCK_UTC_OFFSET: "Offset UTC do Relógio"
|
||||||
|
STR_CLOCK_FORMAT: "Formato do Relógio"
|
||||||
|
STR_CLOCK_FORMAT_24H: "24 horas"
|
||||||
|
STR_CLOCK_FORMAT_12H: "12 horas"
|
||||||
|
STR_CURRENT_TIME: "Hora atual:"
|
||||||
|
STR_NEXT_FIELD: "Próximo"
|
||||||
|
STR_CLOCK_SYNC: "Sincronizar Relógio"
|
||||||
|
STR_CLOCK_SYNC_NOW: "Sincronizar relógio agora"
|
||||||
|
STR_CLOCK_SYNCING: "Sincronizando via NTP..."
|
||||||
|
STR_CLOCK_SYNC_OK: "Relógio sincronizado"
|
||||||
|
STR_CLOCK_SYNC_FAIL: "Falha na sincronização"
|
||||||
|
STR_CLOCK_SYNC_NO_WIFI: "Wi-Fi não conectado"
|
||||||
|
STR_CLOCK_SYNC_NO_WIFI_HINT: "Conecte ao Wi-Fi primeiro, depois tente novamente."
|
||||||
|
STR_CLOCK_SYNCED: "Relógio sincronizado"
|
||||||
|
STR_UI_THEME: "Tema"
|
||||||
STR_THEME_CLASSIC: "Clássico"
|
STR_THEME_CLASSIC: "Clássico"
|
||||||
STR_THEME_LYRA: "Lyra"
|
STR_THEME_LYRA: "Lyra"
|
||||||
STR_THEME_ROUNDEDRAFF: "RoundedRaff"
|
STR_THEME_ROUNDEDRAFF: "RoundedRaff"
|
||||||
STR_THEME_LYRA_EXTENDED: "Lyra Extended"
|
STR_THEME_LYRA_EXTENDED: "Lyra Estendido"
|
||||||
STR_SUNLIGHT_FADING_FIX: "Ajuste desbotamento ao sol"
|
STR_SUNLIGHT_FADING_FIX: "Ajuste desbotamento ao sol"
|
||||||
STR_QUICK_RESUME_TIMEOUT: "Retomada rápida após tempo limite"
|
|
||||||
STR_REMAP_FRONT_BUTTONS: "Remapear botões frontais"
|
STR_REMAP_FRONT_BUTTONS: "Remapear botões frontais"
|
||||||
|
STR_BOOKMARKS: "Marcadores"
|
||||||
|
STR_BOOKMARK_ADDED: "Marcador adicionado."
|
||||||
|
STR_BOOKMARK_REMOVED: "Marcador removido."
|
||||||
STR_OPDS_BROWSER: "Navegador OPDS"
|
STR_OPDS_BROWSER: "Navegador OPDS"
|
||||||
|
STR_SEARCH: "Busca"
|
||||||
STR_COVER_CUSTOM: "Capa + personalizado"
|
STR_COVER_CUSTOM: "Capa + personalizado"
|
||||||
STR_QUICK_RESUME: "Retomada rápida"
|
STR_QUICK_RESUME: "Retomada rápida"
|
||||||
STR_MENU_RECENT_BOOKS: "Livros recentes"
|
STR_MENU_RECENT_BOOKS: "Livros recentes"
|
||||||
@@ -225,11 +278,11 @@ STR_CALIBRE_SETUP: "Configuração"
|
|||||||
STR_CALIBRE_STATUS: "Status"
|
STR_CALIBRE_STATUS: "Status"
|
||||||
STR_CLEAR_BUTTON: "Limpar"
|
STR_CLEAR_BUTTON: "Limpar"
|
||||||
STR_DEFAULT_VALUE: "Padrão"
|
STR_DEFAULT_VALUE: "Padrão"
|
||||||
STR_REMAP_PROMPT: "Pressione um botão frontal cada função"
|
STR_REMAP_PROMPT: "Pressione um botão frontal para cada função"
|
||||||
STR_UNASSIGNED: "Não atribuído"
|
STR_UNASSIGNED: "Não atribuído"
|
||||||
STR_ALREADY_ASSIGNED: "Já atribuído"
|
STR_ALREADY_ASSIGNED: "Já atribuído"
|
||||||
STR_REMAP_RESET_HINT: "Botão lateral cima: redefinir o disposição padrão"
|
STR_REMAP_RESET_HINT: "Botão lateral de cima: redefinir layout padrão"
|
||||||
STR_REMAP_CANCEL_HINT: "Botão lateral baixo: cancelar remapeamento"
|
STR_REMAP_CANCEL_HINT: "Botão lateral de baixo: cancelar remapeamento"
|
||||||
STR_HW_BACK_LABEL: "Voltar (1º botão)"
|
STR_HW_BACK_LABEL: "Voltar (1º botão)"
|
||||||
STR_HW_CONFIRM_LABEL: "Confirmar (2º botão)"
|
STR_HW_CONFIRM_LABEL: "Confirmar (2º botão)"
|
||||||
STR_HW_LEFT_LABEL: "Esquerda (3º botão)"
|
STR_HW_LEFT_LABEL: "Esquerda (3º botão)"
|
||||||
@@ -239,14 +292,15 @@ STR_GO_HOME_BUTTON: "Ir para o início"
|
|||||||
STR_SYNC_PROGRESS: "Sincronizar progresso"
|
STR_SYNC_PROGRESS: "Sincronizar progresso"
|
||||||
STR_DELETE_CACHE: "Excluir cache do livro"
|
STR_DELETE_CACHE: "Excluir cache do livro"
|
||||||
STR_DELETE: "Excluir"
|
STR_DELETE: "Excluir"
|
||||||
|
STR_CONFIRM_DELETE_BOOKMARK: "Excluir este marcador?"
|
||||||
|
STR_DISPLAY_QR: "Mostrar página como QR"
|
||||||
STR_CHAPTER_PREFIX: "Capítulo:"
|
STR_CHAPTER_PREFIX: "Capítulo:"
|
||||||
STR_PAGES_SEPARATOR: "páginas |"
|
STR_PAGES_SEPARATOR: "páginas |"
|
||||||
STR_BOOK_PREFIX: "Livro:"
|
STR_BOOK_PREFIX: "Livro:"
|
||||||
STR_CALIBRE_URL_HINT: "Para o Calibre, adicione /opds ao seu URL"
|
STR_CALIBRE_URL_HINT: "Para o Calibre, adicione /opds ao seu URL"
|
||||||
STR_PERCENT_STEP_HINT: "Esq/Dir: 1% Cima/Baixo: 10%"
|
|
||||||
STR_SYNCING_TIME: "Sincronizando horário..."
|
STR_SYNCING_TIME: "Sincronizando horário..."
|
||||||
STR_CALC_HASH: "Calculando hash documento..."
|
STR_CALC_HASH: "Calculando hash do documento..."
|
||||||
STR_HASH_FAILED: "Falha ao calcular o hash documento"
|
STR_HASH_FAILED: "Falha ao calcular o hash do documento"
|
||||||
STR_FETCH_PROGRESS: "Buscando progresso remoto..."
|
STR_FETCH_PROGRESS: "Buscando progresso remoto..."
|
||||||
STR_UPLOAD_PROGRESS: "Enviando progresso..."
|
STR_UPLOAD_PROGRESS: "Enviando progresso..."
|
||||||
STR_NO_CREDENTIALS_MSG: "Nenhuma credencial configurada"
|
STR_NO_CREDENTIALS_MSG: "Nenhuma credencial configurada"
|
||||||
@@ -264,14 +318,71 @@ STR_UPLOAD_PROMPT: "Enviar posição atual?"
|
|||||||
STR_UPLOAD_SUCCESS: "Progresso enviado!"
|
STR_UPLOAD_SUCCESS: "Progresso enviado!"
|
||||||
STR_SYNC_FAILED_MSG: "Falha na sincronização"
|
STR_SYNC_FAILED_MSG: "Falha na sincronização"
|
||||||
STR_SAVE_PROGRESS_FAILED: "Não foi possível salvar o progresso"
|
STR_SAVE_PROGRESS_FAILED: "Não foi possível salvar o progresso"
|
||||||
STR_SECTION_PREFIX: "Seção"
|
STR_SECTION_PREFIX: "Seção "
|
||||||
STR_UPLOAD: "Enviar"
|
STR_UPLOAD: "Enviar"
|
||||||
STR_BOOK_S_STYLE: "Estilo do livro"
|
STR_BOOK_S_STYLE: "Estilo do livro"
|
||||||
STR_EMBEDDED_STYLE: "Estilo embutido"
|
STR_EMBEDDED_STYLE: "Estilo embutido"
|
||||||
STR_FOCUS_READING: "Leitura focada"
|
STR_FOCUS_READING: "Leitura focada"
|
||||||
STR_OPDS_SERVER_URL: "URL do servidor OPDS"
|
STR_OPDS_SERVER_URL: "URL do servidor OPDS"
|
||||||
|
STR_PWR_BTN_FOOTNOTE_BACK: "Retorno rápido de notas de rodapé"
|
||||||
|
STR_SET_SLEEP_COVER: "Definir capa"
|
||||||
|
STR_FOOTNOTES: "Notas de rodapé"
|
||||||
|
STR_NO_FOOTNOTES: "Nenhuma nota de rodapé nesta página"
|
||||||
|
STR_LINK: "[link]"
|
||||||
STR_SCREENSHOT_BUTTON: "Capturar tela"
|
STR_SCREENSHOT_BUTTON: "Capturar tela"
|
||||||
STR_SLEEP_TIMER_VALUE_FORMAT: "%u min"
|
STR_SLEEP_TIMER_VALUE_FORMAT: "%u min"
|
||||||
STR_SLEEP_NEVER: "Nunca"
|
STR_SLEEP_NEVER: "Nunca"
|
||||||
STR_SLEEP_TIMER_STEP_HINT: "Esq/Dir: 1 min Cima/Baixo: 5 min"
|
STR_STEP_HINT_FRONT: "Botões frontais:"
|
||||||
|
STR_STEP_HINT_SIDE: "Botões laterais:"
|
||||||
|
STR_ADD_SERVER: "Adicionar Servidor"
|
||||||
|
STR_SERVER_NAME: "Nome do Servidor"
|
||||||
|
STR_NO_SERVERS: "Nenhum servidor OPDS configurado"
|
||||||
|
STR_DELETE_SERVER: "Excluir Servidor"
|
||||||
|
STR_OPDS_SERVERS: "Servidores OPDS"
|
||||||
|
STR_AUTO_TURN_ENABLED: "Virada automática ativada: "
|
||||||
|
STR_AUTO_TURN_PAGES_PER_MIN: "Virada automática (páginas por minuto)"
|
||||||
|
STR_MANAGE_FONTS: "Gerenciar fontes"
|
||||||
|
STR_FONT_BROWSER: "Navegador de fontes"
|
||||||
|
STR_LOADING_FONT_LIST: "Carregando lista de fontes..."
|
||||||
|
STR_NO_FONTS_AVAILABLE: "Nenhuma fonte disponível"
|
||||||
|
STR_FONT_INSTALLED: "Fonte instalada!"
|
||||||
|
STR_FONT_INSTALL_FAILED: "Falha na instalação da fonte"
|
||||||
|
STR_INSTALLED: "Instalada"
|
||||||
|
STR_DOWNLOAD_ALL: "Baixar tudo"
|
||||||
|
STR_UPDATE_ALL: "Atualizar tudo"
|
||||||
|
STR_UPDATE_AVAILABLE: "Atualizar"
|
||||||
|
STR_CRASH_TITLE: "Falha do sistema"
|
||||||
|
STR_CRASH_DESCRIPTION: "Um relatório detalhado foi salvo em crash_report.txt. Por favor, inclua este arquivo no seu relatório de erro."
|
||||||
|
STR_CRASH_REASON: "Motivo da falha:"
|
||||||
|
STR_CRASH_NO_REASON: "(Nenhum motivo registrado)"
|
||||||
STR_TILT_PAGE_TURN: "Virar página por inclinação"
|
STR_TILT_PAGE_TURN: "Virar página por inclinação"
|
||||||
|
STR_KB_HINT_MOVE_CURSOR: "Pressione ESQ ou DIR para mover o cursor"
|
||||||
|
STR_KB_HINT_RETURN_CURSOR: "Pressione ESQ para retornar à posição do cursor"
|
||||||
|
STR_KB_HINT_HIDE_PASSWORD: "Segure DIR e pressione [***] para ocultar a senha"
|
||||||
|
STR_KB_HINT_SHOW_PASSWORD: "Segure DIR e pressione [abc] para exibir a senha"
|
||||||
|
STR_KB_HINT_TOGGLE_HIDE_PASSWORD: "Pressione [***] para ocultar a senha"
|
||||||
|
STR_KB_HINT_TOGGLE_SHOW_PASSWORD: "Pressione [abc] para exibir a senha"
|
||||||
|
STR_KB_HINT_EDIT_ENTRY: "Segure CIMA para editar a entrada"
|
||||||
|
STR_KB_TIPS: "Dicas:"
|
||||||
|
STR_KB_HINT_RETURN_KEYBOARD: "Pressione BAIXO para retornar ao teclado"
|
||||||
|
STR_KB_HINT_EXIT_URL_MODE: "Pressione ABC para sair do modo URL"
|
||||||
|
STR_KB_HINT_CLEAR_TEXT: "Segure DEL para limpar todo o texto"
|
||||||
|
STR_KB_HINT_SECONDARY_CHAR: "Segure SELECT para caractere secundário"
|
||||||
|
STR_KB_HINT_UPPER_SECONDARY: "Segure SELECT para MAIÚSCULO ou caractere secundário"
|
||||||
|
STR_KB_HINT_LOWER_SECONDARY: "Segure SELECT para minúsculo ou caractere secundário"
|
||||||
|
STR_KB_HINT_URL_SNIPPETS: "Pressione URL para atalhos"
|
||||||
|
STR_SD_FIRMWARE_UPDATE: "Atualização de firmware via cartão SD"
|
||||||
|
STR_SELECT_FIRMWARE_FILE: "Selecione o arquivo de firmware (.bin)"
|
||||||
|
STR_NO_BIN_FILES: "Nenhum arquivo .bin encontrado"
|
||||||
|
STR_VALIDATING_FIRMWARE: "Validando firmware..."
|
||||||
|
STR_INVALID_FIRMWARE: "Arquivo de firmware inválido"
|
||||||
|
STR_FIRMWARE_TOO_LARGE: "Firmware grande demais para a partição"
|
||||||
|
STR_FIRMWARE_TOO_SMALL: "Arquivo de firmware pequeno demais"
|
||||||
|
STR_FIRMWARE_UPDATE_PROMPT: "Atualizar firmware?"
|
||||||
|
STR_FIRMWARE_FILE_OPEN_FAILED: "Não foi possível abrir o arquivo"
|
||||||
|
STR_FIRMWARE_WRITE_FAILED: "Falha na gravação do firmware"
|
||||||
|
STR_FIRMWARE_UPDATE_DO_NOT_POWER_OFF: "Não desligue o dispositivo!"
|
||||||
|
STR_RECOVERY_MODE: "Modo de Recuperação"
|
||||||
|
STR_RECOVERY_MODE_HINT: "Coloque o arquivo firmware.bin na raiz do cartão SD e selecione-o"
|
||||||
|
STR_ADD_HIDDEN_NETWORK: "Adicionar rede oculta..."
|
||||||
|
STR_ENTER_WIFI_SSID: "Insira o nome da rede (SSID)"
|
||||||
@@ -0,0 +1,394 @@
|
|||||||
|
_language_name: "Português (Portugal)"
|
||||||
|
_language_code: "P2"
|
||||||
|
_order: "27"
|
||||||
|
|
||||||
|
STR_CROSSPOINT: "CrossPoint"
|
||||||
|
STR_BOOTING: "A INICIAR"
|
||||||
|
STR_SLEEPING: "EM REPOUSO"
|
||||||
|
STR_ENTERING_SLEEP: "A entrar em repouso"
|
||||||
|
STR_BROWSE_FILES: "Explorar ficheiros"
|
||||||
|
STR_FILE_TRANSFER: "Transferência de ficheiros"
|
||||||
|
STR_SETTINGS_TITLE: "Definições"
|
||||||
|
STR_CONTINUE_READING: "Continuar a ler"
|
||||||
|
STR_NO_OPEN_BOOK: "Nenhum livro aberto"
|
||||||
|
STR_START_READING: "Comece a ler abaixo"
|
||||||
|
STR_NO_FILES_FOUND: "Nenhum ficheiro encontrado"
|
||||||
|
STR_SELECT_CHAPTER: "Selecionar capítulo"
|
||||||
|
STR_NO_CHAPTERS: "Sem capítulos"
|
||||||
|
STR_END_OF_BOOK: "Fim do livro"
|
||||||
|
STR_EMPTY_CHAPTER: "Capítulo vazio"
|
||||||
|
STR_INDEXING: "A indexar"
|
||||||
|
STR_INDEX_FAILED: "Falha ao indexar - livro inválido"
|
||||||
|
STR_MEMORY_ERROR: "Erro de memória"
|
||||||
|
STR_PAGE_LOAD_ERROR: "Erro ao carregar a página"
|
||||||
|
STR_EMPTY_FILE: "Ficheiro vazio"
|
||||||
|
STR_OUT_OF_BOUNDS: "Fora dos limites"
|
||||||
|
STR_LOADING: "A carregar..."
|
||||||
|
STR_LOADING_POPUP: "A carregar"
|
||||||
|
STR_WIFI_NETWORKS: "Redes Wi-Fi"
|
||||||
|
STR_NO_NETWORKS: "Nenhuma rede encontrada"
|
||||||
|
STR_NETWORKS_FOUND: "%zu redes encontradas"
|
||||||
|
STR_SCANNING: "A procurar..."
|
||||||
|
STR_FINDING_SAVED_WIFI: "A procurar Wi-Fi guardado..."
|
||||||
|
STR_CONNECTING: "A ligar..."
|
||||||
|
STR_CONNECTING_SAVED_WIFI: "A ligar ao Wi-Fi guardado..."
|
||||||
|
STR_SHOW_NETWORKS: "Mostrar"
|
||||||
|
STR_CONNECTED: "Ligado!"
|
||||||
|
STR_CONNECTION_FAILED: "Falha na ligação"
|
||||||
|
STR_FORGET_NETWORK: "Esquecer rede?"
|
||||||
|
STR_SAVE_PASSWORD: "Guardar palavra-passe para a próxima vez?"
|
||||||
|
STR_PRESS_OK_SCAN: "Prima OK para procurar novamente"
|
||||||
|
STR_JOIN_NETWORK: "Aderir a uma rede"
|
||||||
|
STR_CREATE_HOTSPOT: "Criar ponto de acesso"
|
||||||
|
STR_JOIN_DESC: "Ligue-se a uma rede Wi-Fi existente"
|
||||||
|
STR_HOTSPOT_DESC: "Crie uma rede Wi-Fi para outras pessoas se ligarem"
|
||||||
|
STR_STARTING_HOTSPOT: "A iniciar ponto de acesso..."
|
||||||
|
STR_HOTSPOT_MODE: "Modo de ponto de acesso"
|
||||||
|
STR_CONNECT_WIFI_HINT: "Ligue o seu dispositivo a esta rede Wi-Fi"
|
||||||
|
STR_OPEN_URL_HINT: "Abra este URL no seu navegador"
|
||||||
|
STR_OR_HTTP_PREFIX: "ou http://"
|
||||||
|
STR_SCAN_QR_HINT: "ou leia o código QR com o seu telemóvel:"
|
||||||
|
STR_CALIBRE_WIRELESS: "Calibre sem fios"
|
||||||
|
STR_NETWORK_LEGEND: "* = Encriptada | + = Guardada"
|
||||||
|
STR_MAC_ADDRESS: "Endereço MAC:"
|
||||||
|
STR_CHECKING_WIFI: "A verificar o Wi-Fi..."
|
||||||
|
STR_ENTER_WIFI_PASSWORD: "Introduza a palavra-passe do Wi-Fi"
|
||||||
|
STR_ADD_HIDDEN_NETWORK: "Adicionar rede oculta..."
|
||||||
|
STR_ENTER_WIFI_SSID: "Introduza o nome da rede (SSID)"
|
||||||
|
STR_TO_PREFIX: "para "
|
||||||
|
STR_CALIBRE_RECEIVING: "A receber: "
|
||||||
|
STR_CALIBRE_RECEIVED: "Recebido: "
|
||||||
|
STR_CALIBRE_INSTRUCTION_1: "1) Instale o plugin CrossPoint Reader"
|
||||||
|
STR_CALIBRE_INSTRUCTION_2: "2) Esteja na mesma rede Wi-Fi"
|
||||||
|
STR_CALIBRE_INSTRUCTION_3: "3) No Calibre: \"Enviar para o dispositivo\""
|
||||||
|
STR_CALIBRE_INSTRUCTION_4: "\"Mantenha este ecrã aberto durante o envio\""
|
||||||
|
STR_CAT_DISPLAY: "Ecrã"
|
||||||
|
STR_CAT_READER: "Leitor"
|
||||||
|
STR_CAT_CONTROLS: "Controlos"
|
||||||
|
STR_CAT_SYSTEM: "Sistema"
|
||||||
|
STR_SLEEP_SCREEN: "Ecrã de repouso"
|
||||||
|
STR_QUICK_RESUME_TIMEOUT: "Retoma rápida após tempo limite"
|
||||||
|
STR_SLEEP_COVER_MODE: "Modo de capa no ecrã de repouso"
|
||||||
|
STR_HIDE_BATTERY: "Ocultar % da bateria"
|
||||||
|
STR_EXTRA_SPACING: "Espaçamento extra entre parágrafos"
|
||||||
|
STR_TEXT_AA: "Suavização do texto"
|
||||||
|
STR_IMAGES: "Imagens"
|
||||||
|
STR_IMAGES_DISPLAY: "Exibição"
|
||||||
|
STR_IMAGES_PLACEHOLDER: "Espaço reservado"
|
||||||
|
STR_IMAGES_SUPPRESS: "Suprimir"
|
||||||
|
STR_EOB_HOME: "Início"
|
||||||
|
STR_EOB_CONTINUE_WITH: "Continuar com"
|
||||||
|
STR_SHORT_PWR_BTN: "Clique curto no botão de energia"
|
||||||
|
STR_ORIENTATION: "Orientação de leitura"
|
||||||
|
STR_SIDE_BTN_LAYOUT: "Disposição dos botões laterais (leitor)"
|
||||||
|
STR_FRONT_BTN_FOLLOW_ORIENTATION: "Orientar botões frontais"
|
||||||
|
STR_LONG_PRESS_BEHAVIOR: "Comportamento de pressão longa"
|
||||||
|
STR_LONG_PRESS_BEHAVIOR_OFF: "DESL."
|
||||||
|
STR_LONG_PRESS_BEHAVIOR_SKIP: "Saltar capítulo"
|
||||||
|
STR_LONG_PRESS_BEHAVIOR_ORIENTATION: "Alterar orientação"
|
||||||
|
STR_LONG_PRESS_MENU: "Pressão longa no Menu"
|
||||||
|
STR_FONT_PREVIEW_TEXT: "A rápida raposa castanha salta por cima do cão preguiçoso"
|
||||||
|
STR_FONT_FAMILY: "Tipo de letra do leitor"
|
||||||
|
STR_FONT_SIZE: "Tamanho do tipo de letra"
|
||||||
|
STR_LINE_SPACING: "Espaçamento entre linhas"
|
||||||
|
STR_SCREEN_MARGIN: "Margens do ecrã"
|
||||||
|
STR_PARA_ALIGNMENT: "Alinhamento dos parágrafos"
|
||||||
|
STR_HYPHENATION: "Hifenização"
|
||||||
|
STR_TIME_TO_SLEEP: "Tempo até repousar"
|
||||||
|
STR_SHOW_HIDDEN_FILES: "Mostrar ficheiros ocultos"
|
||||||
|
STR_REMOVE_READ_FROM_RECENTS: "Limpar livros lidos da lista de recentes"
|
||||||
|
STR_MOVE_FINISHED_TO_READ: "Mover livros concluídos para a pasta Read"
|
||||||
|
STR_REFRESH_FREQ: "Frequência de atualização"
|
||||||
|
STR_KOREADER_SYNC: "Sincronização KOReader"
|
||||||
|
STR_CHECK_UPDATES: "Procurar atualizações"
|
||||||
|
STR_LANGUAGE: "Idioma"
|
||||||
|
STR_CLEAR_READING_CACHE: "Limpar cache de leitura"
|
||||||
|
STR_USERNAME: "Nome de utilizador"
|
||||||
|
STR_PASSWORD: "Palavra-passe"
|
||||||
|
STR_SYNC_SERVER_URL: "URL do servidor de sincronização"
|
||||||
|
STR_DOCUMENT_MATCHING: "Correspondência de documentos"
|
||||||
|
STR_SEND_METADATA: "Enviar metadados do documento"
|
||||||
|
STR_AUTHENTICATE: "Autenticar"
|
||||||
|
STR_KOREADER_USERNAME: "Utilizador do KOReader"
|
||||||
|
STR_KOREADER_PASSWORD: "Palavra-passe do KOReader"
|
||||||
|
STR_FILENAME: "Nome do ficheiro"
|
||||||
|
STR_BINARY: "Binário"
|
||||||
|
STR_SET_CREDENTIALS_FIRST: "Defina as credenciais primeiro"
|
||||||
|
STR_WIFI_CONN_FAILED: "Falha na ligação Wi-Fi"
|
||||||
|
STR_AUTHENTICATING: "A autenticar..."
|
||||||
|
STR_AUTH_SUCCESS: "Autenticado com sucesso!"
|
||||||
|
STR_KOREADER_AUTH: "Autenticação KOReader"
|
||||||
|
STR_SYNC_READY: "A sincronização KOReader está pronta a usar"
|
||||||
|
STR_AUTH_FAILED: "Falha na autenticação"
|
||||||
|
STR_DONE: "Concluído"
|
||||||
|
STR_CLEAR_CACHE_WARNING_1: "Isto irá limpar todos os dados de livros em cache."
|
||||||
|
STR_CLEAR_CACHE_WARNING_2: "Todo o progresso de leitura será perdido!"
|
||||||
|
STR_CLEAR_CACHE_WARNING_3: "Os livros terão de ser reindexados"
|
||||||
|
STR_CLEAR_CACHE_WARNING_4: "quando forem abertos novamente."
|
||||||
|
STR_CLEARING_CACHE: "A limpar a cache..."
|
||||||
|
STR_CACHE_CLEARED: "Cache limpa"
|
||||||
|
STR_ITEMS_REMOVED: "itens removidos"
|
||||||
|
STR_FAILED_LOWER: "falhou"
|
||||||
|
STR_CLEAR_CACHE_FAILED: "Falha ao limpar a cache"
|
||||||
|
STR_CHECK_SERIAL_OUTPUT: "Verifique a saída de série para obter detalhes"
|
||||||
|
STR_DARK: "Escuro"
|
||||||
|
STR_LIGHT: "Claro"
|
||||||
|
STR_CUSTOM: "Personalizado"
|
||||||
|
STR_COVER: "Capa"
|
||||||
|
STR_NONE_OPT: "Nenhum"
|
||||||
|
STR_FIT: "Ajustar"
|
||||||
|
STR_CROP: "Recortar"
|
||||||
|
STR_NEVER: "Nunca"
|
||||||
|
STR_IN_READER: "No leitor"
|
||||||
|
STR_ALWAYS: "Sempre"
|
||||||
|
STR_IGNORE: "Ignorar"
|
||||||
|
STR_SLEEP: "Repouso"
|
||||||
|
STR_PAGE_TURN: "Virar página"
|
||||||
|
STR_FORCE_REFRESH: "Atualizar ecrã"
|
||||||
|
STR_PORTRAIT: "Retrato"
|
||||||
|
STR_LANDSCAPE_CW: "Paisagem (Dir.)"
|
||||||
|
STR_INVERTED: "Invertido"
|
||||||
|
STR_ORIENTATION_INVERTED: "Retrato 180°"
|
||||||
|
STR_LANDSCAPE_CCW: "Paisagem (Esq.)"
|
||||||
|
STR_PREV_NEXT: "Ant/Seg"
|
||||||
|
STR_NEXT_PREV: "Seg/Ant"
|
||||||
|
STR_KOSYNC: "KOSync"
|
||||||
|
STR_BOOKMARK_OPTION: "Marcador"
|
||||||
|
STR_DISABLED: "Desativado"
|
||||||
|
STR_NOTO_SERIF: "Noto Serif"
|
||||||
|
STR_NOTO_SANS: "Noto Sans"
|
||||||
|
STR_SMALL: "Pequeno"
|
||||||
|
STR_MEDIUM: "Médio"
|
||||||
|
STR_LARGE: "Grande"
|
||||||
|
STR_X_LARGE: "Extra grande"
|
||||||
|
STR_TIGHT: "Apertado"
|
||||||
|
STR_NORMAL: "Normal"
|
||||||
|
STR_WIDE: "Largo"
|
||||||
|
STR_JUSTIFY: "Justificar"
|
||||||
|
STR_ALIGN_LEFT: "Esquerda"
|
||||||
|
STR_CENTER: "Centrar"
|
||||||
|
STR_ALIGN_RIGHT: "Direita"
|
||||||
|
STR_PAGES_1: "1 página"
|
||||||
|
STR_PAGES_5: "5 páginas"
|
||||||
|
STR_PAGES_10: "10 páginas"
|
||||||
|
STR_PAGES_15: "15 páginas"
|
||||||
|
STR_PAGES_30: "30 páginas"
|
||||||
|
STR_UPDATE: "Atualizar"
|
||||||
|
STR_CHECKING_UPDATE: "A procurar atualização..."
|
||||||
|
STR_NEW_UPDATE: "Nova atualização disponível!"
|
||||||
|
STR_CURRENT_VERSION: "Versão atual: "
|
||||||
|
STR_NEW_VERSION: "Nova versão: "
|
||||||
|
STR_UPDATING: "A atualizar..."
|
||||||
|
STR_NO_UPDATE: "Nenhuma atualização disponível"
|
||||||
|
STR_UPDATE_FAILED: "Falha na atualização"
|
||||||
|
STR_UPDATE_COMPLETE: "Atualização concluída"
|
||||||
|
STR_POWER_ON_HINT: "Prima sem soltar o botão de energia para voltar a ligar"
|
||||||
|
STR_RESTARTING_HINT: "A reiniciar... Se o dispositivo não reiniciar, mantenha premido o botão de energia durante alguns segundos."
|
||||||
|
STR_NO_ENTRIES: "Nenhum registo encontrado"
|
||||||
|
STR_DOWNLOADING: "A transferir..."
|
||||||
|
STR_DOWNLOAD_FAILED: "Falha na transferência"
|
||||||
|
STR_ERROR_MSG: "Erro:"
|
||||||
|
STR_UNNAMED: "Sem nome"
|
||||||
|
STR_HOLD_OPEN_TO_DELETE: "Mantenha premido Abrir para eliminar"
|
||||||
|
STR_NO_SERVER_URL: "Nenhum URL de servidor configurado"
|
||||||
|
STR_FETCH_FEED_FAILED: "Falha ao obter o feed"
|
||||||
|
STR_PARSE_FEED_FAILED: "Falha ao analisar o feed"
|
||||||
|
STR_NEXT_PAGE: "Página seguinte »"
|
||||||
|
STR_PREV_PAGE: "« Página anterior"
|
||||||
|
STR_NETWORK_PREFIX: "Rede: "
|
||||||
|
STR_IP_ADDRESS_PREFIX: "Endereço IP: "
|
||||||
|
STR_ERROR_GENERAL_FAILURE: "Erro: falha geral"
|
||||||
|
STR_ERROR_NETWORK_NOT_FOUND: "Erro: rede não encontrada"
|
||||||
|
STR_ERROR_CONNECTION_TIMEOUT: "Erro: tempo limite de ligação esgotado"
|
||||||
|
STR_SD_CARD: "Cartão SD"
|
||||||
|
STR_BACK: "« Voltar"
|
||||||
|
STR_EXIT: "« Sair"
|
||||||
|
STR_HOME: "« Início"
|
||||||
|
STR_SELECT: "Selecionar"
|
||||||
|
STR_SELECTED: "Selecionado"
|
||||||
|
STR_TOGGLE: "Alternar"
|
||||||
|
STR_TOGGLE_BOOKMARK: "Alternar marcador"
|
||||||
|
STR_CONFIRM: "Confirmar"
|
||||||
|
STR_CANCEL: "Cancelar"
|
||||||
|
STR_CONNECT: "Ligar"
|
||||||
|
STR_OPEN: "Abrir"
|
||||||
|
STR_DOWNLOAD: "Transferir"
|
||||||
|
STR_RETRY: "Tentar novamente"
|
||||||
|
STR_YES: "Sim"
|
||||||
|
STR_NO: "Não"
|
||||||
|
STR_SHOW: "Mostrar"
|
||||||
|
STR_HIDE: "Ocultar"
|
||||||
|
STR_STATE_ON: "LIG."
|
||||||
|
STR_STATE_OFF: "DESL."
|
||||||
|
STR_NOT_SET: "Não definido"
|
||||||
|
STR_DIR_LEFT: "Esquerda"
|
||||||
|
STR_DIR_RIGHT: "Direita"
|
||||||
|
STR_DIR_UP: "Cima"
|
||||||
|
STR_DIR_DOWN: "Baixo"
|
||||||
|
STR_OK_BUTTON: "OK"
|
||||||
|
STR_SLEEP_COVER_FILTER: "Filtro da capa de ecrã de repouso"
|
||||||
|
STR_FILTER_CONTRAST: "Contraste"
|
||||||
|
STR_CUSTOMISE_STATUS_BAR: "Personalizar barra de estado"
|
||||||
|
STR_CHAPTER_PAGE_COUNT: "Contagem de páginas do capítulo"
|
||||||
|
STR_BOOK_PROGRESS_PERCENTAGE: "Percentagem de progresso do livro"
|
||||||
|
STR_PROGRESS_BAR: "Barra de progresso"
|
||||||
|
STR_PROGRESS_BAR_THICKNESS: "Espessura da barra de progresso"
|
||||||
|
STR_PROGRESS_BAR_THIN: "Fina"
|
||||||
|
STR_PROGRESS_BAR_MEDIUM: "Média"
|
||||||
|
STR_PROGRESS_BAR_THICK: "Grossa"
|
||||||
|
STR_BOOK: "Livro"
|
||||||
|
STR_CHAPTER: "Capítulo"
|
||||||
|
STR_EXAMPLE_CHAPTER: "Capítulo 21"
|
||||||
|
STR_EXAMPLE_BOOK: "Título do livro"
|
||||||
|
STR_PREVIEW: "Pré-visualização"
|
||||||
|
STR_TITLE: "Título"
|
||||||
|
STR_BATTERY: "Bateria"
|
||||||
|
STR_XTC_STATUS_BAR: "Barra de estado XTC"
|
||||||
|
STR_BOTTOM: "Inferior"
|
||||||
|
STR_TOP: "Superior"
|
||||||
|
STR_CLOCK: "Relógio"
|
||||||
|
STR_CLOCK_UTC_OFFSET: "Deslocamento UTC do relógio"
|
||||||
|
STR_CLOCK_FORMAT: "Formato do relógio"
|
||||||
|
STR_CLOCK_FORMAT_24H: "24 horas"
|
||||||
|
STR_CLOCK_FORMAT_12H: "12 horas"
|
||||||
|
STR_CURRENT_TIME: "Hora atual:"
|
||||||
|
STR_NEXT_FIELD: "Seguinte"
|
||||||
|
STR_CLOCK_SYNC: "Sincronizar relógio"
|
||||||
|
STR_CLOCK_SYNC_NOW: "Sincronizar relógio agora"
|
||||||
|
STR_CLOCK_SYNCING: "A sincronizar via NTP..."
|
||||||
|
STR_CLOCK_SYNC_OK: "Relógio sincronizado"
|
||||||
|
STR_CLOCK_SYNC_FAIL: "Falha na sincronização"
|
||||||
|
STR_CLOCK_SYNC_NO_WIFI: "Wi-Fi não ligado"
|
||||||
|
STR_CLOCK_SYNC_NO_WIFI_HINT: "Ligue-se primeiro ao Wi-Fi e, em seguida, tente novamente."
|
||||||
|
STR_CLOCK_SYNCED: "Relógio sincronizado"
|
||||||
|
STR_UI_THEME: "Tema da interface"
|
||||||
|
STR_THEME_CLASSIC: "Clássico"
|
||||||
|
STR_THEME_LYRA: "Lyra"
|
||||||
|
STR_THEME_ROUNDEDRAFF: "RoundedRaff"
|
||||||
|
STR_THEME_LYRA_EXTENDED: "Lyra Extended"
|
||||||
|
STR_SUNLIGHT_FADING_FIX: "Ajuste de desbotamento ao sol"
|
||||||
|
STR_REMAP_FRONT_BUTTONS: "Remapear botões frontais"
|
||||||
|
STR_BOOKMARKS: "Marcadores"
|
||||||
|
STR_BOOKMARK_ADDED: "Marcador adicionado."
|
||||||
|
STR_BOOKMARK_REMOVED: "Marcador removido."
|
||||||
|
STR_OPDS_BROWSER: "Navegador OPDS"
|
||||||
|
STR_SEARCH: "Pesquisar"
|
||||||
|
STR_COVER_CUSTOM: "Capa + Personalizado"
|
||||||
|
STR_QUICK_RESUME: "Retoma rápida"
|
||||||
|
STR_MENU_RECENT_BOOKS: "Livros recentes"
|
||||||
|
STR_REMOVE_FROM_RECENTS: "Remover dos Livros recentes?"
|
||||||
|
STR_NO_RECENT_BOOKS: "Nenhum livro recente"
|
||||||
|
STR_CALIBRE_DESC: "Usar transferências sem fios de dispositivos do Calibre"
|
||||||
|
STR_FORGET_AND_REMOVE: "Esquecer rede e remover palavra-passe guardada?"
|
||||||
|
STR_FORGET_BUTTON: "Esquecer"
|
||||||
|
STR_CALIBRE_STARTING: "A iniciar o Calibre..."
|
||||||
|
STR_CALIBRE_SETUP: "Configuração"
|
||||||
|
STR_CALIBRE_STATUS: "Estado"
|
||||||
|
STR_CLEAR_BUTTON: "Limpar"
|
||||||
|
STR_DEFAULT_VALUE: "Predefinição"
|
||||||
|
STR_REMAP_PROMPT: "Prima um botão frontal para cada função"
|
||||||
|
STR_UNASSIGNED: "Não atribuído"
|
||||||
|
STR_ALREADY_ASSIGNED: "Já atribuído"
|
||||||
|
STR_REMAP_RESET_HINT: "Botão lateral Cima: Repor disposição predefinida"
|
||||||
|
STR_REMAP_CANCEL_HINT: "Botão lateral Baixo: Cancelar remapeamento"
|
||||||
|
STR_HW_BACK_LABEL: "Voltar (1º botão)"
|
||||||
|
STR_HW_CONFIRM_LABEL: "Confirmar (2º botão)"
|
||||||
|
STR_HW_LEFT_LABEL: "Esquerda (3º botão)"
|
||||||
|
STR_HW_RIGHT_LABEL: "Direita (4º botão)"
|
||||||
|
STR_GO_TO_PERCENT: "Ir para %"
|
||||||
|
STR_GO_HOME_BUTTON: "Ir para o Início"
|
||||||
|
STR_SYNC_PROGRESS: "Sincronizar progresso"
|
||||||
|
STR_DELETE_CACHE: "Eliminar cache do livro"
|
||||||
|
STR_DELETE: "Eliminar"
|
||||||
|
STR_CONFIRM_DELETE_BOOKMARK: "Eliminar este marcador?"
|
||||||
|
STR_DISPLAY_QR: "Mostrar página como QR"
|
||||||
|
STR_CHAPTER_PREFIX: "Capítulo: "
|
||||||
|
STR_PAGES_SEPARATOR: " páginas | "
|
||||||
|
STR_BOOK_PREFIX: "Livro: "
|
||||||
|
STR_CALIBRE_URL_HINT: "Para o Calibre, adicione /opds ao seu URL"
|
||||||
|
STR_SYNCING_TIME: "A sincronizar a hora..."
|
||||||
|
STR_CALC_HASH: "A calcular o hash do documento..."
|
||||||
|
STR_HASH_FAILED: "Falha ao calcular o hash do documento"
|
||||||
|
STR_FETCH_PROGRESS: "A procurar progresso remoto..."
|
||||||
|
STR_UPLOAD_PROGRESS: "A enviar progresso..."
|
||||||
|
STR_NO_CREDENTIALS_MSG: "Nenhuma credencial configurada"
|
||||||
|
STR_KOREADER_SETUP_HINT: "Configure a conta do KOReader nas Definições"
|
||||||
|
STR_PROGRESS_FOUND: "Progresso encontrado!"
|
||||||
|
STR_REMOTE_LABEL: "Remoto:"
|
||||||
|
STR_LOCAL_LABEL: "Local:"
|
||||||
|
STR_PAGE_OVERALL_FORMAT: "Página %d, %.2f%% total"
|
||||||
|
STR_PAGE_TOTAL_OVERALL_FORMAT: "Página %d/%d, %.2f%% total"
|
||||||
|
STR_DEVICE_FROM_FORMAT: " De: %s"
|
||||||
|
STR_APPLY_REMOTE: "Aplicar progresso remoto"
|
||||||
|
STR_UPLOAD_LOCAL: "Enviar progresso local"
|
||||||
|
STR_NO_REMOTE_MSG: "Nenhum progresso remoto encontrado"
|
||||||
|
STR_UPLOAD_PROMPT: "Enviar posição atual?"
|
||||||
|
STR_UPLOAD_SUCCESS: "Progresso enviado!"
|
||||||
|
STR_SYNC_FAILED_MSG: "Falha na sincronização"
|
||||||
|
STR_SAVE_PROGRESS_FAILED: "Não foi possível guardar o progresso"
|
||||||
|
STR_SECTION_PREFIX: "Secção "
|
||||||
|
STR_UPLOAD: "Enviar"
|
||||||
|
STR_BOOK_S_STYLE: "Estilo do livro"
|
||||||
|
STR_EMBEDDED_STYLE: "Estilo embutido"
|
||||||
|
STR_FOCUS_READING: "Leitura focada"
|
||||||
|
STR_OPDS_SERVER_URL: "URL do servidor OPDS"
|
||||||
|
STR_PWR_BTN_FOOTNOTE_BACK: "Retorno rápido das notas de rodapé"
|
||||||
|
STR_SET_SLEEP_COVER: "Definir capa"
|
||||||
|
STR_FOOTNOTES: "Notas de rodapé"
|
||||||
|
STR_NO_FOOTNOTES: "Nenhuma nota de rodapé nesta página"
|
||||||
|
STR_LINK: "[ligação]"
|
||||||
|
STR_SCREENSHOT_BUTTON: "Captura de ecrã"
|
||||||
|
STR_SLEEP_TIMER_VALUE_FORMAT: "%u min"
|
||||||
|
STR_SLEEP_NEVER: "Nunca"
|
||||||
|
STR_STEP_HINT_FRONT: "Botões frontais:"
|
||||||
|
STR_STEP_HINT_SIDE: "Botões laterais:"
|
||||||
|
STR_ADD_SERVER: "Adicionar servidor"
|
||||||
|
STR_SERVER_NAME: "Nome do servidor"
|
||||||
|
STR_NO_SERVERS: "Nenhum servidor OPDS configurado"
|
||||||
|
STR_DELETE_SERVER: "Eliminar servidor"
|
||||||
|
STR_OPDS_SERVERS: "Servidores OPDS"
|
||||||
|
STR_AUTO_TURN_ENABLED: "Virar página automático ativado: "
|
||||||
|
STR_AUTO_TURN_PAGES_PER_MIN: "Virar página automático (Páginas por minuto)"
|
||||||
|
STR_MANAGE_FONTS: "Gerir tipos de letra"
|
||||||
|
STR_FONT_BROWSER: "Navegador de tipos de letra"
|
||||||
|
STR_LOADING_FONT_LIST: "A carregar a lista de tipos de letra..."
|
||||||
|
STR_NO_FONTS_AVAILABLE: "Nenhum tipo de letra disponível"
|
||||||
|
STR_FONT_INSTALLED: "Tipo de letra instalado!"
|
||||||
|
STR_FONT_INSTALL_FAILED: "Falha na instalação do tipo de letra"
|
||||||
|
STR_INSTALLED: "Instalado"
|
||||||
|
STR_DOWNLOAD_ALL: "Transferir tudo"
|
||||||
|
STR_UPDATE_ALL: "Atualizar tudo"
|
||||||
|
STR_UPDATE_AVAILABLE: "Atualizar"
|
||||||
|
STR_CRASH_TITLE: "Falha do sistema"
|
||||||
|
STR_CRASH_DESCRIPTION: "Um relatório detalhado foi guardado em crash_report.txt. Por favor, inclua este ficheiro no seu relatório de erro."
|
||||||
|
STR_CRASH_REASON: "Motivo da falha:"
|
||||||
|
STR_CRASH_NO_REASON: "(Nenhum motivo foi registado)"
|
||||||
|
STR_TILT_PAGE_TURN: "Virar página por inclinação"
|
||||||
|
STR_KB_HINT_MOVE_CURSOR: "Prima ESQUERDA ou DIREITA para mover o cursor"
|
||||||
|
STR_KB_HINT_RETURN_CURSOR: "Prima ESQUERDA para regressar à posição do cursor"
|
||||||
|
STR_KB_HINT_HIDE_PASSWORD: "Mantenha premido DIREITA e depois prima [***] para ocultar a palavra-passe"
|
||||||
|
STR_KB_HINT_SHOW_PASSWORD: "Mantenha premido DIREITA e depois prima [abc] para mostrar a palavra-passe"
|
||||||
|
STR_KB_HINT_TOGGLE_HIDE_PASSWORD: "Prima [***] para ocultar a palavra-passe"
|
||||||
|
STR_KB_HINT_TOGGLE_SHOW_PASSWORD: "Prima [abc] para mostrar a palavra-passe"
|
||||||
|
STR_KB_HINT_EDIT_ENTRY: "Mantenha premido CIMA para editar a entrada"
|
||||||
|
STR_KB_TIPS: "Dicas:"
|
||||||
|
STR_KB_HINT_RETURN_KEYBOARD: "Prima BAIXO para regressar ao teclado"
|
||||||
|
STR_KB_HINT_EXIT_URL_MODE: "Prima ABC para sair do modo URL"
|
||||||
|
STR_KB_HINT_CLEAR_TEXT: "Mantenha premido DEL para limpar todo o texto"
|
||||||
|
STR_KB_HINT_SECONDARY_CHAR: "Mantenha premido SELECT para o carácter secundário"
|
||||||
|
STR_KB_HINT_UPPER_SECONDARY: "Mantenha premido SELECT para MAIÚSCULAS ou carácter secundário"
|
||||||
|
STR_KB_HINT_LOWER_SECONDARY: "Mantenha premido SELECT para minúsculas ou carácter secundário"
|
||||||
|
STR_KB_HINT_URL_SNIPPETS: "Prima URL para atalhos"
|
||||||
|
STR_SD_FIRMWARE_UPDATE: "Atualização de firmware via cartão SD"
|
||||||
|
STR_SELECT_FIRMWARE_FILE: "Selecione o ficheiro de firmware (.bin)"
|
||||||
|
STR_NO_BIN_FILES: "Nenhum ficheiro .bin encontrado"
|
||||||
|
STR_VALIDATING_FIRMWARE: "A validar o firmware..."
|
||||||
|
STR_INVALID_FIRMWARE: "Ficheiro de firmware inválido"
|
||||||
|
STR_FIRMWARE_TOO_LARGE: "Firmware demasiado grande para a partição"
|
||||||
|
STR_FIRMWARE_TOO_SMALL: "Ficheiro de firmware demasiado pequeno"
|
||||||
|
STR_FIRMWARE_UPDATE_PROMPT: "Atualizar firmware?"
|
||||||
|
STR_FIRMWARE_FILE_OPEN_FAILED: "Não foi possível abrir o ficheiro"
|
||||||
|
STR_FIRMWARE_WRITE_FAILED: "Falha na gravação do firmware"
|
||||||
|
STR_FIRMWARE_UPDATE_DO_NOT_POWER_OFF: "Não desligue o dispositivo!"
|
||||||
|
STR_RECOVERY_MODE: "Modo de Recuperação"
|
||||||
|
STR_RECOVERY_MODE_HINT: "Coloque o ficheiro firmware.bin na raiz do cartão SD e selecione-o"
|
||||||
@@ -97,6 +97,7 @@ STR_USERNAME: "Utilizator"
|
|||||||
STR_PASSWORD: "Parolă"
|
STR_PASSWORD: "Parolă"
|
||||||
STR_SYNC_SERVER_URL: "URL server sincronizare"
|
STR_SYNC_SERVER_URL: "URL server sincronizare"
|
||||||
STR_DOCUMENT_MATCHING: "Corespondenţă document"
|
STR_DOCUMENT_MATCHING: "Corespondenţă document"
|
||||||
|
STR_SEND_METADATA: "Trimite metadate document"
|
||||||
STR_AUTHENTICATE: "Autentificare"
|
STR_AUTHENTICATE: "Autentificare"
|
||||||
STR_KOREADER_USERNAME: "Nume utilizator KOReader"
|
STR_KOREADER_USERNAME: "Nume utilizator KOReader"
|
||||||
STR_KOREADER_PASSWORD: "Parolă KOReader"
|
STR_KOREADER_PASSWORD: "Parolă KOReader"
|
||||||
@@ -266,7 +267,6 @@ STR_CHAPTER_PREFIX: "Capitol: "
|
|||||||
STR_PAGES_SEPARATOR: " pagini | "
|
STR_PAGES_SEPARATOR: " pagini | "
|
||||||
STR_BOOK_PREFIX: "Carte: "
|
STR_BOOK_PREFIX: "Carte: "
|
||||||
STR_CALIBRE_URL_HINT: "Pentru Calibre, adăugaţi /opds la URL"
|
STR_CALIBRE_URL_HINT: "Pentru Calibre, adăugaţi /opds la URL"
|
||||||
STR_PERCENT_STEP_HINT: "Stânga/Dreapta: 1% Sus/Jos: 10%"
|
|
||||||
STR_SYNCING_TIME: "Timp de sincronizare..."
|
STR_SYNCING_TIME: "Timp de sincronizare..."
|
||||||
STR_CALC_HASH: "Calcularea hash-ului documentului..."
|
STR_CALC_HASH: "Calcularea hash-ului documentului..."
|
||||||
STR_HASH_FAILED: "Eşec la calcularea hash-ului documentului"
|
STR_HASH_FAILED: "Eşec la calcularea hash-ului documentului"
|
||||||
@@ -298,8 +298,11 @@ STR_NO_FOOTNOTES: "Nicio notă de subsol"
|
|||||||
STR_LINK: "[link]"
|
STR_LINK: "[link]"
|
||||||
STR_SLEEP_TIMER_VALUE_FORMAT: "%u min"
|
STR_SLEEP_TIMER_VALUE_FORMAT: "%u min"
|
||||||
STR_SLEEP_NEVER: "Niciodată"
|
STR_SLEEP_NEVER: "Niciodată"
|
||||||
STR_SLEEP_TIMER_STEP_HINT: "Stânga/Dreapta: 1 min Sus/Jos: 5 min"
|
STR_STEP_HINT_FRONT: "Butoane frontale:"
|
||||||
|
STR_STEP_HINT_SIDE: "Butoane laterale:"
|
||||||
STR_SCREENSHOT_BUTTON: "Captură ecran"
|
STR_SCREENSHOT_BUTTON: "Captură ecran"
|
||||||
STR_AUTO_TURN_ENABLED: "Răsfoire automată: "
|
STR_AUTO_TURN_ENABLED: "Răsfoire automată: "
|
||||||
STR_AUTO_TURN_PAGES_PER_MIN: "Pagini pe minut"
|
STR_AUTO_TURN_PAGES_PER_MIN: "Pagini pe minut"
|
||||||
STR_TILT_PAGE_TURN: "Întoarcere pagină prin înclinare"
|
STR_TILT_PAGE_TURN: "Întoarcere pagină prin înclinare"
|
||||||
|
STR_ADD_HIDDEN_NETWORK: "Adaugă rețea ascunsă..."
|
||||||
|
STR_ENTER_WIFI_SSID: "Introduceți numele rețelei (SSID)"
|
||||||
|
|||||||
@@ -99,6 +99,7 @@ STR_USERNAME: "Имя пользователя"
|
|||||||
STR_PASSWORD: "Пароль"
|
STR_PASSWORD: "Пароль"
|
||||||
STR_SYNC_SERVER_URL: "URL сервера синхронизации"
|
STR_SYNC_SERVER_URL: "URL сервера синхронизации"
|
||||||
STR_DOCUMENT_MATCHING: "Сопоставление документов"
|
STR_DOCUMENT_MATCHING: "Сопоставление документов"
|
||||||
|
STR_SEND_METADATA: "Отправлять метаданные"
|
||||||
STR_AUTHENTICATE: "Авторизация"
|
STR_AUTHENTICATE: "Авторизация"
|
||||||
STR_KOREADER_USERNAME: "Имя пользователя KOReader"
|
STR_KOREADER_USERNAME: "Имя пользователя KOReader"
|
||||||
STR_KOREADER_PASSWORD: "Пароль KOReader"
|
STR_KOREADER_PASSWORD: "Пароль KOReader"
|
||||||
@@ -298,7 +299,6 @@ STR_CHAPTER_PREFIX: "Глава: "
|
|||||||
STR_PAGES_SEPARATOR: " стр. | "
|
STR_PAGES_SEPARATOR: " стр. | "
|
||||||
STR_BOOK_PREFIX: "Книга: "
|
STR_BOOK_PREFIX: "Книга: "
|
||||||
STR_CALIBRE_URL_HINT: "Для Calibre добавьте /opds к URL"
|
STR_CALIBRE_URL_HINT: "Для Calibre добавьте /opds к URL"
|
||||||
STR_PERCENT_STEP_HINT: "Влево/Вправо: 1% Вверх/Вниз: 10%"
|
|
||||||
STR_SYNCING_TIME: "Синхронизация времени..."
|
STR_SYNCING_TIME: "Синхронизация времени..."
|
||||||
STR_CALC_HASH: "Расчёт хэша документа..."
|
STR_CALC_HASH: "Расчёт хэша документа..."
|
||||||
STR_HASH_FAILED: "Не удалось вычислить хэш документа"
|
STR_HASH_FAILED: "Не удалось вычислить хэш документа"
|
||||||
@@ -332,7 +332,8 @@ STR_LINK: "[ссылка]"
|
|||||||
STR_SCREENSHOT_BUTTON: "Сделать снимок экрана"
|
STR_SCREENSHOT_BUTTON: "Сделать снимок экрана"
|
||||||
STR_SLEEP_TIMER_VALUE_FORMAT: "%u мин"
|
STR_SLEEP_TIMER_VALUE_FORMAT: "%u мин"
|
||||||
STR_SLEEP_NEVER: "Никогда"
|
STR_SLEEP_NEVER: "Никогда"
|
||||||
STR_SLEEP_TIMER_STEP_HINT: "Влево/Вправо: 1 мин Вверх/Вниз: 5 мин"
|
STR_STEP_HINT_FRONT: "Передние кнопки:"
|
||||||
|
STR_STEP_HINT_SIDE: "Боковые кнопки:"
|
||||||
STR_ADD_SERVER: "Добавить сервер"
|
STR_ADD_SERVER: "Добавить сервер"
|
||||||
STR_SERVER_NAME: "Имя сервера"
|
STR_SERVER_NAME: "Имя сервера"
|
||||||
STR_NO_SERVERS: "Нет настроенных серверов OPDS"
|
STR_NO_SERVERS: "Нет настроенных серверов OPDS"
|
||||||
@@ -383,3 +384,5 @@ STR_FIRMWARE_WRITE_FAILED: "Ошибка записи прошивки"
|
|||||||
STR_FIRMWARE_UPDATE_DO_NOT_POWER_OFF: "Не выключайте питание!"
|
STR_FIRMWARE_UPDATE_DO_NOT_POWER_OFF: "Не выключайте питание!"
|
||||||
STR_RECOVERY_MODE: "Режим восстановления"
|
STR_RECOVERY_MODE: "Режим восстановления"
|
||||||
STR_RECOVERY_MODE_HINT: "Поместите firmware.bin в корень SD-карты и выберите его"
|
STR_RECOVERY_MODE_HINT: "Поместите firmware.bin в корень SD-карты и выберите его"
|
||||||
|
STR_ADD_HIDDEN_NETWORK: "Добавить скрытую сеть..."
|
||||||
|
STR_ENTER_WIFI_SSID: "Введите имя сети (SSID)"
|
||||||
|
|||||||
@@ -97,7 +97,8 @@ STR_CLEAR_READING_CACHE: "Vymazať vyrovnávaciu pamäť čítania"
|
|||||||
STR_USERNAME: "Používateľské meno"
|
STR_USERNAME: "Používateľské meno"
|
||||||
STR_PASSWORD: "Heslo"
|
STR_PASSWORD: "Heslo"
|
||||||
STR_SYNC_SERVER_URL: "URL synchronizačného servera"
|
STR_SYNC_SERVER_URL: "URL synchronizačného servera"
|
||||||
STR_DOCUMENT_MATCHING: "Párovanie dokumentov"
|
STR_DOCUMENT_MATCHING: "Párovanie dokumentov"
|
||||||
|
STR_SEND_METADATA: "Odosielať metadáta dokumentu"
|
||||||
STR_AUTHENTICATE: "Overiť"
|
STR_AUTHENTICATE: "Overiť"
|
||||||
STR_KOREADER_USERNAME: "Používateľské meno KOReader"
|
STR_KOREADER_USERNAME: "Používateľské meno KOReader"
|
||||||
STR_KOREADER_PASSWORD: "Heslo KOReader"
|
STR_KOREADER_PASSWORD: "Heslo KOReader"
|
||||||
@@ -294,7 +295,6 @@ STR_CHAPTER_PREFIX: "Kapitola:"
|
|||||||
STR_PAGES_SEPARATOR: "strán |"
|
STR_PAGES_SEPARATOR: "strán |"
|
||||||
STR_BOOK_PREFIX: "Kniha:"
|
STR_BOOK_PREFIX: "Kniha:"
|
||||||
STR_CALIBRE_URL_HINT: "Pre Calibre pridajte /opds na koniec URL adresy"
|
STR_CALIBRE_URL_HINT: "Pre Calibre pridajte /opds na koniec URL adresy"
|
||||||
STR_PERCENT_STEP_HINT: "Vľavo/Vpravo: 1 % Hore/Dole: 10 %"
|
|
||||||
STR_SYNCING_TIME: "Čas synchronizácie..."
|
STR_SYNCING_TIME: "Čas synchronizácie..."
|
||||||
STR_CALC_HASH: "Výpočet hashu dokumentu..."
|
STR_CALC_HASH: "Výpočet hashu dokumentu..."
|
||||||
STR_HASH_FAILED: "Nepodarilo sa vypočítať hash dokumentu"
|
STR_HASH_FAILED: "Nepodarilo sa vypočítať hash dokumentu"
|
||||||
@@ -328,7 +328,8 @@ STR_LINK: "[link]"
|
|||||||
STR_SCREENSHOT_BUTTON: "Urobiť snímku obrazovky"
|
STR_SCREENSHOT_BUTTON: "Urobiť snímku obrazovky"
|
||||||
STR_SLEEP_TIMER_VALUE_FORMAT: "%u min"
|
STR_SLEEP_TIMER_VALUE_FORMAT: "%u min"
|
||||||
STR_SLEEP_NEVER: "Nikdy"
|
STR_SLEEP_NEVER: "Nikdy"
|
||||||
STR_SLEEP_TIMER_STEP_HINT: "Vľavo/Vpravo: 1 min Hore/Dole: 5 min"
|
STR_STEP_HINT_FRONT: "Predné tlačidlá:"
|
||||||
|
STR_STEP_HINT_SIDE: "Bočné tlačidlá:"
|
||||||
STR_ADD_SERVER: "Pridať server"
|
STR_ADD_SERVER: "Pridať server"
|
||||||
STR_SERVER_NAME: "Názov servera"
|
STR_SERVER_NAME: "Názov servera"
|
||||||
STR_NO_SERVERS: "Nie sú nakonfigurované žiadne OPDS servery"
|
STR_NO_SERVERS: "Nie sú nakonfigurované žiadne OPDS servery"
|
||||||
@@ -379,3 +380,5 @@ STR_FIRMWARE_WRITE_FAILED: "Zápis firmvéru zlyhal"
|
|||||||
STR_FIRMWARE_UPDATE_DO_NOT_POWER_OFF: "Nevypínajte zariadenie!"
|
STR_FIRMWARE_UPDATE_DO_NOT_POWER_OFF: "Nevypínajte zariadenie!"
|
||||||
STR_RECOVERY_MODE: "Režim obnovenia"
|
STR_RECOVERY_MODE: "Režim obnovenia"
|
||||||
STR_RECOVERY_MODE_HINT: "Umiestnite firmware.bin do koreňového adresára SD karty a vyberte ho"
|
STR_RECOVERY_MODE_HINT: "Umiestnite firmware.bin do koreňového adresára SD karty a vyberte ho"
|
||||||
|
STR_ADD_HIDDEN_NETWORK: "Pridať skrytú sieť..."
|
||||||
|
STR_ENTER_WIFI_SSID: "Zadajte názov siete (SSID)"
|
||||||
|
|||||||
@@ -94,6 +94,7 @@ STR_USERNAME: "Uporabniško ime"
|
|||||||
STR_PASSWORD: "Geslo"
|
STR_PASSWORD: "Geslo"
|
||||||
STR_SYNC_SERVER_URL: "URL strežnika za sinhronizacijo"
|
STR_SYNC_SERVER_URL: "URL strežnika za sinhronizacijo"
|
||||||
STR_DOCUMENT_MATCHING: "Ujemanje dokumentov"
|
STR_DOCUMENT_MATCHING: "Ujemanje dokumentov"
|
||||||
|
STR_SEND_METADATA: "Pošlji metapodatke dokumenta"
|
||||||
STR_AUTHENTICATE: "Avtentikacija"
|
STR_AUTHENTICATE: "Avtentikacija"
|
||||||
STR_KOREADER_USERNAME: "KOReader uporabnik"
|
STR_KOREADER_USERNAME: "KOReader uporabnik"
|
||||||
STR_KOREADER_PASSWORD: "KOReader geslo"
|
STR_KOREADER_PASSWORD: "KOReader geslo"
|
||||||
@@ -263,7 +264,6 @@ STR_CHAPTER_PREFIX: "Poglavje: "
|
|||||||
STR_PAGES_SEPARATOR: " strani | "
|
STR_PAGES_SEPARATOR: " strani | "
|
||||||
STR_BOOK_PREFIX: "Knjiga: "
|
STR_BOOK_PREFIX: "Knjiga: "
|
||||||
STR_CALIBRE_URL_HINT: "Za Calibre dodaj /opds svojemu URL-ju"
|
STR_CALIBRE_URL_HINT: "Za Calibre dodaj /opds svojemu URL-ju"
|
||||||
STR_PERCENT_STEP_HINT: "Levo/desno: 1% Gor/dol: 10%"
|
|
||||||
STR_SYNCING_TIME: "Sinhronizacija časa..."
|
STR_SYNCING_TIME: "Sinhronizacija časa..."
|
||||||
STR_CALC_HASH: "Izračunavanje podpisa dokumenta..."
|
STR_CALC_HASH: "Izračunavanje podpisa dokumenta..."
|
||||||
STR_HASH_FAILED: "Izračun podpisa dokumenta ni uspel"
|
STR_HASH_FAILED: "Izračun podpisa dokumenta ni uspel"
|
||||||
@@ -295,8 +295,11 @@ STR_NO_FOOTNOTES: "Na tej strani ni opomb"
|
|||||||
STR_LINK: "[povezava]"
|
STR_LINK: "[povezava]"
|
||||||
STR_SLEEP_TIMER_VALUE_FORMAT: "%u min"
|
STR_SLEEP_TIMER_VALUE_FORMAT: "%u min"
|
||||||
STR_SLEEP_NEVER: "Nikoli"
|
STR_SLEEP_NEVER: "Nikoli"
|
||||||
STR_SLEEP_TIMER_STEP_HINT: "Levo/desno: 1 min Gor/dol: 5 min"
|
STR_STEP_HINT_FRONT: "Sprednji gumbi:"
|
||||||
|
STR_STEP_HINT_SIDE: "Stranski gumbi:"
|
||||||
STR_SCREENSHOT_BUTTON: "Naredi posnetek zaslona"
|
STR_SCREENSHOT_BUTTON: "Naredi posnetek zaslona"
|
||||||
STR_AUTO_TURN_ENABLED: "Samodejno obračanje: "
|
STR_AUTO_TURN_ENABLED: "Samodejno obračanje: "
|
||||||
STR_AUTO_TURN_PAGES_PER_MIN: "Samodejno obračanje (strani na minuto)"
|
STR_AUTO_TURN_PAGES_PER_MIN: "Samodejno obračanje (strani na minuto)"
|
||||||
STR_TILT_PAGE_TURN: "Obračanje s priklonom"
|
STR_TILT_PAGE_TURN: "Obračanje s priklonom"
|
||||||
|
STR_ADD_HIDDEN_NETWORK: "Dodaj skrito omrežje..."
|
||||||
|
STR_ENTER_WIFI_SSID: "Vnesite ime omrežja (SSID)"
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ STR_NO_CHAPTERS: "Sin capítulos"
|
|||||||
STR_END_OF_BOOK: "Fin del libro"
|
STR_END_OF_BOOK: "Fin del libro"
|
||||||
STR_EMPTY_CHAPTER: "Capítulo vacío"
|
STR_EMPTY_CHAPTER: "Capítulo vacío"
|
||||||
STR_INDEXING: "Indexando"
|
STR_INDEXING: "Indexando"
|
||||||
|
STR_INDEX_FAILED: "No se pudo indexar: libro no válido"
|
||||||
STR_MEMORY_ERROR: "Error de memoria"
|
STR_MEMORY_ERROR: "Error de memoria"
|
||||||
STR_PAGE_LOAD_ERROR: "Error al cargar la página"
|
STR_PAGE_LOAD_ERROR: "Error al cargar la página"
|
||||||
STR_EMPTY_FILE: "Archivo vacío"
|
STR_EMPTY_FILE: "Archivo vacío"
|
||||||
@@ -28,7 +29,10 @@ STR_WIFI_NETWORKS: "Redes Wi-Fi"
|
|||||||
STR_NO_NETWORKS: "No hay redes disponibles"
|
STR_NO_NETWORKS: "No hay redes disponibles"
|
||||||
STR_NETWORKS_FOUND: "%zu red(es) encontrada(s)"
|
STR_NETWORKS_FOUND: "%zu red(es) encontrada(s)"
|
||||||
STR_SCANNING: "Buscando..."
|
STR_SCANNING: "Buscando..."
|
||||||
|
STR_FINDING_SAVED_WIFI: "Buscando redes Wi-Fi guardadas..."
|
||||||
STR_CONNECTING: "Conectando..."
|
STR_CONNECTING: "Conectando..."
|
||||||
|
STR_CONNECTING_SAVED_WIFI: "Conectando a una red Wi-Fi guardada..."
|
||||||
|
STR_SHOW_NETWORKS: "Mostrar"
|
||||||
STR_CONNECTED: "¡Conectado!"
|
STR_CONNECTED: "¡Conectado!"
|
||||||
STR_CONNECTION_FAILED: "Error de conexión"
|
STR_CONNECTION_FAILED: "Error de conexión"
|
||||||
STR_FORGET_NETWORK: "¿Olvidar la red?"
|
STR_FORGET_NETWORK: "¿Olvidar la red?"
|
||||||
@@ -69,6 +73,8 @@ STR_IMAGES: "Imágenes"
|
|||||||
STR_IMAGES_DISPLAY: "Mostrar"
|
STR_IMAGES_DISPLAY: "Mostrar"
|
||||||
STR_IMAGES_PLACEHOLDER: "Reemplazar"
|
STR_IMAGES_PLACEHOLDER: "Reemplazar"
|
||||||
STR_IMAGES_SUPPRESS: "Ocultar"
|
STR_IMAGES_SUPPRESS: "Ocultar"
|
||||||
|
STR_EOB_HOME: "Inicio"
|
||||||
|
STR_EOB_CONTINUE_WITH: "Continuar con"
|
||||||
STR_SHORT_PWR_BTN: "Toque corto del encendido"
|
STR_SHORT_PWR_BTN: "Toque corto del encendido"
|
||||||
STR_ORIENTATION: "Orientación"
|
STR_ORIENTATION: "Orientación"
|
||||||
STR_SIDE_BTN_LAYOUT: "Función botones laterales (lector)"
|
STR_SIDE_BTN_LAYOUT: "Función botones laterales (lector)"
|
||||||
@@ -98,6 +104,7 @@ STR_USERNAME: "Usuario"
|
|||||||
STR_PASSWORD: "Contraseña"
|
STR_PASSWORD: "Contraseña"
|
||||||
STR_SYNC_SERVER_URL: "URL del servidor de sinc."
|
STR_SYNC_SERVER_URL: "URL del servidor de sinc."
|
||||||
STR_DOCUMENT_MATCHING: "Coincidencia de doc."
|
STR_DOCUMENT_MATCHING: "Coincidencia de doc."
|
||||||
|
STR_SEND_METADATA: "Enviar metadatos doc."
|
||||||
STR_AUTHENTICATE: "Autenticar"
|
STR_AUTHENTICATE: "Autenticar"
|
||||||
STR_KOREADER_USERNAME: "Usuario de KOReader"
|
STR_KOREADER_USERNAME: "Usuario de KOReader"
|
||||||
STR_KOREADER_PASSWORD: "Contraseña de KOReader"
|
STR_KOREADER_PASSWORD: "Contraseña de KOReader"
|
||||||
@@ -297,7 +304,6 @@ STR_CHAPTER_PREFIX: "Cap.: "
|
|||||||
STR_PAGES_SEPARATOR: " pág. | "
|
STR_PAGES_SEPARATOR: " pág. | "
|
||||||
STR_BOOK_PREFIX: "Libro: "
|
STR_BOOK_PREFIX: "Libro: "
|
||||||
STR_CALIBRE_URL_HINT: "Para Calibre, agregue /opds a su URL"
|
STR_CALIBRE_URL_HINT: "Para Calibre, agregue /opds a su URL"
|
||||||
STR_PERCENT_STEP_HINT: "Izq./Dcha.: 1% | Subir/Bajar: 10%"
|
|
||||||
STR_SYNCING_TIME: "Tiempo de sincronización..."
|
STR_SYNCING_TIME: "Tiempo de sincronización..."
|
||||||
STR_CALC_HASH: "Calculando hash del documento..."
|
STR_CALC_HASH: "Calculando hash del documento..."
|
||||||
STR_HASH_FAILED: "No se pudo calcular el hash del documento"
|
STR_HASH_FAILED: "No se pudo calcular el hash del documento"
|
||||||
@@ -331,7 +337,8 @@ STR_NO_FOOTNOTES: "No hay notas al pie de esta página"
|
|||||||
STR_LINK: "[enlace]"
|
STR_LINK: "[enlace]"
|
||||||
STR_SLEEP_TIMER_VALUE_FORMAT: "%u min."
|
STR_SLEEP_TIMER_VALUE_FORMAT: "%u min."
|
||||||
STR_SLEEP_NEVER: "Nunca"
|
STR_SLEEP_NEVER: "Nunca"
|
||||||
STR_SLEEP_TIMER_STEP_HINT: "Izq./Dcha.: 1 min. Subir/Bajar: 5 min."
|
STR_STEP_HINT_FRONT: "Botones frontales:"
|
||||||
|
STR_STEP_HINT_SIDE: "Botones laterales:"
|
||||||
STR_SCREENSHOT_BUTTON: "Tomar captura de pantalla"
|
STR_SCREENSHOT_BUTTON: "Tomar captura de pantalla"
|
||||||
STR_ADD_SERVER: "Agregar servidor"
|
STR_ADD_SERVER: "Agregar servidor"
|
||||||
STR_SERVER_NAME: "Nombre de servidor"
|
STR_SERVER_NAME: "Nombre de servidor"
|
||||||
@@ -383,3 +390,5 @@ STR_FIRMWARE_WRITE_FAILED: "Falló la escritura del firmware"
|
|||||||
STR_FIRMWARE_UPDATE_DO_NOT_POWER_OFF: "¡No apague el dispositivo!"
|
STR_FIRMWARE_UPDATE_DO_NOT_POWER_OFF: "¡No apague el dispositivo!"
|
||||||
STR_RECOVERY_MODE: "Modo de recuperación"
|
STR_RECOVERY_MODE: "Modo de recuperación"
|
||||||
STR_RECOVERY_MODE_HINT: "Ponga firmware.bin en la raíz de la tarj. SD y selecciónelo"
|
STR_RECOVERY_MODE_HINT: "Ponga firmware.bin en la raíz de la tarj. SD y selecciónelo"
|
||||||
|
STR_ADD_HIDDEN_NETWORK: "Añadir red oculta..."
|
||||||
|
STR_ENTER_WIFI_SSID: "Introduce el nombre de la red (SSID)"
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ STR_NO_CHAPTERS: "Inga kapitel"
|
|||||||
STR_END_OF_BOOK: "Slutet på boken"
|
STR_END_OF_BOOK: "Slutet på boken"
|
||||||
STR_EMPTY_CHAPTER: "Tomt kapitel"
|
STR_EMPTY_CHAPTER: "Tomt kapitel"
|
||||||
STR_INDEXING: "Indexerar"
|
STR_INDEXING: "Indexerar"
|
||||||
|
STR_INDEX_FAILED: "Misslyckades att indexera - ogiltig bok"
|
||||||
STR_MEMORY_ERROR: "Minnesfel"
|
STR_MEMORY_ERROR: "Minnesfel"
|
||||||
STR_PAGE_LOAD_ERROR: "Sidladdningsfel"
|
STR_PAGE_LOAD_ERROR: "Sidladdningsfel"
|
||||||
STR_EMPTY_FILE: "Tom fil"
|
STR_EMPTY_FILE: "Tom fil"
|
||||||
@@ -61,7 +62,7 @@ STR_CAT_READER: "Läsare"
|
|||||||
STR_CAT_CONTROLS: "Kontroller"
|
STR_CAT_CONTROLS: "Kontroller"
|
||||||
STR_CAT_SYSTEM: "System"
|
STR_CAT_SYSTEM: "System"
|
||||||
STR_SLEEP_SCREEN: "Viloskärm"
|
STR_SLEEP_SCREEN: "Viloskärm"
|
||||||
STR_SEAMLESS_SLEEP: "Sida som viloskärm"
|
STR_QUICK_RESUME_TIMEOUT: "Snabb återupptagning efter timeout"
|
||||||
STR_SLEEP_COVER_MODE: "Viloskärmens omslagsläge"
|
STR_SLEEP_COVER_MODE: "Viloskärmens omslagsläge"
|
||||||
STR_HIDE_BATTERY: "Dölj batteriprocent"
|
STR_HIDE_BATTERY: "Dölj batteriprocent"
|
||||||
STR_EXTRA_SPACING: "Extra paragrafmellanrum"
|
STR_EXTRA_SPACING: "Extra paragrafmellanrum"
|
||||||
@@ -70,6 +71,8 @@ STR_IMAGES: "Bilder"
|
|||||||
STR_IMAGES_DISPLAY: "Visa"
|
STR_IMAGES_DISPLAY: "Visa"
|
||||||
STR_IMAGES_PLACEHOLDER: "Platshållare"
|
STR_IMAGES_PLACEHOLDER: "Platshållare"
|
||||||
STR_IMAGES_SUPPRESS: "Dölj"
|
STR_IMAGES_SUPPRESS: "Dölj"
|
||||||
|
STR_EOB_HOME: "Hem"
|
||||||
|
STR_EOB_CONTINUE_WITH: "Fortsätt med"
|
||||||
STR_SHORT_PWR_BTN: "Kort strömknappsklick"
|
STR_SHORT_PWR_BTN: "Kort strömknappsklick"
|
||||||
STR_ORIENTATION: "Läsrikting"
|
STR_ORIENTATION: "Läsrikting"
|
||||||
STR_SIDE_BTN_LAYOUT: "Sidoknappslayout (Läsare)"
|
STR_SIDE_BTN_LAYOUT: "Sidoknappslayout (Läsare)"
|
||||||
@@ -78,6 +81,7 @@ STR_LONG_PRESS_BEHAVIOR: "Beteende vid lång knapptryckning"
|
|||||||
STR_LONG_PRESS_BEHAVIOR_OFF: "AV"
|
STR_LONG_PRESS_BEHAVIOR_OFF: "AV"
|
||||||
STR_LONG_PRESS_BEHAVIOR_SKIP: "Hoppa över kapitel"
|
STR_LONG_PRESS_BEHAVIOR_SKIP: "Hoppa över kapitel"
|
||||||
STR_LONG_PRESS_BEHAVIOR_ORIENTATION: "Ändra orientering"
|
STR_LONG_PRESS_BEHAVIOR_ORIENTATION: "Ändra orientering"
|
||||||
|
STR_LONG_PRESS_MENU: "Långtrycksmeny"
|
||||||
STR_FONT_PREVIEW_TEXT: "Flygande bäckasiner söka hwila på mjuka tuvor"
|
STR_FONT_PREVIEW_TEXT: "Flygande bäckasiner söka hwila på mjuka tuvor"
|
||||||
STR_FONT_FAMILY: "Eboksläsarens typsnittsfamilj"
|
STR_FONT_FAMILY: "Eboksläsarens typsnittsfamilj"
|
||||||
STR_FONT_SIZE: "Eboksläsarens typsnittsstorlek"
|
STR_FONT_SIZE: "Eboksläsarens typsnittsstorlek"
|
||||||
@@ -98,6 +102,7 @@ STR_USERNAME: "Användarnamn"
|
|||||||
STR_PASSWORD: "Lösenord"
|
STR_PASSWORD: "Lösenord"
|
||||||
STR_SYNC_SERVER_URL: "Synkronisera serveradress"
|
STR_SYNC_SERVER_URL: "Synkronisera serveradress"
|
||||||
STR_DOCUMENT_MATCHING: "Dokumentmatchning"
|
STR_DOCUMENT_MATCHING: "Dokumentmatchning"
|
||||||
|
STR_SEND_METADATA: "Skicka dokumentmetadata"
|
||||||
STR_AUTHENTICATE: "Autentisera "
|
STR_AUTHENTICATE: "Autentisera "
|
||||||
STR_KOREADER_USERNAME: "KOReader användarnamn"
|
STR_KOREADER_USERNAME: "KOReader användarnamn"
|
||||||
STR_KOREADER_PASSWORD: "KOReader lösenord"
|
STR_KOREADER_PASSWORD: "KOReader lösenord"
|
||||||
@@ -142,6 +147,8 @@ STR_ORIENTATION_INVERTED: "Porträtt 180°"
|
|||||||
STR_LANDSCAPE_CCW: "Landskap moturs"
|
STR_LANDSCAPE_CCW: "Landskap moturs"
|
||||||
STR_PREV_NEXT: "Förra/Nästa"
|
STR_PREV_NEXT: "Förra/Nästa"
|
||||||
STR_NEXT_PREV: "Nästa/Förra"
|
STR_NEXT_PREV: "Nästa/Förra"
|
||||||
|
STR_KOSYNC: "KOSync"
|
||||||
|
STR_BOOKMARK_OPTION: "Bokmärke"
|
||||||
STR_DISABLED: "Inaktiverad"
|
STR_DISABLED: "Inaktiverad"
|
||||||
STR_NOTO_SERIF: "Noto Serif"
|
STR_NOTO_SERIF: "Noto Serif"
|
||||||
STR_NOTO_SANS: "Noto Sans"
|
STR_NOTO_SANS: "Noto Sans"
|
||||||
@@ -255,7 +262,6 @@ STR_THEME_LYRA: "Lyra"
|
|||||||
STR_THEME_ROUNDEDRAFF: "RoundedRaff"
|
STR_THEME_ROUNDEDRAFF: "RoundedRaff"
|
||||||
STR_THEME_LYRA_EXTENDED: "Lyra utökad"
|
STR_THEME_LYRA_EXTENDED: "Lyra utökad"
|
||||||
STR_SUNLIGHT_FADING_FIX: "Fix för solskensmattning"
|
STR_SUNLIGHT_FADING_FIX: "Fix för solskensmattning"
|
||||||
STR_QUICK_RESUME_TIMEOUT: "Snabb återupptagning efter timeout"
|
|
||||||
STR_REMAP_FRONT_BUTTONS: "Ändra frontknappar"
|
STR_REMAP_FRONT_BUTTONS: "Ändra frontknappar"
|
||||||
STR_BOOKMARKS: "Bokmärken"
|
STR_BOOKMARKS: "Bokmärken"
|
||||||
STR_BOOKMARK_ADDED: "Bokmärke tillagt."
|
STR_BOOKMARK_ADDED: "Bokmärke tillagt."
|
||||||
@@ -295,7 +301,6 @@ STR_CHAPTER_PREFIX: "Kapitel:"
|
|||||||
STR_PAGES_SEPARATOR: " sidor | "
|
STR_PAGES_SEPARATOR: " sidor | "
|
||||||
STR_BOOK_PREFIX: "Bok:"
|
STR_BOOK_PREFIX: "Bok:"
|
||||||
STR_CALIBRE_URL_HINT: "För Calibre: lägg till /opds i din adress"
|
STR_CALIBRE_URL_HINT: "För Calibre: lägg till /opds i din adress"
|
||||||
STR_PERCENT_STEP_HINT: "Vänster/Höger: 1% Upp/Ner 10%"
|
|
||||||
STR_SYNCING_TIME: "Synkroniserar tid…"
|
STR_SYNCING_TIME: "Synkroniserar tid…"
|
||||||
STR_CALC_HASH: "Beräknar dokumenthash"
|
STR_CALC_HASH: "Beräknar dokumenthash"
|
||||||
STR_HASH_FAILED: "Misslyckades att beräkna dokumenthash"
|
STR_HASH_FAILED: "Misslyckades att beräkna dokumenthash"
|
||||||
@@ -322,14 +327,16 @@ STR_BOOK_S_STYLE: "Bokstil"
|
|||||||
STR_EMBEDDED_STYLE: "Inbäddad stil"
|
STR_EMBEDDED_STYLE: "Inbäddad stil"
|
||||||
STR_FOCUS_READING: "Fokusläsning"
|
STR_FOCUS_READING: "Fokusläsning"
|
||||||
STR_OPDS_SERVER_URL: "OPDS-serveradress"
|
STR_OPDS_SERVER_URL: "OPDS-serveradress"
|
||||||
|
STR_PWR_BTN_FOOTNOTE_BACK: "Snabbåtergång från fotnoter"
|
||||||
STR_SET_SLEEP_COVER: "Ställ in omslag"
|
STR_SET_SLEEP_COVER: "Ställ in omslag"
|
||||||
STR_FOOTNOTES: "Fotnoter"
|
STR_FOOTNOTES: "Fotnoter"
|
||||||
STR_NO_FOOTNOTES: "Inga fotnoter på den här sidan"
|
STR_NO_FOOTNOTES: "Inga fotnoter på den här sidan"
|
||||||
STR_LINK: "[länk]"
|
STR_LINK: "[länk]"
|
||||||
|
STR_SCREENSHOT_BUTTON: "Ta en skärmdump"
|
||||||
STR_SLEEP_TIMER_VALUE_FORMAT: "%u min"
|
STR_SLEEP_TIMER_VALUE_FORMAT: "%u min"
|
||||||
STR_SLEEP_NEVER: "Aldrig"
|
STR_SLEEP_NEVER: "Aldrig"
|
||||||
STR_SLEEP_TIMER_STEP_HINT: "Vänster/Höger: 1 min Upp/Ner: 5 min"
|
STR_STEP_HINT_FRONT: "Framknappar:"
|
||||||
STR_SCREENSHOT_BUTTON: "Ta en skärmdump"
|
STR_STEP_HINT_SIDE: "Sidoknappar:"
|
||||||
STR_ADD_SERVER: "Lägg till server"
|
STR_ADD_SERVER: "Lägg till server"
|
||||||
STR_SERVER_NAME: "Servernamn"
|
STR_SERVER_NAME: "Servernamn"
|
||||||
STR_NO_SERVERS: "Inga OPDS-servrar konfigurerade"
|
STR_NO_SERVERS: "Inga OPDS-servrar konfigurerade"
|
||||||
@@ -380,3 +387,5 @@ STR_FIRMWARE_WRITE_FAILED: "Skrivning till firmware misslyckades"
|
|||||||
STR_FIRMWARE_UPDATE_DO_NOT_POWER_OFF: "Stäng inte av!"
|
STR_FIRMWARE_UPDATE_DO_NOT_POWER_OFF: "Stäng inte av!"
|
||||||
STR_RECOVERY_MODE: "Återställningsläge"
|
STR_RECOVERY_MODE: "Återställningsläge"
|
||||||
STR_RECOVERY_MODE_HINT: "Placera firmware.bin i SD-kortroten och välj den"
|
STR_RECOVERY_MODE_HINT: "Placera firmware.bin i SD-kortroten och välj den"
|
||||||
|
STR_ADD_HIDDEN_NETWORK: "Lägg till dolt nätverk..."
|
||||||
|
STR_ENTER_WIFI_SSID: "Ange nätverksnamn (SSID)"
|
||||||
|
|||||||
@@ -68,10 +68,10 @@ STR_SHORT_PWR_BTN: "Kısa Güç Tuşu Tıklaması"
|
|||||||
STR_ORIENTATION: "Okuma Yönü"
|
STR_ORIENTATION: "Okuma Yönü"
|
||||||
STR_SIDE_BTN_LAYOUT: "Yan Tuş Dizilimi (okuyucu)"
|
STR_SIDE_BTN_LAYOUT: "Yan Tuş Dizilimi (okuyucu)"
|
||||||
STR_FRONT_BTN_FOLLOW_ORIENTATION: "Ön düğmeleri yönlendir"
|
STR_FRONT_BTN_FOLLOW_ORIENTATION: "Ön düğmeleri yönlendir"
|
||||||
STR_LONG_PRESS_BEHAVIOR: "Long-press button behavior"
|
STR_LONG_PRESS_BEHAVIOR: "Uzun basma tuş davranışı"
|
||||||
STR_LONG_PRESS_BEHAVIOR_OFF: "OFF"
|
STR_LONG_PRESS_BEHAVIOR_OFF: "KAPALI"
|
||||||
STR_LONG_PRESS_BEHAVIOR_SKIP: "Chapter skip"
|
STR_LONG_PRESS_BEHAVIOR_SKIP: "Bölüm atlama"
|
||||||
STR_LONG_PRESS_BEHAVIOR_ORIENTATION: "Orientation change"
|
STR_LONG_PRESS_BEHAVIOR_ORIENTATION: "Yön değiştirme"
|
||||||
STR_FONT_PREVIEW_TEXT: "Pijamalı hasta yağız şoföre çabucak güvendi"
|
STR_FONT_PREVIEW_TEXT: "Pijamalı hasta yağız şoföre çabucak güvendi"
|
||||||
STR_FONT_FAMILY: "Okuyucu Yazı Tipi Ailesi"
|
STR_FONT_FAMILY: "Okuyucu Yazı Tipi Ailesi"
|
||||||
STR_FONT_SIZE: "Arayüz Yazı Boyutu"
|
STR_FONT_SIZE: "Arayüz Yazı Boyutu"
|
||||||
@@ -92,6 +92,7 @@ STR_USERNAME: "Kullanıcı Adı"
|
|||||||
STR_PASSWORD: "Şifre"
|
STR_PASSWORD: "Şifre"
|
||||||
STR_SYNC_SERVER_URL: "Senkronizasyon Sunucu Adresi"
|
STR_SYNC_SERVER_URL: "Senkronizasyon Sunucu Adresi"
|
||||||
STR_DOCUMENT_MATCHING: "Belge Eşleştirme"
|
STR_DOCUMENT_MATCHING: "Belge Eşleştirme"
|
||||||
|
STR_SEND_METADATA: "Belge üst verisi gönder"
|
||||||
STR_AUTHENTICATE: "Kimlik Doğrula"
|
STR_AUTHENTICATE: "Kimlik Doğrula"
|
||||||
STR_KOREADER_USERNAME: "KOReader Kullanıcı Adı"
|
STR_KOREADER_USERNAME: "KOReader Kullanıcı Adı"
|
||||||
STR_KOREADER_PASSWORD: "KOReader Şifresi"
|
STR_KOREADER_PASSWORD: "KOReader Şifresi"
|
||||||
@@ -241,7 +242,6 @@ STR_CHAPTER_PREFIX: "Bölüm: "
|
|||||||
STR_PAGES_SEPARATOR: " sayfa | "
|
STR_PAGES_SEPARATOR: " sayfa | "
|
||||||
STR_BOOK_PREFIX: "Kitap: "
|
STR_BOOK_PREFIX: "Kitap: "
|
||||||
STR_CALIBRE_URL_HINT: "Calibre için URL'nize /opds ekleyin"
|
STR_CALIBRE_URL_HINT: "Calibre için URL'nize /opds ekleyin"
|
||||||
STR_PERCENT_STEP_HINT: "Sol/Sağ: %1 Yukarı/Aşağı: %10"
|
|
||||||
STR_SYNCING_TIME: "Zaman senkronize ediliyor..."
|
STR_SYNCING_TIME: "Zaman senkronize ediliyor..."
|
||||||
STR_CALC_HASH: "Belge özeti hesaplanıyor..."
|
STR_CALC_HASH: "Belge özeti hesaplanıyor..."
|
||||||
STR_HASH_FAILED: "Belge özeti hesaplanamadı"
|
STR_HASH_FAILED: "Belge özeti hesaplanamadı"
|
||||||
@@ -289,7 +289,8 @@ STR_IMAGES_SUPPRESS: "Bastır"
|
|||||||
STR_LINK: "[bağlantı]"
|
STR_LINK: "[bağlantı]"
|
||||||
STR_SLEEP_TIMER_VALUE_FORMAT: "%u dak"
|
STR_SLEEP_TIMER_VALUE_FORMAT: "%u dak"
|
||||||
STR_SLEEP_NEVER: "Asla"
|
STR_SLEEP_NEVER: "Asla"
|
||||||
STR_SLEEP_TIMER_STEP_HINT: "Sol/Sağ: 1 dak Yukarı/Aşağı: 5 dak"
|
STR_STEP_HINT_FRONT: "Ön tuşlar:"
|
||||||
|
STR_STEP_HINT_SIDE: "Yan tuşlar:"
|
||||||
STR_NO_FILES_FOUND: "Dosya bulunamadı"
|
STR_NO_FILES_FOUND: "Dosya bulunamadı"
|
||||||
STR_NO_FOOTNOTES: "Bu sayfada dipnot yok"
|
STR_NO_FOOTNOTES: "Bu sayfada dipnot yok"
|
||||||
STR_PREVIEW: "Önizleme"
|
STR_PREVIEW: "Önizleme"
|
||||||
@@ -303,3 +304,91 @@ STR_SELECTED: "Seçili"
|
|||||||
STR_SHOW: "Göster"
|
STR_SHOW: "Göster"
|
||||||
STR_TITLE: "Başlık"
|
STR_TITLE: "Başlık"
|
||||||
STR_TILT_PAGE_TURN: "Eğerek sayfa çevirme"
|
STR_TILT_PAGE_TURN: "Eğerek sayfa çevirme"
|
||||||
|
STR_ADD_HIDDEN_NETWORK: "Gizli ağ ekle..."
|
||||||
|
STR_ENTER_WIFI_SSID: "Ağ adını girin (SSID)"
|
||||||
|
STR_ADD_SERVER: "Sunucu Ekle"
|
||||||
|
STR_BOOKMARKS: "Yer İmleri"
|
||||||
|
STR_BOOKMARK_ADDED: "Yer imi eklendi."
|
||||||
|
STR_BOOKMARK_OPTION: "Yer İmi"
|
||||||
|
STR_BOTTOM: "Alt"
|
||||||
|
STR_CLOCK: "Saat"
|
||||||
|
STR_CLOCK_FORMAT: "Saat Biçimi"
|
||||||
|
STR_CLOCK_FORMAT_12H: "12 saat"
|
||||||
|
STR_CLOCK_FORMAT_24H: "24 saat"
|
||||||
|
STR_CLOCK_SYNC: "Saati Eşitle"
|
||||||
|
STR_CLOCK_SYNCED: "Saat Eşitlendi"
|
||||||
|
STR_CLOCK_SYNCING: "NTP'den eşitleniyor..."
|
||||||
|
STR_CLOCK_SYNC_FAIL: "Eşitleme başarısız"
|
||||||
|
STR_CLOCK_SYNC_NOW: "Saati şimdi eşitle"
|
||||||
|
STR_CLOCK_SYNC_NO_WIFI: "Wi-Fi bağlı değil"
|
||||||
|
STR_CLOCK_SYNC_NO_WIFI_HINT: "Önce Wi-Fi'ye bağlanın, sonra tekrar deneyin."
|
||||||
|
STR_CLOCK_SYNC_OK: "Saat eşitlendi"
|
||||||
|
STR_CLOCK_UTC_OFFSET: "Saat UTC Farkı"
|
||||||
|
STR_CONFIRM_DELETE_BOOKMARK: "Bu yer imi silinsin mi?"
|
||||||
|
STR_CONNECTING_SAVED_WIFI: "Kayıtlı Wi-Fi'ye bağlanılıyor..."
|
||||||
|
STR_CRASH_DESCRIPTION: "Ayrıntılı rapor crash_report.txt dosyasına kaydedildi. Lütfen hata bildiriminize bu dosyayı ekleyin."
|
||||||
|
STR_CRASH_NO_REASON: "(Neden kaydedilmedi)"
|
||||||
|
STR_CRASH_REASON: "Çökme nedeni:"
|
||||||
|
STR_CRASH_TITLE: "Sistem Çökmesi"
|
||||||
|
STR_CURRENT_TIME: "Şu anki saat:"
|
||||||
|
STR_DELETE_SERVER: "Sunucuyu Sil"
|
||||||
|
STR_DISABLED: "Devre dışı"
|
||||||
|
STR_DOWNLOAD_ALL: "Tümünü İndir"
|
||||||
|
STR_EOB_CONTINUE_WITH: "Şununla devam et"
|
||||||
|
STR_EOB_HOME: "Ana Ekran"
|
||||||
|
STR_FINDING_SAVED_WIFI: "Kayıtlı Wi-Fi aranıyor..."
|
||||||
|
STR_FIRMWARE_FILE_OPEN_FAILED: "Dosya açılamıyor"
|
||||||
|
STR_FIRMWARE_TOO_LARGE: "Firmware, bölümlemeye sığmıyor"
|
||||||
|
STR_FIRMWARE_TOO_SMALL: "Firmware dosyası çok küçük"
|
||||||
|
STR_FIRMWARE_UPDATE_DO_NOT_POWER_OFF: "Cihazı kapatmayın!"
|
||||||
|
STR_FIRMWARE_UPDATE_PROMPT: "Firmware güncellensin mi?"
|
||||||
|
STR_FIRMWARE_WRITE_FAILED: "Firmware yazılamadı"
|
||||||
|
STR_FONT_BROWSER: "Yazı Tipi Tarayıcısı"
|
||||||
|
STR_FONT_INSTALLED: "Yazı tipi yüklendi!"
|
||||||
|
STR_FONT_INSTALL_FAILED: "Yazı tipi yüklenemedi"
|
||||||
|
STR_FORCE_REFRESH: "Ekranı Yenile"
|
||||||
|
STR_INDEX_FAILED: "Endeksleme başarısız - geçersiz kitap"
|
||||||
|
STR_INSTALLED: "Yüklü"
|
||||||
|
STR_INVALID_FIRMWARE: "Geçersiz firmware dosyası"
|
||||||
|
STR_KB_HINT_CLEAR_TEXT: "Tüm metni silmek için DEL tuşunu basılı tutun"
|
||||||
|
STR_KB_HINT_EDIT_ENTRY: "Girdiyi düzenlemek için UP tuşunu basılı tutun"
|
||||||
|
STR_KB_HINT_EXIT_URL_MODE: "URL modundan çıkmak için ABC'ye basın"
|
||||||
|
STR_KB_HINT_HIDE_PASSWORD: "Şifreyi gizlemek için RIGHT'ı basılı tutup [***] tuşuna basın"
|
||||||
|
STR_KB_HINT_LOWER_SECONDARY: "Küçük harf veya ikincil karakter için SELECT'i basılı tutun"
|
||||||
|
STR_KB_HINT_MOVE_CURSOR: "İmleci taşımak için LEFT veya RIGHT'a basın"
|
||||||
|
STR_KB_HINT_RETURN_CURSOR: "İmleç konumuna dönmek için LEFT'e basın"
|
||||||
|
STR_KB_HINT_RETURN_KEYBOARD: "Klavyeye dönmek için DOWN'a basın"
|
||||||
|
STR_KB_HINT_SECONDARY_CHAR: "İkincil karakter için SELECT'i basılı tutun"
|
||||||
|
STR_KB_HINT_SHOW_PASSWORD: "Şifreyi göstermek için RIGHT'ı basılı tutup [abc] tuşuna basın"
|
||||||
|
STR_KB_HINT_TOGGLE_HIDE_PASSWORD: "Şifreyi gizlemek için [***] tuşuna basın"
|
||||||
|
STR_KB_HINT_TOGGLE_SHOW_PASSWORD: "Şifreyi göstermek için [abc] tuşuna basın"
|
||||||
|
STR_KB_HINT_UPPER_SECONDARY: "BÜYÜK harf veya ikincil karakter için SELECT'i basılı tutun"
|
||||||
|
STR_KB_HINT_URL_SNIPPETS: "Hazır kalıplar için URL'ye basın"
|
||||||
|
STR_KB_TIPS: "İpuçları:"
|
||||||
|
STR_KOSYNC: "KOSync"
|
||||||
|
STR_LOADING_FONT_LIST: "Yazı tipi listesi yükleniyor..."
|
||||||
|
STR_LONG_PRESS_MENU: "Uzun Basma Menüsü"
|
||||||
|
STR_MANAGE_FONTS: "Yazı Tiplerini Yönet"
|
||||||
|
STR_NEXT_FIELD: "Sonraki"
|
||||||
|
STR_NEXT_PAGE: "Sonraki Sayfa »"
|
||||||
|
STR_NO_BIN_FILES: ".bin dosyası bulunamadı"
|
||||||
|
STR_NO_FONTS_AVAILABLE: "Kullanılabilir yazı tipi yok"
|
||||||
|
STR_NO_SERVERS: "Yapılandırılmış OPDS sunucusu yok"
|
||||||
|
STR_OPDS_SERVERS: "OPDS Sunucuları"
|
||||||
|
STR_PREV_PAGE: "« Önceki Sayfa"
|
||||||
|
STR_PWR_BTN_FOOTNOTE_BACK: "Dipnottan hızlı dönüş"
|
||||||
|
STR_RECOVERY_MODE: "Kurtarma Modu"
|
||||||
|
STR_RECOVERY_MODE_HINT: "firmware.bin dosyasını SD kartın köküne koyup seçin"
|
||||||
|
STR_RESTARTING_HINT: "Yeniden başlatılıyor... Cihaz yeniden başlamazsa güç düğmesini birkaç saniye basılı tutun."
|
||||||
|
STR_SD_FIRMWARE_UPDATE: "SD Karttan Firmware Güncelleme"
|
||||||
|
STR_SEARCH: "Ara"
|
||||||
|
STR_SELECT_FIRMWARE_FILE: "Firmware dosyası seçin (.bin)"
|
||||||
|
STR_SERVER_NAME: "Sunucu Adı"
|
||||||
|
STR_SET_SLEEP_COVER: "Kapak Ayarla"
|
||||||
|
STR_SHOW_NETWORKS: "Göster"
|
||||||
|
STR_THEME_ROUNDEDRAFF: "RoundedRaff"
|
||||||
|
STR_TOP: "Üst"
|
||||||
|
STR_UPDATE_ALL: "Tümünü Güncelle"
|
||||||
|
STR_UPDATE_AVAILABLE: "Güncelle"
|
||||||
|
STR_VALIDATING_FIRMWARE: "Firmware doğrulanıyor..."
|
||||||
|
STR_XTC_STATUS_BAR: "XTC Durum Çubuğu"
|
||||||
|
|||||||
@@ -98,6 +98,7 @@ STR_USERNAME: "Ім'я користувача"
|
|||||||
STR_PASSWORD: "Пароль"
|
STR_PASSWORD: "Пароль"
|
||||||
STR_SYNC_SERVER_URL: "URL для синхронізації"
|
STR_SYNC_SERVER_URL: "URL для синхронізації"
|
||||||
STR_DOCUMENT_MATCHING: "Порівняння документів"
|
STR_DOCUMENT_MATCHING: "Порівняння документів"
|
||||||
|
STR_SEND_METADATA: "Надсилати метадані"
|
||||||
STR_AUTHENTICATE: "Автентифікувати"
|
STR_AUTHENTICATE: "Автентифікувати"
|
||||||
STR_KOREADER_USERNAME: "Ім'я користувача KOReader"
|
STR_KOREADER_USERNAME: "Ім'я користувача KOReader"
|
||||||
STR_KOREADER_PASSWORD: "Пароль KOReader"
|
STR_KOREADER_PASSWORD: "Пароль KOReader"
|
||||||
@@ -294,7 +295,6 @@ STR_CHAPTER_PREFIX: "Розділ: "
|
|||||||
STR_PAGES_SEPARATOR: " сторінок | "
|
STR_PAGES_SEPARATOR: " сторінок | "
|
||||||
STR_BOOK_PREFIX: "Книга: "
|
STR_BOOK_PREFIX: "Книга: "
|
||||||
STR_CALIBRE_URL_HINT: "Для Calibre додайте /opds до вашої URL"
|
STR_CALIBRE_URL_HINT: "Для Calibre додайте /opds до вашої URL"
|
||||||
STR_PERCENT_STEP_HINT: "Вліво/Вправо: 1% Вгору/Вниз: 10%"
|
|
||||||
STR_SYNCING_TIME: "Синхронізація часу..."
|
STR_SYNCING_TIME: "Синхронізація часу..."
|
||||||
STR_CALC_HASH: "Обчислення хешу документа..."
|
STR_CALC_HASH: "Обчислення хешу документа..."
|
||||||
STR_HASH_FAILED: "Не вдалося обчислити хеш документа"
|
STR_HASH_FAILED: "Не вдалося обчислити хеш документа"
|
||||||
@@ -329,7 +329,8 @@ STR_LINK: "[посилання]"
|
|||||||
STR_SCREENSHOT_BUTTON: "Знімок екрана"
|
STR_SCREENSHOT_BUTTON: "Знімок екрана"
|
||||||
STR_SLEEP_TIMER_VALUE_FORMAT: "%u хв"
|
STR_SLEEP_TIMER_VALUE_FORMAT: "%u хв"
|
||||||
STR_SLEEP_NEVER: "Ніколи"
|
STR_SLEEP_NEVER: "Ніколи"
|
||||||
STR_SLEEP_TIMER_STEP_HINT: "Вліво/Вправо: 1 хв Вгору/Вниз: 5 хв"
|
STR_STEP_HINT_FRONT: "Передні кнопки:"
|
||||||
|
STR_STEP_HINT_SIDE: "Бічні кнопки:"
|
||||||
STR_ADD_SERVER: "Додати сервер"
|
STR_ADD_SERVER: "Додати сервер"
|
||||||
STR_SERVER_NAME: "Назва сервера"
|
STR_SERVER_NAME: "Назва сервера"
|
||||||
STR_NO_SERVERS: "Не налаштовано жодного сервера OPDS"
|
STR_NO_SERVERS: "Не налаштовано жодного сервера OPDS"
|
||||||
@@ -380,3 +381,5 @@ STR_FIRMWARE_WRITE_FAILED: "Помилка запису прошивки"
|
|||||||
STR_FIRMWARE_UPDATE_DO_NOT_POWER_OFF: "Не вимикайте пристрій!"
|
STR_FIRMWARE_UPDATE_DO_NOT_POWER_OFF: "Не вимикайте пристрій!"
|
||||||
STR_RECOVERY_MODE: "Режим відновлення"
|
STR_RECOVERY_MODE: "Режим відновлення"
|
||||||
STR_RECOVERY_MODE_HINT: "Помістіть firmware.bin у корінь SD-карти та виберіть його"
|
STR_RECOVERY_MODE_HINT: "Помістіть firmware.bin у корінь SD-карти та виберіть його"
|
||||||
|
STR_ADD_HIDDEN_NETWORK: "Додати приховану мережу..."
|
||||||
|
STR_ENTER_WIFI_SSID: "Введіть назву мережі (SSID)"
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ STR_BOOTING: "ARRENCANT"
|
|||||||
STR_SLEEPING: "ENTRANT EN REPÒS"
|
STR_SLEEPING: "ENTRANT EN REPÒS"
|
||||||
STR_ENTERING_SLEEP: "Entrant en repòs"
|
STR_ENTERING_SLEEP: "Entrant en repòs"
|
||||||
STR_BROWSE_FILES: "Explora arxius"
|
STR_BROWSE_FILES: "Explora arxius"
|
||||||
STR_FILE_TRANSFER: "Transferència"
|
STR_FILE_TRANSFER: "Transferència d'arxius"
|
||||||
STR_SETTINGS_TITLE: "Configuració"
|
STR_SETTINGS_TITLE: "Configuració"
|
||||||
STR_CONTINUE_READING: "Continua llegint"
|
STR_CONTINUE_READING: "Continua llegint"
|
||||||
STR_NO_OPEN_BOOK: "Cap llibre obert"
|
STR_NO_OPEN_BOOK: "Cap llibre obert"
|
||||||
@@ -18,6 +18,7 @@ STR_NO_CHAPTERS: "Sense capítols"
|
|||||||
STR_END_OF_BOOK: "Final del llibre"
|
STR_END_OF_BOOK: "Final del llibre"
|
||||||
STR_EMPTY_CHAPTER: "Capítol buit"
|
STR_EMPTY_CHAPTER: "Capítol buit"
|
||||||
STR_INDEXING: "S'està indexant"
|
STR_INDEXING: "S'està indexant"
|
||||||
|
STR_INDEX_FAILED: "No s'ha pogut indexar: llibre no vàlid"
|
||||||
STR_MEMORY_ERROR: "Error de memòria"
|
STR_MEMORY_ERROR: "Error de memòria"
|
||||||
STR_PAGE_LOAD_ERROR: "Error en carregar la pàgina"
|
STR_PAGE_LOAD_ERROR: "Error en carregar la pàgina"
|
||||||
STR_EMPTY_FILE: "Arxiu buit"
|
STR_EMPTY_FILE: "Arxiu buit"
|
||||||
@@ -28,60 +29,65 @@ STR_WIFI_NETWORKS: "Xarxes Wi-Fi"
|
|||||||
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"
|
||||||
STR_SCANNING: "S'està escanejant..."
|
STR_SCANNING: "S'està escanejant..."
|
||||||
|
STR_FINDING_SAVED_WIFI: "S'estan buscant xarxes Wi-Fi guardades..."
|
||||||
STR_CONNECTING: "S'està connectant..."
|
STR_CONNECTING: "S'està connectant..."
|
||||||
|
STR_CONNECTING_SAVED_WIFI: "S'està connectant a una xarxa Wi-Fi guardada..."
|
||||||
|
STR_SHOW_NETWORKS: "Mostra"
|
||||||
STR_CONNECTED: "S'ha connectat!"
|
STR_CONNECTED: "S'ha connectat!"
|
||||||
STR_CONNECTION_FAILED: "Error de connexió"
|
STR_CONNECTION_FAILED: "Error de connexió"
|
||||||
STR_FORGET_NETWORK: "Voleu oblidar esta xarxa?"
|
STR_FORGET_NETWORK: "Vols oblidar esta xarxa?"
|
||||||
STR_SAVE_PASSWORD: "Voleu guardar la contrasenya per a la pròxima vegada?"
|
STR_SAVE_PASSWORD: "Vols guardar la contrasenya per a la pròxima vegada?"
|
||||||
STR_PRESS_OK_SCAN: "Premeu OK per tornar a escanejar"
|
STR_PRESS_OK_SCAN: "Prem OK per tornar a escanejar"
|
||||||
STR_JOIN_NETWORK: "Uneix-te a una xarxa"
|
STR_JOIN_NETWORK: "Unix-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 Wi-Fi existent"
|
STR_JOIN_DESC: "Connecta't a una xarxa Wi-Fi existent"
|
||||||
STR_HOTSPOT_DESC: "Crea una xarxa Wi-Fi per unir-s'hi"
|
STR_HOTSPOT_DESC: "Crea una xarxa Wi-Fi per unir-s'hi"
|
||||||
STR_STARTING_HOTSPOT: "S'està iniciant el punt d'accés..."
|
STR_STARTING_HOTSPOT: "S'està iniciant el punt d'accés..."
|
||||||
STR_HOTSPOT_MODE: "Mode de punt d'accés"
|
STR_HOTSPOT_MODE: "Mode de punt d'accés"
|
||||||
STR_CONNECT_WIFI_HINT: "Connecteu el dispositiu a esta xarxa Wi-Fi"
|
STR_CONNECT_WIFI_HINT: "Connecta el dispositiu a esta xarxa Wi-Fi"
|
||||||
STR_OPEN_URL_HINT: "Obriu este URL al navegador"
|
STR_OPEN_URL_HINT: "Obri este URL al navegador"
|
||||||
STR_OR_HTTP_PREFIX: "o http://"
|
STR_OR_HTTP_PREFIX: "o http://"
|
||||||
STR_SCAN_QR_HINT: "o escanegeu el codi QR amb el telèfon:"
|
STR_SCAN_QR_HINT: "o escaneja el codi QR amb el telèfon:"
|
||||||
STR_CALIBRE_WIRELESS: "Calibre sense fils"
|
STR_CALIBRE_WIRELESS: "Calibre sense fils"
|
||||||
STR_NETWORK_LEGEND: "* = Encriptat | + = Guardat"
|
STR_NETWORK_LEGEND: "* = Encriptat | + = Guardat"
|
||||||
STR_MAC_ADDRESS: "Adreça MAC:"
|
STR_MAC_ADDRESS: "Adreça MAC:"
|
||||||
STR_CHECKING_WIFI: "S'està comprovant el Wi-Fi..."
|
STR_CHECKING_WIFI: "S'està comprovant el Wi-Fi..."
|
||||||
STR_ENTER_WIFI_PASSWORD: "Introduïu la contrasenya Wi-Fi"
|
STR_ENTER_WIFI_PASSWORD: "Introduïx la contrasenya Wi-Fi"
|
||||||
STR_TO_PREFIX: "a "
|
STR_TO_PREFIX: "a "
|
||||||
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_INSTRUCTION_1: "1) Instal·leu el connector CrossPoint Reader"
|
STR_CALIBRE_INSTRUCTION_1: "1) Instal·la el connector CrossPoint Reader"
|
||||||
STR_CALIBRE_INSTRUCTION_2: "2) Estigueu a la mateixa xarxa Wi-Fi"
|
STR_CALIBRE_INSTRUCTION_2: "2) Estigues a la mateixa xarxa Wi-Fi"
|
||||||
STR_CALIBRE_INSTRUCTION_3: "3) A Calibre: \"Envia a un dispositiu\""
|
STR_CALIBRE_INSTRUCTION_3: "3) A Calibre: \"Envia a un dispositiu\""
|
||||||
STR_CALIBRE_INSTRUCTION_4: "\"Mantingueu esta pantalla oberta mentre s'envia\""
|
STR_CALIBRE_INSTRUCTION_4: "\"Mantín esta pantalla oberta mentre s'envia\""
|
||||||
STR_CAT_DISPLAY: "Visualització"
|
STR_CAT_DISPLAY: "Visualització"
|
||||||
STR_CAT_READER: "Lector"
|
STR_CAT_READER: "Lector"
|
||||||
STR_CAT_CONTROLS: "Controls"
|
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_QUICK_RESUME_TIMEOUT: "Represa ràpida després del temps"
|
STR_QUICK_RESUME_TIMEOUT: "Represa ràpida per inactivitat"
|
||||||
STR_SLEEP_COVER_MODE: "Mode de pantalla de repòs"
|
STR_SLEEP_COVER_MODE: "Ajust de la portada en repòs"
|
||||||
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: "Imatges"
|
||||||
STR_IMAGES_DISPLAY: "Mostrar"
|
STR_IMAGES_DISPLAY: "Mostrar"
|
||||||
STR_IMAGES_PLACEHOLDER: "Text de mostra"
|
STR_IMAGES_PLACEHOLDER: "Text alternatiu"
|
||||||
STR_IMAGES_SUPPRESS: "Eliminar"
|
STR_IMAGES_SUPPRESS: "Eliminar"
|
||||||
STR_SHORT_PWR_BTN: "Clic curt del botó d'engegada"
|
STR_EOB_HOME: "Inici"
|
||||||
|
STR_EOB_CONTINUE_WITH: "Continua amb"
|
||||||
|
STR_SHORT_PWR_BTN: "Pulsació curta del botó d'engegada"
|
||||||
STR_ORIENTATION: "Orientació de lectura"
|
STR_ORIENTATION: "Orientació de lectura"
|
||||||
STR_SIDE_BTN_LAYOUT: "Disposició botons laterals"
|
STR_SIDE_BTN_LAYOUT: "Disposició botons laterals"
|
||||||
STR_FRONT_BTN_FOLLOW_ORIENTATION: "Orientar botons frontals"
|
STR_FRONT_BTN_FOLLOW_ORIENTATION: "Orientar botons frontals"
|
||||||
STR_LONG_PRESS_BEHAVIOR: "Comportament de prémer llargament el botó"
|
STR_LONG_PRESS_BEHAVIOR: "Acció en mantindre premut un botó"
|
||||||
STR_LONG_PRESS_BEHAVIOR_OFF: "Desactivat"
|
STR_LONG_PRESS_BEHAVIOR_OFF: "Desactivat"
|
||||||
STR_LONG_PRESS_BEHAVIOR_SKIP: "Saltar capítols"
|
STR_LONG_PRESS_BEHAVIOR_SKIP: "Saltar capítols"
|
||||||
STR_LONG_PRESS_BEHAVIOR_ORIENTATION: "Canvi d'orientació"
|
STR_LONG_PRESS_BEHAVIOR_ORIENTATION: "Canvi d'orientació"
|
||||||
STR_LONG_PRESS_MENU: "Funció de pulsació llarga"
|
STR_LONG_PRESS_MENU: "Funció de pulsació llarga"
|
||||||
STR_FONT_PREVIEW_TEXT: "Jove xef, porti whisky amb quinze glaçons d'hidrogen, coi!"
|
STR_FONT_PREVIEW_TEXT: "Jove xef, porti whisky amb quinze glaçons d'hidrogen, coi!"
|
||||||
STR_FONT_FAMILY: "Família de fonts"
|
STR_FONT_FAMILY: "Família de fonts"
|
||||||
STR_FONT_SIZE: "Grandària de la lletra (UI)"
|
STR_FONT_SIZE: "Cos de lletra del lector"
|
||||||
STR_LINE_SPACING: "Interlineat del lector"
|
STR_LINE_SPACING: "Interlineat del lector"
|
||||||
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"
|
||||||
@@ -89,25 +95,27 @@ 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_SLEEP_TIMER_VALUE_FORMAT: "%u min"
|
STR_SLEEP_TIMER_VALUE_FORMAT: "%u min"
|
||||||
STR_SLEEP_NEVER: "Mai"
|
STR_SLEEP_NEVER: "Mai"
|
||||||
STR_SLEEP_TIMER_STEP_HINT: "Esquerra/Dreta: 1 min Amunt/Avall: 5 min"
|
STR_STEP_HINT_FRONT: "Botons frontals:"
|
||||||
|
STR_STEP_HINT_SIDE: "Botons laterals:"
|
||||||
STR_SHOW_HIDDEN_FILES: "Mostra arxius ocults"
|
STR_SHOW_HIDDEN_FILES: "Mostra arxius ocults"
|
||||||
STR_REMOVE_READ_FROM_RECENTS: "Esborra els llibres llegits de la llista de recents"
|
STR_REMOVE_READ_FROM_RECENTS: "Esborra els llibres llegits de la llista de recents"
|
||||||
STR_MOVE_FINISHED_TO_READ: "Mou els llibres acabats a la carpeta Read"
|
STR_MOVE_FINISHED_TO_READ: "Mou els llibres acabats a la carpeta Read"
|
||||||
STR_REFRESH_FREQ: "Freqüència de refresc"
|
STR_REFRESH_FREQ: "Freqüència d'actualització"
|
||||||
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: "Llengua"
|
||||||
STR_CLEAR_READING_CACHE: "Esborra la memòria cau de lectura"
|
STR_CLEAR_READING_CACHE: "Esborra la memòria cau de lectura"
|
||||||
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ó"
|
||||||
STR_DOCUMENT_MATCHING: "Coincidència de documents"
|
STR_DOCUMENT_MATCHING: "Coincidència de documents"
|
||||||
|
STR_SEND_METADATA: "Envia metadades del document"
|
||||||
STR_AUTHENTICATE: "Autentica"
|
STR_AUTHENTICATE: "Autentica"
|
||||||
STR_KOREADER_USERNAME: "Nom d'usuari del KOReader"
|
STR_KOREADER_USERNAME: "Nom d'usuari del KOReader"
|
||||||
STR_KOREADER_PASSWORD: "Contrasenya del KOReader"
|
STR_KOREADER_PASSWORD: "Contrasenya del KOReader"
|
||||||
STR_FILENAME: "Nom d'arxiu"
|
STR_FILENAME: "Nom d'arxiu"
|
||||||
STR_BINARY: "Binari"
|
STR_BINARY: "Binari"
|
||||||
STR_SET_CREDENTIALS_FIRST: "Estableix les credencials primer"
|
STR_SET_CREDENTIALS_FIRST: "Establix les credencials primer"
|
||||||
STR_WIFI_CONN_FAILED: "Connexió Wi-Fi fallida"
|
STR_WIFI_CONN_FAILED: "Connexió Wi-Fi fallida"
|
||||||
STR_AUTHENTICATING: "S'està autenticant..."
|
STR_AUTHENTICATING: "S'està autenticant..."
|
||||||
STR_AUTH_SUCCESS: "Autenticació correcta!"
|
STR_AUTH_SUCCESS: "Autenticació correcta!"
|
||||||
@@ -147,7 +155,7 @@ STR_PREV_NEXT: "Anterior/Següent"
|
|||||||
STR_NEXT_PREV: "Següent/Anterior"
|
STR_NEXT_PREV: "Següent/Anterior"
|
||||||
STR_KOSYNC: "KOSync"
|
STR_KOSYNC: "KOSync"
|
||||||
STR_BOOKMARK_OPTION: "Punt de llibre"
|
STR_BOOKMARK_OPTION: "Punt de llibre"
|
||||||
STR_DISABLED: "Desactivats"
|
STR_DISABLED: "Sense funció"
|
||||||
STR_NOTO_SERIF: "Noto Serif"
|
STR_NOTO_SERIF: "Noto Serif"
|
||||||
STR_NOTO_SANS: "Noto Sans"
|
STR_NOTO_SANS: "Noto Sans"
|
||||||
STR_SMALL: "Petita"
|
STR_SMALL: "Petita"
|
||||||
@@ -175,16 +183,16 @@ STR_UPDATING: "S'està actualitzant..."
|
|||||||
STR_NO_UPDATE: "No hi ha actualitzacions disponibles"
|
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: "Prem i mantín premut el botó d'encesa per tornar a engegar"
|
||||||
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"
|
||||||
STR_ERROR_MSG: "Error:"
|
STR_ERROR_MSG: "Error:"
|
||||||
STR_UNNAMED: "Sense nom"
|
STR_UNNAMED: "Sense nom"
|
||||||
STR_HOLD_OPEN_TO_DELETE: "Manteniu premut Obre per esborrar"
|
STR_HOLD_OPEN_TO_DELETE: "Mantín premut Obri per esborrar"
|
||||||
STR_NO_SERVER_URL: "No s'ha configurat cap URL de servidor"
|
STR_NO_SERVER_URL: "No s'ha configurat cap URL de servidor"
|
||||||
STR_FETCH_FEED_FAILED: "Ha fallat l'obtenció del feed"
|
STR_FETCH_FEED_FAILED: "Ha fallat l'obtenció del canal de continguts"
|
||||||
STR_PARSE_FEED_FAILED: "Ha fallat l'anàlisi del feed"
|
STR_PARSE_FEED_FAILED: "Ha fallat l'anàlisi del canal de continguts"
|
||||||
STR_NETWORK_PREFIX: "Xarxa: "
|
STR_NETWORK_PREFIX: "Xarxa: "
|
||||||
STR_IP_ADDRESS_PREFIX: "Adreça IP: "
|
STR_IP_ADDRESS_PREFIX: "Adreça IP: "
|
||||||
STR_ERROR_GENERAL_FAILURE: "Error: Fallada general"
|
STR_ERROR_GENERAL_FAILURE: "Error: Fallada general"
|
||||||
@@ -197,11 +205,11 @@ STR_HOME: "« Inici"
|
|||||||
STR_SELECT: "Selecciona"
|
STR_SELECT: "Selecciona"
|
||||||
STR_SELECTED: "Seleccionat"
|
STR_SELECTED: "Seleccionat"
|
||||||
STR_TOGGLE: "Canvia"
|
STR_TOGGLE: "Canvia"
|
||||||
STR_TOGGLE_BOOKMARK: "Canvia punt de llibre"
|
STR_TOGGLE_BOOKMARK: "Afig o elimina el punt de llibre"
|
||||||
STR_CONFIRM: "Confirma"
|
STR_CONFIRM: "Confirma"
|
||||||
STR_CANCEL: "Cancel·la"
|
STR_CANCEL: "Cancel·la"
|
||||||
STR_CONNECT: "Connecta"
|
STR_CONNECT: "Connecta"
|
||||||
STR_OPEN: "Obre"
|
STR_OPEN: "Obri"
|
||||||
STR_DOWNLOAD: "Descarrega"
|
STR_DOWNLOAD: "Descarrega"
|
||||||
STR_RETRY: "Reintenta"
|
STR_RETRY: "Reintenta"
|
||||||
STR_YES: "Sí"
|
STR_YES: "Sí"
|
||||||
@@ -216,7 +224,7 @@ STR_DIR_RIGHT: "Dreta"
|
|||||||
STR_DIR_UP: "Amunt"
|
STR_DIR_UP: "Amunt"
|
||||||
STR_DIR_DOWN: "Avall"
|
STR_DIR_DOWN: "Avall"
|
||||||
STR_OK_BUTTON: "OK"
|
STR_OK_BUTTON: "OK"
|
||||||
STR_SLEEP_COVER_FILTER: "Filtre de pantalla de repòs"
|
STR_SLEEP_COVER_FILTER: "Filtre de la portada en repòs"
|
||||||
STR_FILTER_CONTRAST: "Contrast"
|
STR_FILTER_CONTRAST: "Contrast"
|
||||||
STR_CUSTOMISE_STATUS_BAR: "Personalitza la barra d'estat"
|
STR_CUSTOMISE_STATUS_BAR: "Personalitza la barra d'estat"
|
||||||
STR_CHAPTER_PAGE_COUNT: "Comptador de pàgines del capítol"
|
STR_CHAPTER_PAGE_COUNT: "Comptador de pàgines del capítol"
|
||||||
@@ -246,20 +254,20 @@ 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_MENU_RECENT_BOOKS: "Llibres recents"
|
STR_MENU_RECENT_BOOKS: "Llibres recents"
|
||||||
STR_REMOVE_FROM_RECENTS: "Voleu 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"
|
||||||
STR_CALIBRE_DESC: "Utilitza les transferències sense fils de Calibre"
|
STR_CALIBRE_DESC: "Utilitza les transferències sense fils de Calibre"
|
||||||
STR_FORGET_AND_REMOVE: "Voleu eliminar la contrasenya guardada?"
|
STR_FORGET_AND_REMOVE: "Vols oblidar la xarxa i eliminar la contrasenya guardada?"
|
||||||
STR_FORGET_BUTTON: "Oblida"
|
STR_FORGET_BUTTON: "Oblida"
|
||||||
STR_CALIBRE_STARTING: "S'està iniciant el Calibre..."
|
STR_CALIBRE_STARTING: "S'està iniciant el Calibre..."
|
||||||
STR_CALIBRE_SETUP: "Configura"
|
STR_CALIBRE_SETUP: "Configura"
|
||||||
STR_CALIBRE_STATUS: "Estat"
|
STR_CALIBRE_STATUS: "Estat"
|
||||||
STR_CLEAR_BUTTON: "Esborra"
|
STR_CLEAR_BUTTON: "Esborra"
|
||||||
STR_DEFAULT_VALUE: "Per defecte"
|
STR_DEFAULT_VALUE: "Per defecte"
|
||||||
STR_REMAP_PROMPT: "Premeu un botó frontal per a cada rol"
|
STR_REMAP_PROMPT: "Prem un botó frontal per a cada rol"
|
||||||
STR_UNASSIGNED: "No assignat"
|
STR_UNASSIGNED: "No assignat"
|
||||||
STR_ALREADY_ASSIGNED: "Ja assignat"
|
STR_ALREADY_ASSIGNED: "Ja assignat"
|
||||||
STR_REMAP_RESET_HINT: "Botó lateral Amunt: Restableix la disposició per defecte"
|
STR_REMAP_RESET_HINT: "Botó lateral Amunt: Restablix la disposició per defecte"
|
||||||
STR_REMAP_CANCEL_HINT: "Botó lateral Avall: Cancel·la la reassignació"
|
STR_REMAP_CANCEL_HINT: "Botó lateral Avall: Cancel·la la reassignació"
|
||||||
STR_HW_BACK_LABEL: "Arrere (1r botó)"
|
STR_HW_BACK_LABEL: "Arrere (1r botó)"
|
||||||
STR_HW_CONFIRM_LABEL: "Confirma (2n botó)"
|
STR_HW_CONFIRM_LABEL: "Confirma (2n botó)"
|
||||||
@@ -270,20 +278,19 @@ 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_DELETE: "Esborra"
|
||||||
STR_CONFIRM_DELETE_BOOKMARK: "Voleu esborrar este punt de llibre?"
|
STR_CONFIRM_DELETE_BOOKMARK: "Vols esborrar este punt de llibre?"
|
||||||
STR_DISPLAY_QR: "Mostra la pàgina com a QR"
|
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_CALIBRE_URL_HINT: "Per al Calibre, afegiu /opds a la URL"
|
STR_CALIBRE_URL_HINT: "Per al Calibre, afig /opds a la URL"
|
||||||
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..."
|
||||||
STR_CALC_HASH: "S'està calculant el hash del document..."
|
STR_CALC_HASH: "S'està calculant l'empremta electrònica del document..."
|
||||||
STR_HASH_FAILED: "No s'ha pogut calcular el hash del document"
|
STR_HASH_FAILED: "No s'ha pogut calcular l'empremta electrònica del document"
|
||||||
STR_FETCH_PROGRESS: "S'està obtenint el progrés remot..."
|
STR_FETCH_PROGRESS: "S'està obtenint el progrés remot..."
|
||||||
STR_UPLOAD_PROGRESS: "S'està pujant el progrés..."
|
STR_UPLOAD_PROGRESS: "S'està pujant el progrés..."
|
||||||
STR_NO_CREDENTIALS_MSG: "No s'han configurat credencials"
|
STR_NO_CREDENTIALS_MSG: "No s'han configurat credencials"
|
||||||
STR_KOREADER_SETUP_HINT: "Configureu el compte de KOReader en Configuració"
|
STR_KOREADER_SETUP_HINT: "Configura el compte de KOReader en Configuració"
|
||||||
STR_PROGRESS_FOUND: "S'ha trobat progrés!"
|
STR_PROGRESS_FOUND: "S'ha trobat progrés!"
|
||||||
STR_REMOTE_LABEL: "Remot:"
|
STR_REMOTE_LABEL: "Remot:"
|
||||||
STR_LOCAL_LABEL: "Local:"
|
STR_LOCAL_LABEL: "Local:"
|
||||||
@@ -293,7 +300,7 @@ STR_DEVICE_FROM_FORMAT: " De: %s"
|
|||||||
STR_APPLY_REMOTE: "Aplica el progrés remot"
|
STR_APPLY_REMOTE: "Aplica el progrés remot"
|
||||||
STR_UPLOAD_LOCAL: "Puja el progrés local"
|
STR_UPLOAD_LOCAL: "Puja el progrés local"
|
||||||
STR_NO_REMOTE_MSG: "No s'ha trobat progrés remot"
|
STR_NO_REMOTE_MSG: "No s'ha trobat progrés remot"
|
||||||
STR_UPLOAD_PROMPT: "Voleu pujar la posició actual?"
|
STR_UPLOAD_PROMPT: "Vols pujar la posició actual?"
|
||||||
STR_UPLOAD_SUCCESS: "Progrés pujat!"
|
STR_UPLOAD_SUCCESS: "Progrés pujat!"
|
||||||
STR_SYNC_FAILED_MSG: "Sincronització fallida"
|
STR_SYNC_FAILED_MSG: "Sincronització fallida"
|
||||||
STR_SAVE_PROGRESS_FAILED: "No s'ha pogut guardar el progrés"
|
STR_SAVE_PROGRESS_FAILED: "No s'ha pogut guardar el progrés"
|
||||||
@@ -308,11 +315,11 @@ STR_FOOTNOTES: "Notes al peu"
|
|||||||
STR_NO_FOOTNOTES: "No hi ha notes al peu en esta pàgina"
|
STR_NO_FOOTNOTES: "No hi ha notes al peu en esta pàgina"
|
||||||
STR_LINK: "[enllaç]"
|
STR_LINK: "[enllaç]"
|
||||||
STR_SCREENSHOT_BUTTON: "Fes una captura de pantalla"
|
STR_SCREENSHOT_BUTTON: "Fes una captura de pantalla"
|
||||||
STR_AUTO_TURN_ENABLED: "Passar automàtic activat: "
|
STR_AUTO_TURN_ENABLED: "Pas automàtic de pàgina activat"
|
||||||
STR_AUTO_TURN_PAGES_PER_MIN: "Passar automàtic (pàgines per minut)"
|
STR_AUTO_TURN_PAGES_PER_MIN: "Pas automàtic de pàgina (pàg./min)"
|
||||||
STR_TILT_PAGE_TURN: "Pas de pàgina per inclinació"
|
STR_TILT_PAGE_TURN: "Pas de pàgina per inclinació"
|
||||||
STR_FORCE_REFRESH: "Refresca la pantalla"
|
STR_FORCE_REFRESH: "Refresca la pantalla"
|
||||||
STR_RESTARTING_HINT: "S'està reiniciant... Si el dispositiu no es reinicia, manteniu premut el botó d'encesa durant uns segons."
|
STR_RESTARTING_HINT: "S'està reiniciant... Si el dispositiu no es reinicia, mantín premut el botó d'encesa durant uns segons."
|
||||||
STR_NEXT_PAGE: "Pàgina següent »"
|
STR_NEXT_PAGE: "Pàgina següent »"
|
||||||
STR_PREV_PAGE: "« Pàgina anterior"
|
STR_PREV_PAGE: "« Pàgina anterior"
|
||||||
STR_XTC_STATUS_BAR: "Barra d'estat XTC"
|
STR_XTC_STATUS_BAR: "Barra d'estat XTC"
|
||||||
@@ -351,35 +358,37 @@ STR_INSTALLED: "Instal·lat"
|
|||||||
STR_DOWNLOAD_ALL: "Descarrega-ho tot"
|
STR_DOWNLOAD_ALL: "Descarrega-ho tot"
|
||||||
STR_UPDATE_ALL: "Actualitza-ho tot"
|
STR_UPDATE_ALL: "Actualitza-ho tot"
|
||||||
STR_UPDATE_AVAILABLE: "Actualitza"
|
STR_UPDATE_AVAILABLE: "Actualitza"
|
||||||
STR_CRASH_TITLE: "Bloqueig del sistema"
|
STR_CRASH_TITLE: "Fallada del sistema"
|
||||||
STR_CRASH_DESCRIPTION: "S'ha guardat un informe detallat a crash_report.txt. Incloeu este arxiu en l'informe d'errors."
|
STR_CRASH_DESCRIPTION: "S'ha guardat un informe detallat a crash_report.txt. Inclou este arxiu en l'informe d'errors."
|
||||||
STR_CRASH_REASON: "Motiu del bloqueig:"
|
STR_CRASH_REASON: "Motiu de la fallada"
|
||||||
STR_CRASH_NO_REASON: "(No s'ha registrat cap motiu)"
|
STR_CRASH_NO_REASON: "(No s'ha registrat cap motiu)"
|
||||||
STR_KB_HINT_MOVE_CURSOR: "Prem Esquerra o Dreta per moure el cursor"
|
STR_KB_HINT_MOVE_CURSOR: "Prem Esquerra o Dreta per moure el cursor"
|
||||||
STR_KB_HINT_RETURN_CURSOR: "Prem Esquerra per tornar a la posició del cursor"
|
STR_KB_HINT_RETURN_CURSOR: "Prem Esquerra per tornar a la posició del cursor"
|
||||||
STR_KB_HINT_HIDE_PASSWORD: "Mantén premut Dreta i prem [***] per ocultar la contrasenya"
|
STR_KB_HINT_HIDE_PASSWORD: "Mantín premut Dreta i prem [***] per ocultar la contrasenya"
|
||||||
STR_KB_HINT_SHOW_PASSWORD: "Mantén premut Dreta i prem [abc] per mostrar la contrasenya"
|
STR_KB_HINT_SHOW_PASSWORD: "Mantín premut Dreta i prem [abc] per mostrar la contrasenya"
|
||||||
STR_KB_HINT_TOGGLE_HIDE_PASSWORD: "Prem [***] per ocultar la contrasenya"
|
STR_KB_HINT_TOGGLE_HIDE_PASSWORD: "Prem [***] per ocultar la contrasenya"
|
||||||
STR_KB_HINT_TOGGLE_SHOW_PASSWORD: "Prem [abc] per mostrar la contrasenya"
|
STR_KB_HINT_TOGGLE_SHOW_PASSWORD: "Prem [abc] per mostrar la contrasenya"
|
||||||
STR_KB_HINT_EDIT_ENTRY: "Mantén premut Amunt per editar l'entrada"
|
STR_KB_HINT_EDIT_ENTRY: "Mantín premut Amunt per editar l'entrada"
|
||||||
STR_KB_TIPS: "Consells:"
|
STR_KB_TIPS: "Consells:"
|
||||||
STR_KB_HINT_RETURN_KEYBOARD: "Prem Avall per tornar al teclat"
|
STR_KB_HINT_RETURN_KEYBOARD: "Prem Avall per tornar al teclat"
|
||||||
STR_KB_HINT_EXIT_URL_MODE: "Prem ABC per eixir del mode URL"
|
STR_KB_HINT_EXIT_URL_MODE: "Prem ABC per eixir del mode URL"
|
||||||
STR_KB_HINT_CLEAR_TEXT: "Mantén premut DEL per esborrar tot el text"
|
STR_KB_HINT_CLEAR_TEXT: "Mantín premut DEL per esborrar tot el text"
|
||||||
STR_KB_HINT_SECONDARY_CHAR: "Mantén premut SELECT per al caràcter secundari"
|
STR_KB_HINT_SECONDARY_CHAR: "Mantín premut SELECT: caràcter secundari"
|
||||||
STR_KB_HINT_UPPER_SECONDARY: "Mantén premut SELECT per MAJÚSCULES o caràcter secundari"
|
STR_KB_HINT_UPPER_SECONDARY: "Mantín premut SELECT: majúscules o caràcter secundari"
|
||||||
STR_KB_HINT_LOWER_SECONDARY: "Mantén premut SELECT per minúscules o caràcter secundari"
|
STR_KB_HINT_LOWER_SECONDARY: "Mantín premut SELECT: minúscules o caràcter secundari"
|
||||||
STR_KB_HINT_URL_SNIPPETS: "Prem URL per inserir fragments"
|
STR_KB_HINT_URL_SNIPPETS: "Prem URL per inserir fragments"
|
||||||
STR_SD_FIRMWARE_UPDATE: "Actualització de firmware des de la targeta SD"
|
STR_SD_FIRMWARE_UPDATE: "Actualització de firmware des de la targeta SD"
|
||||||
STR_SELECT_FIRMWARE_FILE: "Seleccioneu un arxiu de firmware (.bin)"
|
STR_SELECT_FIRMWARE_FILE: "Selecciona un arxiu de firmware (.bin)"
|
||||||
STR_NO_BIN_FILES: "No s'han trobat arxius .bin"
|
STR_NO_BIN_FILES: "No s'han trobat arxius .bin"
|
||||||
STR_VALIDATING_FIRMWARE: "S'està validant el firmware..."
|
STR_VALIDATING_FIRMWARE: "S'està validant el firmware..."
|
||||||
STR_INVALID_FIRMWARE: "Arxiu de firmware no vàlid"
|
STR_INVALID_FIRMWARE: "Arxiu de firmware no vàlid"
|
||||||
STR_FIRMWARE_TOO_LARGE: "El firmware és massa gran per a la partició"
|
STR_FIRMWARE_TOO_LARGE: "El firmware és massa gran per a la partició"
|
||||||
STR_FIRMWARE_TOO_SMALL: "L'arxiu de firmware és massa menut"
|
STR_FIRMWARE_TOO_SMALL: "L'arxiu de firmware és massa menut"
|
||||||
STR_FIRMWARE_UPDATE_PROMPT: "Voleu actualitzar el firmware?"
|
STR_FIRMWARE_UPDATE_PROMPT: "Vols actualitzar el firmware?"
|
||||||
STR_FIRMWARE_FILE_OPEN_FAILED: "No es pot obrir l'arxiu"
|
STR_FIRMWARE_FILE_OPEN_FAILED: "No es pot obrir l'arxiu"
|
||||||
STR_FIRMWARE_WRITE_FAILED: "Ha fallat l'escriptura del firmware"
|
STR_FIRMWARE_WRITE_FAILED: "Ha fallat l'escriptura del firmware"
|
||||||
STR_FIRMWARE_UPDATE_DO_NOT_POWER_OFF: "No apagueu el dispositiu!"
|
STR_FIRMWARE_UPDATE_DO_NOT_POWER_OFF: "No apagues el dispositiu!"
|
||||||
STR_RECOVERY_MODE: "Mode de recuperació"
|
STR_RECOVERY_MODE: "Mode de recuperació"
|
||||||
STR_RECOVERY_MODE_HINT: "Poseu firmware.bin a l'arrel de la targeta SD i seleccioneu-lo"
|
STR_RECOVERY_MODE_HINT: "Posa firmware.bin a l'arrel de la targeta SD i selecciona'l"
|
||||||
|
STR_ADD_HIDDEN_NETWORK: "Afig una xarxa oculta..."
|
||||||
|
STR_ENTER_WIFI_SSID: "Introduïx el nom de la xarxa (SSID)"
|
||||||
|
|||||||
@@ -98,6 +98,7 @@ STR_USERNAME: "Tên đăng nhập"
|
|||||||
STR_PASSWORD: "Mật khẩu"
|
STR_PASSWORD: "Mật khẩu"
|
||||||
STR_SYNC_SERVER_URL: "URL máy chủ đồng bộ"
|
STR_SYNC_SERVER_URL: "URL máy chủ đồng bộ"
|
||||||
STR_DOCUMENT_MATCHING: "Khớp tài liệu"
|
STR_DOCUMENT_MATCHING: "Khớp tài liệu"
|
||||||
|
STR_SEND_METADATA: "Gửi siêu dữ liệu tài liệu"
|
||||||
STR_AUTHENTICATE: "Xác thực"
|
STR_AUTHENTICATE: "Xác thực"
|
||||||
STR_KOREADER_USERNAME: "Tên đăng nhập KOReader"
|
STR_KOREADER_USERNAME: "Tên đăng nhập KOReader"
|
||||||
STR_KOREADER_PASSWORD: "Mật khẩu KOReader"
|
STR_KOREADER_PASSWORD: "Mật khẩu KOReader"
|
||||||
@@ -294,7 +295,6 @@ STR_CHAPTER_PREFIX: "Chương: "
|
|||||||
STR_PAGES_SEPARATOR: " trang | "
|
STR_PAGES_SEPARATOR: " trang | "
|
||||||
STR_BOOK_PREFIX: "Sách: "
|
STR_BOOK_PREFIX: "Sách: "
|
||||||
STR_CALIBRE_URL_HINT: "Với Calibre, thêm /opds vào URL"
|
STR_CALIBRE_URL_HINT: "Với Calibre, thêm /opds vào URL"
|
||||||
STR_PERCENT_STEP_HINT: "Trái/Phải: 1% Lên/Xuống: 10%"
|
|
||||||
STR_SYNCING_TIME: "Đang đồng bộ giờ..."
|
STR_SYNCING_TIME: "Đang đồng bộ giờ..."
|
||||||
STR_CALC_HASH: "Đang tính mã băm tài liệu..."
|
STR_CALC_HASH: "Đang tính mã băm tài liệu..."
|
||||||
STR_HASH_FAILED: "Không tính được mã băm tài liệu"
|
STR_HASH_FAILED: "Không tính được mã băm tài liệu"
|
||||||
@@ -328,7 +328,8 @@ STR_LINK: "[liên kết]"
|
|||||||
STR_SCREENSHOT_BUTTON: "Chụp màn hình"
|
STR_SCREENSHOT_BUTTON: "Chụp màn hình"
|
||||||
STR_SLEEP_TIMER_VALUE_FORMAT: "%u phút"
|
STR_SLEEP_TIMER_VALUE_FORMAT: "%u phút"
|
||||||
STR_SLEEP_NEVER: "Không bao giờ"
|
STR_SLEEP_NEVER: "Không bao giờ"
|
||||||
STR_SLEEP_TIMER_STEP_HINT: "Trái/Phải: 1 phút Lên/Xuống: 5 phút"
|
STR_STEP_HINT_FRONT: "Nút trước:"
|
||||||
|
STR_STEP_HINT_SIDE: "Nút bên:"
|
||||||
STR_ADD_SERVER: "Thêm máy chủ"
|
STR_ADD_SERVER: "Thêm máy chủ"
|
||||||
STR_SERVER_NAME: "Tên máy chủ"
|
STR_SERVER_NAME: "Tên máy chủ"
|
||||||
STR_NO_SERVERS: "Chưa cấu hình máy chủ OPDS"
|
STR_NO_SERVERS: "Chưa cấu hình máy chủ OPDS"
|
||||||
@@ -379,3 +380,5 @@ STR_FIRMWARE_WRITE_FAILED: "Ghi firmware thất bại"
|
|||||||
STR_FIRMWARE_UPDATE_DO_NOT_POWER_OFF: "Không được tắt nguồn!"
|
STR_FIRMWARE_UPDATE_DO_NOT_POWER_OFF: "Không được tắt nguồn!"
|
||||||
STR_RECOVERY_MODE: "Chế độ phục hồi"
|
STR_RECOVERY_MODE: "Chế độ phục hồi"
|
||||||
STR_RECOVERY_MODE_HINT: "Đặt firmware.bin ở thư mục gốc thẻ SD rồi chọn"
|
STR_RECOVERY_MODE_HINT: "Đặt firmware.bin ở thư mục gốc thẻ SD rồi chọn"
|
||||||
|
STR_ADD_HIDDEN_NETWORK: "Thêm mạng ẩn..."
|
||||||
|
STR_ENTER_WIFI_SSID: "Nhập tên mạng (SSID)"
|
||||||
|
|||||||
@@ -13,6 +13,11 @@ enum class InflateStatus {
|
|||||||
|
|
||||||
// Streaming deflate decompressor wrapping uzlib.
|
// Streaming deflate decompressor wrapping uzlib.
|
||||||
//
|
//
|
||||||
|
// NOTE: retained ONLY for FontDecompressor's tiny one-shot flash-resident group
|
||||||
|
// decompressions, where uzlib's ~1KB state beats tinfl's ~11KB on the
|
||||||
|
// OOM-sensitive render path. All throughput paths (zip entries, PNG IDAT) use
|
||||||
|
// InflateStream (lib/miniz), which decodes several times faster.
|
||||||
|
//
|
||||||
// Two modes:
|
// Two modes:
|
||||||
// init(false) — one-shot: input is a contiguous buffer, call read() once.
|
// init(false) — one-shot: input is a contiguous buffer, call read() once.
|
||||||
// init(true) — streaming: allocates a 32KB ring buffer for back-references
|
// init(true) — streaming: allocates a 32KB ring buffer for back-references
|
||||||
|
|||||||
@@ -278,13 +278,18 @@ class XPathParagraphResolver final : public Print {
|
|||||||
path.push_back({name, siblingIndex});
|
path.push_back({name, siblingIndex});
|
||||||
parentStates.emplace_back();
|
parentStates.emplace_back();
|
||||||
|
|
||||||
|
// Count both <p> and <li> as paragraph-like positions, matching how the section
|
||||||
|
// layout tracks them (xpathParagraphIndex and xpathListItemIndex). This ensures
|
||||||
|
// KOReader progress in list items maps to the correct XPath.
|
||||||
if (name == "p") {
|
if (name == "p") {
|
||||||
paragraphCount++;
|
paragraphCount++;
|
||||||
if (paragraphCount == targetParagraph) {
|
} else if (name == "li") {
|
||||||
xpath = buildParagraphXPath(spineIndex, path, 0, 0);
|
paragraphCount++;
|
||||||
stopped = true;
|
}
|
||||||
XML_StopParser(parser, XML_FALSE);
|
if (paragraphCount == targetParagraph) {
|
||||||
}
|
xpath = buildParagraphXPath(spineIndex, path, 0, 0);
|
||||||
|
stopped = true;
|
||||||
|
XML_StopParser(parser, XML_FALSE);
|
||||||
}
|
}
|
||||||
|
|
||||||
depth++;
|
depth++;
|
||||||
|
|||||||
@@ -1,118 +1,45 @@
|
|||||||
#include "KOReaderCredentialStore.h"
|
#include "KOReaderCredentialStore.h"
|
||||||
|
|
||||||
#include <HalStorage.h>
|
|
||||||
#include <Logging.h>
|
#include <Logging.h>
|
||||||
#include <MD5Builder.h>
|
#include <MD5Builder.h>
|
||||||
#include <ObfuscationUtils.h>
|
#include <ObfuscationUtils.h>
|
||||||
#include <Serialization.h>
|
|
||||||
|
|
||||||
#include "KOReaderJsonIO.h"
|
|
||||||
|
|
||||||
// Initialize the static instance
|
|
||||||
KOReaderCredentialStore KOReaderCredentialStore::instance;
|
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
// File format version (for binary migration)
|
|
||||||
constexpr uint8_t KOREADER_FILE_VERSION = 1;
|
|
||||||
|
|
||||||
// File paths
|
|
||||||
constexpr char KOREADER_FILE_BIN[] = "/.crosspoint/koreader.bin";
|
|
||||||
constexpr char KOREADER_FILE_JSON[] = "/.crosspoint/koreader.json";
|
|
||||||
constexpr char KOREADER_FILE_BAK[] = "/.crosspoint/koreader.bin.bak";
|
|
||||||
|
|
||||||
// Default sync server URL
|
// Default sync server URL
|
||||||
constexpr char DEFAULT_SERVER_URL[] = "https://sync.koreader.rocks:443";
|
constexpr char DEFAULT_SERVER_URL[] = "https://sync.koreader.rocks:443";
|
||||||
|
|
||||||
// Legacy obfuscation key - "KOReader" in ASCII (only used for binary migration)
|
|
||||||
constexpr uint8_t LEGACY_OBFUSCATION_KEY[] = {0x4B, 0x4F, 0x52, 0x65, 0x61, 0x64, 0x65, 0x72};
|
|
||||||
constexpr size_t LEGACY_KEY_LENGTH = sizeof(LEGACY_OBFUSCATION_KEY);
|
|
||||||
|
|
||||||
void legacyDeobfuscate(std::string& data) {
|
|
||||||
for (size_t i = 0; i < data.size(); i++) {
|
|
||||||
data[i] ^= LEGACY_OBFUSCATION_KEY[i % LEGACY_KEY_LENGTH];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
bool KOReaderCredentialStore::saveToFile() const {
|
void KOReaderCredentialStore::toJson(JsonDocument& doc) const {
|
||||||
Storage.mkdir("/.crosspoint");
|
doc["username"] = getUsername();
|
||||||
return KOReaderJsonIO::save(*this, KOREADER_FILE_JSON);
|
doc["password_obf"] = obfuscation::obfuscateToBase64(getPassword());
|
||||||
|
doc["serverUrl"] = getServerUrl();
|
||||||
|
doc["matchMethod"] = static_cast<uint8_t>(getMatchMethod());
|
||||||
|
doc["sendMetadata"] = getSendMetadata();
|
||||||
}
|
}
|
||||||
|
|
||||||
bool KOReaderCredentialStore::loadFromFile() {
|
bool KOReaderCredentialStore::fromJson(JsonVariantConst doc) {
|
||||||
// Try JSON first
|
std::string user = doc["username"] | "";
|
||||||
if (Storage.exists(KOREADER_FILE_JSON)) {
|
|
||||||
String json = Storage.readFile(KOREADER_FILE_JSON);
|
|
||||||
if (!json.isEmpty()) {
|
|
||||||
bool resave = false;
|
|
||||||
bool result = KOReaderJsonIO::load(*this, json.c_str(), &resave);
|
|
||||||
if (result && resave) {
|
|
||||||
saveToFile();
|
|
||||||
LOG_DBG("KRS", "Resaved KOReader credentials to update format");
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fall back to binary migration
|
bool needsResave = false;
|
||||||
if (Storage.exists(KOREADER_FILE_BIN)) {
|
std::string pass = extractPassword(doc, needsResave);
|
||||||
if (loadFromBinaryFile()) {
|
|
||||||
if (saveToFile()) {
|
|
||||||
Storage.rename(KOREADER_FILE_BIN, KOREADER_FILE_BAK);
|
|
||||||
LOG_DBG("KRS", "Migrated koreader.bin to koreader.json");
|
|
||||||
return true;
|
|
||||||
} else {
|
|
||||||
LOG_ERR("KRS", "Failed to save KOReader credentials during migration");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
LOG_DBG("KRS", "No credentials file found");
|
setCredentials(user, pass);
|
||||||
return false;
|
setServerUrl(doc["serverUrl"] | "");
|
||||||
}
|
|
||||||
|
|
||||||
bool KOReaderCredentialStore::loadFromBinaryFile() {
|
uint8_t method = doc["matchMethod"] | (uint8_t)0;
|
||||||
HalFile file;
|
if (method <= static_cast<uint8_t>(DocumentMatchMethod::BINARY)) {
|
||||||
if (!Storage.openFileForRead("KRS", KOREADER_FILE_BIN, file)) {
|
setMatchMethod(static_cast<DocumentMatchMethod>(method));
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
uint8_t version;
|
|
||||||
serialization::readPod(file, version);
|
|
||||||
if (version != KOREADER_FILE_VERSION) {
|
|
||||||
LOG_DBG("KRS", "Unknown file version: %u", version);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (file.available()) {
|
|
||||||
serialization::readString(file, username);
|
|
||||||
} else {
|
} else {
|
||||||
username.clear();
|
LOG_DBG("KRS", "Invalid matchMethod %u in JSON, resetting to FILENAME", method);
|
||||||
|
setMatchMethod(DocumentMatchMethod::FILENAME);
|
||||||
|
}
|
||||||
|
setSendMetadata(doc["sendMetadata"] | false);
|
||||||
|
|
||||||
|
if (needsResave) {
|
||||||
|
LOG_DBG("KRS", "Resaved KOReader credentials to update format");
|
||||||
|
saveToFile();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (file.available()) {
|
|
||||||
serialization::readString(file, password);
|
|
||||||
legacyDeobfuscate(password);
|
|
||||||
} else {
|
|
||||||
password.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (file.available()) {
|
|
||||||
serialization::readString(file, serverUrl);
|
|
||||||
} else {
|
|
||||||
serverUrl.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (file.available()) {
|
|
||||||
uint8_t method;
|
|
||||||
serialization::readPod(file, method);
|
|
||||||
matchMethod = static_cast<DocumentMatchMethod>(method);
|
|
||||||
} else {
|
|
||||||
matchMethod = DocumentMatchMethod::FILENAME;
|
|
||||||
}
|
|
||||||
|
|
||||||
LOG_DBG("KRS", "Loaded KOReader credentials from binary for user: %s", username.c_str());
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -173,3 +100,8 @@ void KOReaderCredentialStore::setMatchMethod(DocumentMatchMethod method) {
|
|||||||
matchMethod = method;
|
matchMethod = method;
|
||||||
LOG_DBG("KRS", "Set match method: %s", method == DocumentMatchMethod::FILENAME ? "Filename" : "Binary");
|
LOG_DBG("KRS", "Set match method: %s", method == DocumentMatchMethod::FILENAME ? "Filename" : "Binary");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void KOReaderCredentialStore::setSendMetadata(bool enabled) {
|
||||||
|
sendMetadata = enabled;
|
||||||
|
LOG_DBG("KRS", "Set send metadata: %s", enabled ? "true" : "false");
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
#include <ArduinoJson.h>
|
||||||
|
#include <PersistableStore.h>
|
||||||
|
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
|
||||||
@@ -14,30 +17,25 @@ enum class DocumentMatchMethod : uint8_t {
|
|||||||
* and base64-encoded before writing to JSON (not cryptographically secure,
|
* and base64-encoded before writing to JSON (not cryptographically secure,
|
||||||
* but prevents casual reading and ties credentials to the specific device).
|
* but prevents casual reading and ties credentials to the specific device).
|
||||||
*/
|
*/
|
||||||
class KOReaderCredentialStore {
|
|
||||||
|
class KOReaderCredentialStore : public PersistableStore<KOReaderCredentialStore> {
|
||||||
private:
|
private:
|
||||||
static KOReaderCredentialStore instance;
|
|
||||||
std::string username;
|
std::string username;
|
||||||
std::string password;
|
std::string password;
|
||||||
std::string serverUrl; // Custom sync server URL (empty = default)
|
std::string serverUrl; // Custom sync server URL (empty = default)
|
||||||
DocumentMatchMethod matchMethod = DocumentMatchMethod::FILENAME; // Default to filename for compatibility
|
DocumentMatchMethod matchMethod = DocumentMatchMethod::FILENAME; // Default to filename for compatibility
|
||||||
|
bool sendMetadata = false; // Send document metadata with progress sync
|
||||||
|
|
||||||
// Private constructor for singleton
|
// Private constructor for singleton
|
||||||
KOReaderCredentialStore() = default;
|
KOReaderCredentialStore() = default;
|
||||||
|
~KOReaderCredentialStore() = default;
|
||||||
|
|
||||||
bool loadFromBinaryFile();
|
friend class PersistableStore<KOReaderCredentialStore>;
|
||||||
|
|
||||||
public:
|
public:
|
||||||
// Delete copy constructor and assignment
|
static const char* getFilePath() { return "/.crosspoint/koreader.json"; }
|
||||||
KOReaderCredentialStore(const KOReaderCredentialStore&) = delete;
|
void toJson(JsonDocument& doc) const;
|
||||||
KOReaderCredentialStore& operator=(const KOReaderCredentialStore&) = delete;
|
bool fromJson(JsonVariantConst doc);
|
||||||
|
|
||||||
// Get singleton instance
|
|
||||||
static KOReaderCredentialStore& getInstance() { return instance; }
|
|
||||||
|
|
||||||
// Save/load from SD card
|
|
||||||
bool saveToFile() const;
|
|
||||||
bool loadFromFile();
|
|
||||||
|
|
||||||
// Credential management
|
// Credential management
|
||||||
void setCredentials(const std::string& user, const std::string& pass);
|
void setCredentials(const std::string& user, const std::string& pass);
|
||||||
@@ -63,6 +61,10 @@ class KOReaderCredentialStore {
|
|||||||
// Document matching method
|
// Document matching method
|
||||||
void setMatchMethod(DocumentMatchMethod method);
|
void setMatchMethod(DocumentMatchMethod method);
|
||||||
DocumentMatchMethod getMatchMethod() const { return matchMethod; }
|
DocumentMatchMethod getMatchMethod() const { return matchMethod; }
|
||||||
|
|
||||||
|
// Send metadata setting
|
||||||
|
void setSendMetadata(bool enabled);
|
||||||
|
bool getSendMetadata() const { return sendMetadata; }
|
||||||
};
|
};
|
||||||
|
|
||||||
// Helper macro to access credential store
|
// Helper macro to access credential store
|
||||||
|
|||||||
@@ -1,51 +0,0 @@
|
|||||||
#include "KOReaderJsonIO.h"
|
|
||||||
|
|
||||||
#include <ArduinoJson.h>
|
|
||||||
#include <HalStorage.h>
|
|
||||||
#include <Logging.h>
|
|
||||||
#include <ObfuscationUtils.h>
|
|
||||||
|
|
||||||
#include "KOReaderCredentialStore.h"
|
|
||||||
|
|
||||||
namespace KOReaderJsonIO {
|
|
||||||
|
|
||||||
bool save(const KOReaderCredentialStore& store, const char* path) {
|
|
||||||
JsonDocument doc;
|
|
||||||
doc["username"] = store.getUsername();
|
|
||||||
doc["password_obf"] = obfuscation::obfuscateToBase64(store.getPassword());
|
|
||||||
doc["serverUrl"] = store.getServerUrl();
|
|
||||||
doc["matchMethod"] = static_cast<uint8_t>(store.getMatchMethod());
|
|
||||||
|
|
||||||
String json;
|
|
||||||
serializeJson(doc, json);
|
|
||||||
return Storage.writeFile(path, json);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool load(KOReaderCredentialStore& store, const char* json, bool* needsResave) {
|
|
||||||
if (needsResave) *needsResave = false;
|
|
||||||
JsonDocument doc;
|
|
||||||
auto error = deserializeJson(doc, json);
|
|
||||||
if (error) {
|
|
||||||
LOG_ERR("KRS", "JSON parse error: %s", error.c_str());
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
std::string user = doc["username"] | std::string("");
|
|
||||||
|
|
||||||
bool ok = false;
|
|
||||||
std::string pass = obfuscation::deobfuscateFromBase64(doc["password_obf"] | "", &ok);
|
|
||||||
if (!ok || pass.empty()) {
|
|
||||||
pass = doc["password"] | std::string("");
|
|
||||||
if (!pass.empty() && needsResave) *needsResave = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
store.setCredentials(user, pass);
|
|
||||||
store.setServerUrl(doc["serverUrl"] | std::string(""));
|
|
||||||
|
|
||||||
uint8_t method = doc["matchMethod"] | (uint8_t)0;
|
|
||||||
store.setMatchMethod(static_cast<DocumentMatchMethod>(method));
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace KOReaderJsonIO
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
class KOReaderCredentialStore;
|
|
||||||
|
|
||||||
namespace KOReaderJsonIO {
|
|
||||||
bool save(const KOReaderCredentialStore& store, const char* path);
|
|
||||||
bool load(KOReaderCredentialStore& store, const char* json, bool* needsResave);
|
|
||||||
} // namespace KOReaderJsonIO
|
|
||||||
@@ -2,10 +2,10 @@
|
|||||||
|
|
||||||
#include <ArduinoJson.h>
|
#include <ArduinoJson.h>
|
||||||
#include <Logging.h>
|
#include <Logging.h>
|
||||||
#include <esp_crt_bundle.h>
|
#include <SecureHttpClient.h>
|
||||||
#include <esp_http_client.h>
|
#include <base64.h>
|
||||||
|
|
||||||
#include <ctime>
|
#include <string>
|
||||||
|
|
||||||
#include "KOReaderCredentialStore.h"
|
#include "KOReaderCredentialStore.h"
|
||||||
|
|
||||||
@@ -16,82 +16,35 @@ namespace {
|
|||||||
constexpr char DEVICE_NAME[] = "CrossPoint";
|
constexpr char DEVICE_NAME[] = "CrossPoint";
|
||||||
constexpr char DEVICE_ID[] = "crosspoint-reader";
|
constexpr char DEVICE_ID[] = "crosspoint-reader";
|
||||||
|
|
||||||
// Small TLS buffers to fit in ESP32-C3's limited heap (~46KB free after WiFi).
|
// KOSync's TLS-1.3 servers can't be reached through the precompiled system
|
||||||
// KOSync payloads are tiny JSON (<1KB), so 2KB buffers are sufficient.
|
// mbedTLS (TLS 1.3 is stubbed out), so requests run over wolfSSL via
|
||||||
// Default 16KB buffers cause OOM during TLS handshake.
|
// SecureHttpClient. The handshake still needs working heap; gate on it. wolfSSL's
|
||||||
constexpr int HTTP_BUF_SIZE = 2048;
|
// footprint is smaller than mbedTLS's old ~48KB peak, but keep a conservative
|
||||||
|
// floor. Check both total free heap and largest contiguous block so fragmented
|
||||||
// Cloudflare tunnels send a 3-cert Google Trust Services chain. During the TLS handshake
|
// heap does not fall through into a failed TLS allocation path.
|
||||||
// mbedTLS makes many small allocations that collectively consume ~48KB of heap. With only
|
|
||||||
// ~50KB free after WiFi connects, the session drove min-free-ever down to 2600 bytes before
|
|
||||||
// failing with MBEDTLS_ERR_X509_ALLOC_FAILED (-0x2880). Check total free heap (not max
|
|
||||||
// contiguous block) because the failure mode is aggregate exhaustion, not one large alloc.
|
|
||||||
constexpr uint32_t MIN_HEAP_FOR_TLS = 55000;
|
constexpr uint32_t MIN_HEAP_FOR_TLS = 55000;
|
||||||
|
|
||||||
// Response buffer for reading HTTP body
|
// Apply the shared KOSync auth headers after begin(). x-auth-* is the native
|
||||||
struct ResponseBuffer {
|
// KOSync scheme; Basic auth is added for Calibre-Web-Automated compatibility.
|
||||||
char* data = nullptr;
|
void applyAuthHeaders(freeink::SecureHttpClient& http) {
|
||||||
int len = 0;
|
http.addHeader("Accept", "application/vnd.koreader.v1+json");
|
||||||
int capacity = 0;
|
http.addHeader("x-auth-user", KOREADER_STORE.getUsername());
|
||||||
|
http.addHeader("x-auth-key", KOREADER_STORE.getMd5Password());
|
||||||
~ResponseBuffer() { free(data); }
|
const std::string credentials = KOREADER_STORE.getUsername() + ":" + KOREADER_STORE.getPassword();
|
||||||
|
const String encoded = base64::encode(credentials.c_str());
|
||||||
bool ensure(int size) {
|
http.addHeader("Authorization", std::string("Basic ") + encoded.c_str());
|
||||||
if (size <= capacity) return true;
|
|
||||||
char* newData = (char*)realloc(data, size);
|
|
||||||
if (!newData) return false;
|
|
||||||
data = newData;
|
|
||||||
capacity = size;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// HTTP event handler to collect response body
|
|
||||||
esp_err_t httpEventHandler(esp_http_client_event_t* evt) {
|
|
||||||
auto* buf = static_cast<ResponseBuffer*>(evt->user_data);
|
|
||||||
if (evt->event_id == HTTP_EVENT_ON_DATA && buf) {
|
|
||||||
if (buf->ensure(buf->len + evt->data_len + 1)) {
|
|
||||||
memcpy(buf->data + buf->len, evt->data, evt->data_len);
|
|
||||||
buf->len += evt->data_len;
|
|
||||||
buf->data[buf->len] = '\0';
|
|
||||||
} else {
|
|
||||||
LOG_ERR("KOSync", "Response buffer allocation failed (%d bytes)", evt->data_len);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return ESP_OK;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create configured esp_http_client with small TLS buffers
|
// True when free heap is too low to risk a TLS handshake.
|
||||||
esp_http_client_handle_t createClient(const char* url, ResponseBuffer* buf,
|
bool insufficientHeap() {
|
||||||
esp_http_client_method_t method = HTTP_METHOD_GET) {
|
const uint32_t freeHeap = ESP.getFreeHeap();
|
||||||
esp_http_client_config_t config = {};
|
const uint32_t maxAllocHeap = ESP.getMaxAllocHeap();
|
||||||
config.url = url;
|
if (freeHeap < MIN_HEAP_FOR_TLS || maxAllocHeap < MIN_HEAP_FOR_TLS) {
|
||||||
config.event_handler = httpEventHandler;
|
LOG_ERR("KOSync", "Insufficient heap for TLS handshake: %u bytes free, %u max alloc (need %u)", freeHeap,
|
||||||
config.user_data = buf;
|
maxAllocHeap, MIN_HEAP_FOR_TLS);
|
||||||
config.method = method;
|
return true;
|
||||||
config.timeout_ms = 15000;
|
|
||||||
config.buffer_size = HTTP_BUF_SIZE;
|
|
||||||
config.buffer_size_tx = HTTP_BUF_SIZE;
|
|
||||||
config.crt_bundle_attach = esp_crt_bundle_attach;
|
|
||||||
|
|
||||||
// HTTP Basic Auth for Calibre-Web-Automated compatibility
|
|
||||||
config.username = KOREADER_STORE.getUsername().c_str();
|
|
||||||
config.password = KOREADER_STORE.getPassword().c_str();
|
|
||||||
config.auth_type = HTTP_AUTH_TYPE_BASIC;
|
|
||||||
|
|
||||||
esp_http_client_handle_t client = esp_http_client_init(&config);
|
|
||||||
if (!client) return nullptr;
|
|
||||||
|
|
||||||
// KOSync auth headers
|
|
||||||
if (esp_http_client_set_header(client, "Accept", "application/vnd.koreader.v1+json") != ESP_OK ||
|
|
||||||
esp_http_client_set_header(client, "x-auth-user", KOREADER_STORE.getUsername().c_str()) != ESP_OK ||
|
|
||||||
esp_http_client_set_header(client, "x-auth-key", KOREADER_STORE.getMd5Password().c_str()) != ESP_OK) {
|
|
||||||
LOG_ERR("KOSync", "Failed to set auth headers");
|
|
||||||
esp_http_client_cleanup(client);
|
|
||||||
return nullptr;
|
|
||||||
}
|
}
|
||||||
|
return false;
|
||||||
return client;
|
|
||||||
}
|
}
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
@@ -102,26 +55,24 @@ KOReaderSyncClient::Error KOReaderSyncClient::authenticate() {
|
|||||||
return NO_CREDENTIALS;
|
return NO_CREDENTIALS;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string url = KOREADER_STORE.getBaseUrl() + "/users/auth";
|
const std::string url = KOREADER_STORE.getBaseUrl() + "/users/auth";
|
||||||
const uint32_t freeHeap = ESP.getFreeHeap();
|
LOG_DBG("KOSync", "Authenticating: %s (heap: %u)", url.c_str(), (unsigned)ESP.getFreeHeap());
|
||||||
LOG_DBG("KOSync", "Authenticating: %s (heap: %u)", url.c_str(), (unsigned)freeHeap);
|
if (insufficientHeap()) return LOW_MEMORY;
|
||||||
if (freeHeap < MIN_HEAP_FOR_TLS) {
|
|
||||||
LOG_ERR("KOSync", "Insufficient heap for TLS handshake: %u bytes free (need %u)", freeHeap, MIN_HEAP_FOR_TLS);
|
freeink::SecureHttpClient http;
|
||||||
return LOW_MEMORY;
|
http.setInsecure();
|
||||||
|
if (!http.begin(url)) {
|
||||||
|
LOG_ERR("KOSync", "Bad URL: %s", url.c_str());
|
||||||
|
return NETWORK_ERROR;
|
||||||
}
|
}
|
||||||
|
applyAuthHeaders(http);
|
||||||
ResponseBuffer buf;
|
const int httpCode = http.GET();
|
||||||
esp_http_client_handle_t client = createClient(url.c_str(), &buf);
|
http.end();
|
||||||
if (!client) return NETWORK_ERROR;
|
|
||||||
|
|
||||||
esp_err_t err = esp_http_client_perform(client);
|
|
||||||
const int httpCode = esp_http_client_get_status_code(client);
|
|
||||||
lastHttpCode = httpCode;
|
lastHttpCode = httpCode;
|
||||||
esp_http_client_cleanup(client);
|
|
||||||
|
|
||||||
LOG_DBG("KOSync", "Auth response: %d (err: %d)", httpCode, err);
|
LOG_DBG("KOSync", "Auth response: %d", httpCode);
|
||||||
|
|
||||||
if (err != ESP_OK) return NETWORK_ERROR;
|
if (httpCode <= 0) return NETWORK_ERROR;
|
||||||
if (httpCode == 200) return OK;
|
if (httpCode == 200) return OK;
|
||||||
if (httpCode == 401) return AUTH_FAILED;
|
if (httpCode == 401) return AUTH_FAILED;
|
||||||
return SERVER_ERROR;
|
return SERVER_ERROR;
|
||||||
@@ -135,30 +86,31 @@ KOReaderSyncClient::Error KOReaderSyncClient::getProgress(const std::string& doc
|
|||||||
return NO_CREDENTIALS;
|
return NO_CREDENTIALS;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string url = KOREADER_STORE.getBaseUrl() + "/syncs/progress/" + documentHash;
|
const std::string url = KOREADER_STORE.getBaseUrl() + "/syncs/progress/" + documentHash;
|
||||||
const uint32_t freeHeap = ESP.getFreeHeap();
|
LOG_DBG("KOSync", "Getting progress: %s (heap: %u)", url.c_str(), (unsigned)ESP.getFreeHeap());
|
||||||
LOG_DBG("KOSync", "Getting progress: %s (heap: %u)", url.c_str(), (unsigned)freeHeap);
|
if (insufficientHeap()) return LOW_MEMORY;
|
||||||
if (freeHeap < MIN_HEAP_FOR_TLS) {
|
|
||||||
LOG_ERR("KOSync", "Insufficient heap for TLS handshake: %u bytes free (need %u)", freeHeap, MIN_HEAP_FOR_TLS);
|
freeink::SecureHttpClient http;
|
||||||
return LOW_MEMORY;
|
http.setInsecure();
|
||||||
|
if (!http.begin(url)) {
|
||||||
|
LOG_ERR("KOSync", "Bad URL: %s", url.c_str());
|
||||||
|
return NETWORK_ERROR;
|
||||||
|
}
|
||||||
|
applyAuthHeaders(http);
|
||||||
|
const int httpCode = http.GET();
|
||||||
|
lastHttpCode = httpCode;
|
||||||
|
|
||||||
|
LOG_DBG("KOSync", "Get progress response: %d", httpCode);
|
||||||
|
|
||||||
|
if (httpCode <= 0) {
|
||||||
|
http.end();
|
||||||
|
return NETWORK_ERROR;
|
||||||
}
|
}
|
||||||
|
|
||||||
ResponseBuffer buf;
|
if (httpCode == 200) {
|
||||||
esp_http_client_handle_t client = createClient(url.c_str(), &buf);
|
|
||||||
if (!client) return NETWORK_ERROR;
|
|
||||||
|
|
||||||
esp_err_t err = esp_http_client_perform(client);
|
|
||||||
const int httpCode = esp_http_client_get_status_code(client);
|
|
||||||
lastHttpCode = httpCode;
|
|
||||||
esp_http_client_cleanup(client);
|
|
||||||
|
|
||||||
LOG_DBG("KOSync", "Get progress response: %d (err: %d)", httpCode, err);
|
|
||||||
|
|
||||||
if (err != ESP_OK) return NETWORK_ERROR;
|
|
||||||
|
|
||||||
if (httpCode == 200 && buf.data) {
|
|
||||||
JsonDocument doc;
|
JsonDocument doc;
|
||||||
const DeserializationError error = deserializeJson(doc, buf.data);
|
const DeserializationError error = deserializeJson(doc, http.getString().c_str());
|
||||||
|
http.end();
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
LOG_ERR("KOSync", "JSON parse failed: %s", error.c_str());
|
LOG_ERR("KOSync", "JSON parse failed: %s", error.c_str());
|
||||||
@@ -176,6 +128,7 @@ KOReaderSyncClient::Error KOReaderSyncClient::getProgress(const std::string& doc
|
|||||||
return OK;
|
return OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
http.end();
|
||||||
if (httpCode == 401) return AUTH_FAILED;
|
if (httpCode == 401) return AUTH_FAILED;
|
||||||
if (httpCode == 404) return NOT_FOUND;
|
if (httpCode == 404) return NOT_FOUND;
|
||||||
return SERVER_ERROR;
|
return SERVER_ERROR;
|
||||||
@@ -188,17 +141,19 @@ KOReaderSyncClient::Error KOReaderSyncClient::updateProgress(const KOReaderProgr
|
|||||||
return NO_CREDENTIALS;
|
return NO_CREDENTIALS;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string url = KOREADER_STORE.getBaseUrl() + "/syncs/progress";
|
const std::string url = KOREADER_STORE.getBaseUrl() + "/syncs/progress";
|
||||||
const uint32_t freeHeap = ESP.getFreeHeap();
|
LOG_DBG("KOSync", "Updating progress: %s (heap: %u)", url.c_str(), (unsigned)ESP.getFreeHeap());
|
||||||
LOG_DBG("KOSync", "Updating progress: %s (heap: %u)", url.c_str(), (unsigned)freeHeap);
|
if (insufficientHeap()) return LOW_MEMORY;
|
||||||
if (freeHeap < MIN_HEAP_FOR_TLS) {
|
|
||||||
LOG_ERR("KOSync", "Insufficient heap for TLS handshake: %u bytes free (need %u)", freeHeap, MIN_HEAP_FOR_TLS);
|
|
||||||
return LOW_MEMORY;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build JSON body
|
// Build JSON body
|
||||||
JsonDocument doc;
|
JsonDocument doc;
|
||||||
doc["document"] = progress.document;
|
doc["document"] = progress.document;
|
||||||
|
if (progress.metadata.has_value()) {
|
||||||
|
auto meta = doc["metadata"].to<JsonObject>();
|
||||||
|
meta["filename"] = progress.metadata->filename;
|
||||||
|
meta["title"] = progress.metadata->title;
|
||||||
|
meta["authors"] = progress.metadata->authors;
|
||||||
|
}
|
||||||
doc["progress"] = progress.progress;
|
doc["progress"] = progress.progress;
|
||||||
doc["percentage"] = progress.percentage;
|
doc["percentage"] = progress.percentage;
|
||||||
doc["device"] = DEVICE_NAME;
|
doc["device"] = DEVICE_NAME;
|
||||||
@@ -209,25 +164,21 @@ KOReaderSyncClient::Error KOReaderSyncClient::updateProgress(const KOReaderProgr
|
|||||||
|
|
||||||
LOG_DBG("KOSync", "Request body: %s", body.c_str());
|
LOG_DBG("KOSync", "Request body: %s", body.c_str());
|
||||||
|
|
||||||
ResponseBuffer buf;
|
freeink::SecureHttpClient http;
|
||||||
esp_http_client_handle_t client = createClient(url.c_str(), &buf, HTTP_METHOD_PUT);
|
http.setInsecure();
|
||||||
if (!client) return NETWORK_ERROR;
|
if (!http.begin(url)) {
|
||||||
|
LOG_ERR("KOSync", "Bad URL: %s", url.c_str());
|
||||||
if (esp_http_client_set_header(client, "Content-Type", "application/json") != ESP_OK ||
|
|
||||||
esp_http_client_set_post_field(client, body.c_str(), body.length()) != ESP_OK) {
|
|
||||||
LOG_ERR("KOSync", "Failed to set request body");
|
|
||||||
esp_http_client_cleanup(client);
|
|
||||||
return NETWORK_ERROR;
|
return NETWORK_ERROR;
|
||||||
}
|
}
|
||||||
|
applyAuthHeaders(http);
|
||||||
esp_err_t err = esp_http_client_perform(client);
|
http.addHeader("Content-Type", "application/json");
|
||||||
const int httpCode = esp_http_client_get_status_code(client);
|
const int httpCode = http.sendRequest("PUT", body);
|
||||||
|
http.end();
|
||||||
lastHttpCode = httpCode;
|
lastHttpCode = httpCode;
|
||||||
esp_http_client_cleanup(client);
|
|
||||||
|
|
||||||
LOG_DBG("KOSync", "Update progress response: %d (err: %d)", httpCode, err);
|
LOG_DBG("KOSync", "Update progress response: %d", httpCode);
|
||||||
|
|
||||||
if (err != ESP_OK) return NETWORK_ERROR;
|
if (httpCode <= 0) return NETWORK_ERROR;
|
||||||
if (httpCode == 200 || httpCode == 202) return OK;
|
if (httpCode == 200 || httpCode == 202) return OK;
|
||||||
if (httpCode == 401) return AUTH_FAILED;
|
if (httpCode == 401) return AUTH_FAILED;
|
||||||
return SERVER_ERROR;
|
return SERVER_ERROR;
|
||||||
|
|||||||
@@ -1,16 +1,30 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
#include <cstdint>
|
||||||
|
#include <optional>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Optional document metadata sent alongside progress sync requests.
|
||||||
|
* Mirrors the metadata object added in KOReader PR #15306.
|
||||||
|
* The official sync server ignores this field; custom servers may use it.
|
||||||
|
*/
|
||||||
|
struct KOReaderMetadata {
|
||||||
|
std::string filename; // e.g. "my_book.epub"
|
||||||
|
std::string title; // Document title from EPUB metadata
|
||||||
|
std::string authors; // Author(s) from EPUB metadata
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Progress data from KOReader sync server.
|
* Progress data from KOReader sync server.
|
||||||
*/
|
*/
|
||||||
struct KOReaderProgress {
|
struct KOReaderProgress {
|
||||||
std::string document; // Document hash
|
std::string document; // Document hash
|
||||||
std::string progress; // XPath-like progress string
|
std::string progress; // XPath-like progress string
|
||||||
float percentage; // Progress percentage (0.0 to 1.0)
|
float percentage; // Progress percentage (0.0 to 1.0)
|
||||||
std::string device; // Device name
|
std::string device; // Device name
|
||||||
std::string deviceId; // Device ID
|
std::string deviceId; // Device ID
|
||||||
int64_t timestamp; // Unix timestamp of last update
|
int64_t timestamp; // Unix timestamp of last update
|
||||||
|
std::optional<KOReaderMetadata> metadata; // Optional document metadata
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -709,12 +709,13 @@ SavedProgressPosition ProgressMapper::toSavedProgress(const std::shared_ptr<Epub
|
|||||||
float intra =
|
float intra =
|
||||||
(pos.totalPages > 1) ? static_cast<float>(pos.pageNumber) / static_cast<float>(pos.totalPages - 1) : 0.0f;
|
(pos.totalPages > 1) ? static_cast<float>(pos.pageNumber) / static_cast<float>(pos.totalPages - 1) : 0.0f;
|
||||||
result.percentage = epub->calculateProgress(pos.spineIndex, intra);
|
result.percentage = epub->calculateProgress(pos.spineIndex, intra);
|
||||||
// Progress-based XPath correctly handles both <p> and <li> positions.
|
if (pos.hasParagraphIndex && pos.paragraphIndex > 0) {
|
||||||
result.xpath = ChapterXPathResolver::findXPathForProgress(epub, pos.spineIndex, intra);
|
|
||||||
// Fall back to paragraph-index lookup when progress-based resolution fails.
|
|
||||||
if (result.xpath.empty() && pos.hasParagraphIndex && pos.paragraphIndex > 0) {
|
|
||||||
result.xpath = ChapterXPathResolver::findXPathForParagraph(epub, pos.spineIndex, pos.paragraphIndex);
|
result.xpath = ChapterXPathResolver::findXPathForParagraph(epub, pos.spineIndex, pos.paragraphIndex);
|
||||||
}
|
}
|
||||||
|
// Fall back to progress-based XPath, then synthetic progress mapping.
|
||||||
|
if (result.xpath.empty()) {
|
||||||
|
result.xpath = ChapterXPathResolver::findXPathForProgress(epub, pos.spineIndex, intra);
|
||||||
|
}
|
||||||
if (result.xpath.empty()) {
|
if (result.xpath.empty()) {
|
||||||
result.xpath = generateXPath(epub, pos.spineIndex, intra);
|
result.xpath = generateXPath(epub, pos.spineIndex, intra);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,51 @@
|
|||||||
|
#include "BuildScratch.h"
|
||||||
|
|
||||||
|
#include <Logging.h>
|
||||||
|
|
||||||
|
#include <atomic>
|
||||||
|
|
||||||
|
namespace buildscratch {
|
||||||
|
namespace {
|
||||||
|
uint8_t* block = nullptr;
|
||||||
|
size_t blockLen = 0;
|
||||||
|
// atomic exchange so an opportunistic claim from another task can never
|
||||||
|
// double-hand-out the block (single core, but FreeRTOS preempts).
|
||||||
|
std::atomic<bool> claimed{false};
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
void lend(uint8_t* buf, const size_t len) {
|
||||||
|
if (block) {
|
||||||
|
LOG_ERR("SCR", "Build scratch lent twice; ignoring second lend");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
block = buf;
|
||||||
|
blockLen = len;
|
||||||
|
claimed.store(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
void reclaim() {
|
||||||
|
if (claimed.load()) {
|
||||||
|
// A consumer still holds the block. The storage stays valid (it is the
|
||||||
|
// framebuffer allocation, never freed) but its contents are about to be
|
||||||
|
// clobbered; the consumer's output will be garbage. Loud log so a
|
||||||
|
// lifetime bug is visible instead of a silent corrupt decode.
|
||||||
|
LOG_ERR("SCR", "Build scratch reclaimed while still claimed");
|
||||||
|
}
|
||||||
|
block = nullptr;
|
||||||
|
blockLen = 0;
|
||||||
|
claimed.store(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t* claim(const size_t minLen, size_t* lenOut) {
|
||||||
|
if (!block || blockLen < minLen) return nullptr;
|
||||||
|
bool expected = false;
|
||||||
|
if (!claimed.compare_exchange_strong(expected, true)) return nullptr;
|
||||||
|
if (lenOut) *lenOut = blockLen;
|
||||||
|
return block;
|
||||||
|
}
|
||||||
|
|
||||||
|
void release(const uint8_t* p) {
|
||||||
|
if (p && p == block) claimed.store(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace buildscratch
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <cstddef>
|
||||||
|
#include <cstdint>
|
||||||
|
|
||||||
|
// Registry for the framebuffer bytes lent out during a build phase
|
||||||
|
// (GfxRenderer::FrameBufferLoan). The lender (GfxRenderer) deposits the block
|
||||||
|
// with lend()/reclaim(); a memory-hungry consumer (e.g. InflateStream's ~43KB
|
||||||
|
// tinfl state + window) may claim() it instead of allocating from the heap.
|
||||||
|
//
|
||||||
|
// Exactly one claimant at a time; claim() returns nullptr when the block is
|
||||||
|
// absent or already claimed, and consumers must fall back to the heap. The
|
||||||
|
// underlying storage is the framebuffer allocation itself, which is never
|
||||||
|
// freed -- so even the pathological case (reclaim() while still claimed, which
|
||||||
|
// logs an error) reads garbage, never freed memory.
|
||||||
|
namespace buildscratch {
|
||||||
|
|
||||||
|
// Lender side (GfxRenderer only).
|
||||||
|
void lend(uint8_t* buf, size_t len);
|
||||||
|
void reclaim();
|
||||||
|
|
||||||
|
// Consumer side: exclusive claim of the whole block if it is at least minLen
|
||||||
|
// bytes; nullptr means "use the heap". Release with the same pointer.
|
||||||
|
uint8_t* claim(size_t minLen, size_t* lenOut = nullptr);
|
||||||
|
void release(const uint8_t* p);
|
||||||
|
|
||||||
|
} // namespace buildscratch
|
||||||
@@ -11,6 +11,13 @@ extern "C" {
|
|||||||
#include <Utf8.h>
|
#include <Utf8.h>
|
||||||
|
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
|
#include <mutex>
|
||||||
|
|
||||||
|
// Guards the static bidi_char buffers in applyBidiVisual() and
|
||||||
|
// computeVisualWordOrder(). The bidi+shaping pipeline is not reentrant;
|
||||||
|
// this mutex serialises access so multi-core callers don't corrupt each
|
||||||
|
// other's intermediate state.
|
||||||
|
static std::mutex bidiMutex;
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
@@ -68,11 +75,25 @@ int detectParagraphLevel(const char* utf8, const int fallbackLevel, const int ma
|
|||||||
return fallbackLevel & 1;
|
return fallbackLevel & 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool isTransparentMark(const uint32_t cp) {
|
||||||
|
// RTL-script combining marks: Hebrew niqqud/cantillation and Arabic
|
||||||
|
// harakat/Quranic annotation. Transparent for Arabic joining (do_shape
|
||||||
|
// skips them), zero-advance for measurement, and rendered as overlays on
|
||||||
|
// the preceding base glyph when the active font carries their glyphs.
|
||||||
|
// The cp >= 0x0591 guard keeps Latin combining marks (U+0300-U+036F, also
|
||||||
|
// NSM) on their existing utf8IsCombiningMark() rendering path.
|
||||||
|
return cp >= 0x0591 && bidi_class(cp) == NSM;
|
||||||
|
}
|
||||||
|
|
||||||
bool applyBidiVisual(const char* utf8, std::string& out, int paragraphLevel) {
|
bool applyBidiVisual(const char* utf8, std::string& out, int paragraphLevel) {
|
||||||
if (!utf8 || !*utf8) return false;
|
if (!utf8 || !*utf8) return false;
|
||||||
|
const std::lock_guard<std::mutex> lock(bidiMutex);
|
||||||
|
|
||||||
static bidi_char line[BIDI_MAX_LINE];
|
static bidi_char line[BIDI_MAX_LINE];
|
||||||
|
static bidi_char shaped[BIDI_MAX_LINE];
|
||||||
int count = 0;
|
int count = 0;
|
||||||
|
int lastBase = -1; // last non-formatter character (mintty's ibase)
|
||||||
|
uint8_t pendingJoiners = 0; // ZWJ/ZWNJ seen since lastBase
|
||||||
auto* p = reinterpret_cast<const unsigned char*>(utf8);
|
auto* p = reinterpret_cast<const unsigned char*>(utf8);
|
||||||
while (*p) {
|
while (*p) {
|
||||||
if (count >= BIDI_MAX_LINE) {
|
if (count >= BIDI_MAX_LINE) {
|
||||||
@@ -84,18 +105,66 @@ bool applyBidiVisual(const char* utf8, std::string& out, int paragraphLevel) {
|
|||||||
if (!cp || cp == REPLACEMENT_GLYPH) break;
|
if (!cp || cp == REPLACEMENT_GLYPH) break;
|
||||||
line[count].origwc = line[count].wc = cp;
|
line[count].origwc = line[count].wc = cp;
|
||||||
line[count].index = static_cast<uint16_t>(count);
|
line[count].index = static_cast<uint16_t>(count);
|
||||||
|
line[count].joiners = 0;
|
||||||
|
|
||||||
|
// Flag Arabic joining formatters mintty-style (termline.c): the ZWJ/ZWNJ
|
||||||
|
// goes into the low nibble of the character it follows and the high
|
||||||
|
// nibble of the character it precedes. Flags are assigned in logical
|
||||||
|
// order here; do_shape() reads them after reordering.
|
||||||
|
if (cp == 0x200C || cp == 0x200D) {
|
||||||
|
const uint8_t joiner = (cp == 0x200D) ? ZWJ : ZWNJ;
|
||||||
|
if (lastBase >= 0) line[lastBase].joiners |= joiner;
|
||||||
|
pendingJoiners |= joiner;
|
||||||
|
} else {
|
||||||
|
line[count].joiners = pendingJoiners << 4;
|
||||||
|
pendingJoiners = 0;
|
||||||
|
lastBase = count;
|
||||||
|
}
|
||||||
count++;
|
count++;
|
||||||
}
|
}
|
||||||
if (!count) return false;
|
if (!count) return false;
|
||||||
|
|
||||||
const bool autodir = (paragraphLevel < 0);
|
const bool autodir = (paragraphLevel < 0);
|
||||||
const int level = autodir ? 0 : (paragraphLevel & 1);
|
const int level = autodir ? 0 : (paragraphLevel & 1);
|
||||||
|
|
||||||
|
// Order matters (mintty does the same): do_bidi() first to obtain visual
|
||||||
|
// order, then do_shape() — contextual forms are resolved from *visual*
|
||||||
|
// adjacency, and shaping presentation forms must never be reordered.
|
||||||
do_bidi(autodir, level, line, count);
|
do_bidi(autodir, level, line, count);
|
||||||
|
do_shape(line, shaped, count);
|
||||||
|
|
||||||
out.clear();
|
out.clear();
|
||||||
out.reserve(std::strlen(utf8));
|
out.reserve(std::strlen(utf8));
|
||||||
|
// Lam-Alef collapse sentinel and zero-width joining formatters have done
|
||||||
|
// their job during shaping and have no glyphs to render.
|
||||||
|
const auto filtered = [](const uint32_t cp) { return cp == LIGATURE_PLACEHOLDER || cp == 0x200C || cp == 0x200D; };
|
||||||
for (int i = 0; i < count; i++) {
|
for (int i = 0; i < count; i++) {
|
||||||
utf8AppendCodepoint(line[i].wc, out);
|
const uint32_t cp = shaped[i].wc;
|
||||||
|
if (filtered(cp)) continue;
|
||||||
|
if (!isTransparentMark(cp)) {
|
||||||
|
utf8AppendCodepoint(cp, out);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// UAX#9 rule L3: reversing an RTL run leaves combining marks *before*
|
||||||
|
// their base character. The renderer overlays a mark on the most
|
||||||
|
// recently drawn glyph, so emit the base first, then its marks in
|
||||||
|
// logical order. `index` is the original logical position: a base
|
||||||
|
// following its marks with a *lower* index means the run was reversed.
|
||||||
|
int j = i; // [i, j) = the run of marks (and filtered entries)
|
||||||
|
while (j < count && (filtered(shaped[j].wc) || isTransparentMark(shaped[j].wc))) j++;
|
||||||
|
if (j < count && shaped[j].index < shaped[i].index) {
|
||||||
|
utf8AppendCodepoint(shaped[j].wc, out);
|
||||||
|
for (int k = j - 1; k >= i; k--) {
|
||||||
|
if (isTransparentMark(shaped[k].wc)) utf8AppendCodepoint(shaped[k].wc, out);
|
||||||
|
}
|
||||||
|
i = j; // base already emitted
|
||||||
|
} else {
|
||||||
|
// Unreversed (or trailing, base-less) marks already follow their base.
|
||||||
|
for (int k = i; k < j; k++) {
|
||||||
|
if (isTransparentMark(shaped[k].wc)) utf8AppendCodepoint(shaped[k].wc, out);
|
||||||
|
}
|
||||||
|
i = j - 1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -105,6 +174,7 @@ bool computeVisualWordOrder(const std::vector<std::string>& words, bool paragrap
|
|||||||
visualOrder.clear();
|
visualOrder.clear();
|
||||||
const size_t nWords = words.size();
|
const size_t nWords = words.size();
|
||||||
if (nWords <= 1 || nWords > BIDI_MAX_LINE) return false;
|
if (nWords <= 1 || nWords > BIDI_MAX_LINE) return false;
|
||||||
|
const std::lock_guard<std::mutex> lock(bidiMutex);
|
||||||
|
|
||||||
static bidi_char line[BIDI_MAX_LINE];
|
static bidi_char line[BIDI_MAX_LINE];
|
||||||
int count = 0;
|
int count = 0;
|
||||||
@@ -121,6 +191,7 @@ bool computeVisualWordOrder(const std::vector<std::string>& words, bool paragrap
|
|||||||
if (!cp || cp == REPLACEMENT_GLYPH) break;
|
if (!cp || cp == REPLACEMENT_GLYPH) break;
|
||||||
line[count].origwc = line[count].wc = cp;
|
line[count].origwc = line[count].wc = cp;
|
||||||
line[count].index = static_cast<uint16_t>(w);
|
line[count].index = static_cast<uint16_t>(w);
|
||||||
|
line[count].joiners = 0;
|
||||||
count++;
|
count++;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -131,6 +202,7 @@ bool computeVisualWordOrder(const std::vector<std::string>& words, bool paragrap
|
|||||||
}
|
}
|
||||||
line[count].origwc = line[count].wc = ' ';
|
line[count].origwc = line[count].wc = ' ';
|
||||||
line[count].index = static_cast<uint16_t>(nWords);
|
line[count].index = static_cast<uint16_t>(nWords);
|
||||||
|
line[count].joiners = 0;
|
||||||
count++;
|
count++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,14 @@ bool startsWithRtl(const char* utf8, int maxStrongChars = RTL_PARAGRAPH_PROBE_DE
|
|||||||
|
|
||||||
int detectParagraphLevel(const char* utf8, int fallbackLevel = 0, int maxStrongChars = 64);
|
int detectParagraphLevel(const char* utf8, int fallbackLevel = 0, int maxStrongChars = 64);
|
||||||
|
|
||||||
|
// True for RTL-script non-spacing marks (Hebrew niqqud/cantillation, Arabic
|
||||||
|
// harakat and Quranic annotation): zero-width for measurement, transparent for
|
||||||
|
// Arabic joining, and rendered as overlays on the preceding base glyph when
|
||||||
|
// the active font carries their glyphs (SD fonts; built-in fonts don't).
|
||||||
|
// Latin combining marks (U+0300-U+036F) intentionally return false — they are
|
||||||
|
// handled by the utf8IsCombiningMark() rendering path.
|
||||||
|
bool isTransparentMark(uint32_t cp);
|
||||||
|
|
||||||
// paragraphLevel: -1 = auto-detect, 0 = LTR, 1 = RTL
|
// paragraphLevel: -1 = auto-detect, 0 = LTR, 1 = RTL
|
||||||
bool applyBidiVisual(const char* utf8, std::string& out, int paragraphLevel = -1);
|
bool applyBidiVisual(const char* utf8, std::string& out, int paragraphLevel = -1);
|
||||||
|
|
||||||
|
|||||||
@@ -1,15 +1,19 @@
|
|||||||
/* bidiclasses.t — bidi class table for CrossPoint Hebrew/English epub.
|
/* bidiclasses.t — bidi class table for CrossPoint RTL (Hebrew/Arabic) epub.
|
||||||
*
|
*
|
||||||
* Coverage rationale:
|
* Coverage rationale:
|
||||||
* Hebrew + English is the primary target. However, CrossPoint renders
|
* Hebrew and Arabic-script languages (Arabic, Farsi, Urdu, Sindhi, Pashto,
|
||||||
* Latin and Cyrillic scripts for many other languages, so these MUST be
|
* Kurdish) are the RTL targets. CrossPoint also renders Latin and Cyrillic
|
||||||
* classified as L (not fall through to ON) to avoid regression when they
|
* scripts for many other languages, so these MUST be classified as L (not
|
||||||
* appear adjacent to Hebrew runs.
|
* fall through to ON) to avoid regression when they appear adjacent to
|
||||||
|
* RTL runs.
|
||||||
*
|
*
|
||||||
* Scripts NOT in this table fall through to ON — correct per UAX#9 for
|
* Scripts NOT in this table fall through to ON — correct per UAX#9 for
|
||||||
* scripts CrossPoint's fonts don't support (CJK, Arabic, Devanagari, etc.)
|
* scripts CrossPoint's fonts don't support (CJK, Devanagari, etc.)
|
||||||
* ON is the right class for "unknown" — it behaves neutrally.
|
* ON is the right class for "unknown" — it behaves neutrally.
|
||||||
*
|
*
|
||||||
|
* Arabic ranges are sourced from Unicode UCD extracted/DerivedBidiClass.txt
|
||||||
|
* (values verified against Unicode 17.0.0).
|
||||||
|
*
|
||||||
* Entries sorted ascending by first (binary search requirement).
|
* Entries sorted ascending by first (binary search requirement).
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@@ -84,6 +88,36 @@
|
|||||||
{0x05D0, 0x05EA, R}, /* alef … tav */
|
{0x05D0, 0x05EA, R}, /* alef … tav */
|
||||||
{0x05F0, 0x05F4, R}, /* alternative forms + geresh/gershayim */
|
{0x05F0, 0x05F4, R}, /* alternative forms + geresh/gershayim */
|
||||||
|
|
||||||
|
/* ── Arabic / Perso-Arabic (DerivedBidiClass.txt) ───────────────────── */
|
||||||
|
/* Letters are AL (Arabic Letter), harakat/marks are NSM, Arabic-Indic
|
||||||
|
digits are AN, extended (Farsi/Urdu) digits are EN.
|
||||||
|
0x0606-0x0607, 0x060E-0x060F, 0x06DE, 0x06E9 are ON — fall through. */
|
||||||
|
{0x0600, 0x0605, AN}, /* Arabic number signs (Cf) */
|
||||||
|
{0x0608, 0x0608, AL}, /* Arabic ray */
|
||||||
|
{0x0609, 0x060A, ET}, /* per mille / per ten thousand */
|
||||||
|
{0x060B, 0x060B, AL}, /* afghani sign */
|
||||||
|
{0x060C, 0x060C, CS}, /* Arabic comma */
|
||||||
|
{0x060D, 0x060D, AL}, /* Arabic date separator */
|
||||||
|
{0x0610, 0x061A, NSM}, /* honorifics + small marks */
|
||||||
|
{0x061B, 0x061F, AL}, /* semicolon, ALM, end-of-text, hamza mark, question mark */
|
||||||
|
{0x0620, 0x064A, AL}, /* core Arabic letters + tatweel */
|
||||||
|
{0x064B, 0x065F, NSM}, /* harakat (fathatan … wavy hamza below) */
|
||||||
|
{0x0660, 0x0669, AN}, /* Arabic-Indic digits ٠-٩ */
|
||||||
|
{0x066A, 0x066A, ET}, /* Arabic percent sign */
|
||||||
|
{0x066B, 0x066C, AN}, /* decimal / thousands separators */
|
||||||
|
{0x066D, 0x066F, AL}, /* five-pointed star, dotless beh/qaf */
|
||||||
|
{0x0670, 0x0670, NSM}, /* superscript alef */
|
||||||
|
{0x0671, 0x06D5, AL}, /* extended letters: Farsi, Urdu, Sindhi, Pashto, Kurdish */
|
||||||
|
{0x06D6, 0x06DC, NSM}, /* Quranic annotation marks */
|
||||||
|
{0x06DD, 0x06DD, AN}, /* end of ayah */
|
||||||
|
{0x06DF, 0x06E4, NSM},
|
||||||
|
{0x06E5, 0x06E6, AL}, /* small waw / small yeh */
|
||||||
|
{0x06E7, 0x06E8, NSM},
|
||||||
|
{0x06EA, 0x06ED, NSM},
|
||||||
|
{0x06EE, 0x06EF, AL}, /* dal/reh with inverted V */
|
||||||
|
{0x06F0, 0x06F9, EN}, /* extended Arabic-Indic digits ۰-۹ (Farsi/Urdu) — EN per UCD */
|
||||||
|
{0x06FA, 0x06FF, AL},
|
||||||
|
|
||||||
/* ── Latin Extended Additional (L) ─────────────────────────────────── */
|
/* ── Latin Extended Additional (L) ─────────────────────────────────── */
|
||||||
/* Covers accented chars for Vietnamese, Welsh, Romanian, etc.
|
/* Covers accented chars for Vietnamese, Welsh, Romanian, etc.
|
||||||
Not currently rendered by CrossPoint fonts, but costs only 2 table rows. */
|
Not currently rendered by CrossPoint fonts, but costs only 2 table rows. */
|
||||||
@@ -110,6 +144,14 @@
|
|||||||
{0x2069, 0x2069, PDI},
|
{0x2069, 0x2069, PDI},
|
||||||
{0x206A, 0x206F, BN},
|
{0x206A, 0x206F, BN},
|
||||||
|
|
||||||
|
/* ── Arabic presentation forms (output of do_shape()) ───────────────── */
|
||||||
|
/* Contextual/ligature forms emitted by the shaper must classify as AL so
|
||||||
|
a reshaped line still resolves RTL. Ranges per DerivedBidiClass.txt;
|
||||||
|
0xFBC3-0xFBD2 are ON — fall through. */
|
||||||
|
{0xFB50, 0xFBC2, AL}, /* Presentation Forms-A: Perso-Arabic contextual forms */
|
||||||
|
{0xFBD3, 0xFBFF, AL}, /* Presentation Forms-A: NG … Farsi Yeh forms */
|
||||||
|
{0xFE70, 0xFE74, AL}, /* Presentation Forms-B: harakat isolated forms */
|
||||||
|
{0xFE76, 0xFEFC, AL}, /* Presentation Forms-B: contextual forms + Lam-Alef ligatures */
|
||||||
|
|
||||||
/* ── Byte Order Mark ────────────────────────────────────────────────── */
|
/* ── Byte Order Mark ────────────────────────────────────────────────── */
|
||||||
{0xFEFF, 0xFEFF, BN},
|
{0xFEFF, 0xFEFF, BN},
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user