Compare commits
34
Commits
cecdbefa0e
...
a86a644adc
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a86a644adc | ||
|
|
02398c8a96 | ||
|
|
ba92cb902e | ||
|
|
4d19d38152 | ||
|
|
302c0771dd | ||
|
|
c1a396c1ba | ||
|
|
078d9ef535 | ||
|
|
17e4230068 | ||
|
|
34e7ed249b | ||
|
|
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 |
@@ -47,6 +47,7 @@ git ls-files --exclude-standard ${GIT_LS_FILES_FLAGS} \
|
||||
| grep -v -E '^lib/EpdFont/builtinFonts/' \
|
||||
| grep -v -E '^lib/Epub/Epub/hyphenation/generated/' \
|
||||
| grep -v -E '^lib/uzlib/' \
|
||||
| grep -v -E '^lib/miniz/third_party/' \
|
||||
| xargs -r "${CLANG_FORMAT_BIN}" -style=file -i
|
||||
# Restore strict pipeline failure handling for the rest of the script.
|
||||
set -o pipefail
|
||||
|
||||
@@ -90,13 +90,19 @@ if (parsedSize != fileSize) {
|
||||
|
||||
## `section.bin`
|
||||
|
||||
### Version 29
|
||||
### Version 30
|
||||
|
||||
Each file in `sections/*.bin` stores one laid-out spine section. The header is
|
||||
also the cache-busting key: if any layout-affecting setting differs from the
|
||||
current reader settings, the section is discarded and rebuilt.
|
||||
|
||||
Version 29 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,
|
||||
image rendering mode, and Focus Reading
|
||||
@@ -119,7 +125,7 @@ import std.mem;
|
||||
import std.string;
|
||||
import std.core;
|
||||
|
||||
#define EXPECTED_VERSION 29
|
||||
#define EXPECTED_VERSION 30
|
||||
#define MAX_STRING_LENGTH 65535
|
||||
#define FOOTNOTE_NUMBER_LEN 32
|
||||
#define FOOTNOTE_HREF_LEN 96
|
||||
|
||||
+8
-3
@@ -32,9 +32,14 @@ networks or in hotspot mode when you control who is connected.
|
||||
## Join Network Mode
|
||||
|
||||
1. Select **Join Network**.
|
||||
2. Pick a 2.4 GHz Wi-Fi network from the scan results.
|
||||
3. Enter the password if prompted.
|
||||
4. Save credentials if you want the reader to reconnect automatically next time.
|
||||
2. If you have saved Wi-Fi credentials, CrossPoint first tries the last
|
||||
connected network, then other visible saved networks in signal-strength
|
||||
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:
|
||||
|
||||
|
||||
+1
-1
Submodule freeink-sdk updated: 2442e1d672...421d75d011
@@ -44,16 +44,17 @@ void EpdFont::getTextBounds(const char* string, const int startX, const int star
|
||||
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) {
|
||||
const auto kernFP = getKerning(prevCp, cp); // 4.4 fixed-point kern
|
||||
lastBaseX += fp4::toPixel(prevAdvanceFP + kernFP);
|
||||
}
|
||||
|
||||
const int glyphBaseX =
|
||||
isCombining ? combiningMark::centerOver(lastBaseX, lastBaseLeft, lastBaseWidth, glyph->left, glyph->width)
|
||||
: lastBaseX;
|
||||
const int glyphBaseX = isCombining ? combiningMark::anchorOver(anchor, lastBaseX, lastBaseLeft, lastBaseWidth,
|
||||
glyph->left, glyph->width)
|
||||
: lastBaseX;
|
||||
const int glyphBaseY = startY - raiseBy;
|
||||
|
||||
*minX = std::min(*minX, glyphBaseX + glyph->left);
|
||||
|
||||
+76
-11
@@ -36,24 +36,71 @@ namespace combiningMark {
|
||||
|
||||
constexpr int MIN_GAP_PX = 1;
|
||||
|
||||
/// Compute the cursor-X at which to render a combining mark so its bitmap
|
||||
/// is visually centered over the base glyph's bitmap.
|
||||
constexpr int centerOver(int baseCursorPos, int baseLeft, int baseWidth, int markLeft, int markWidth) {
|
||||
return baseCursorPos + baseLeft + baseWidth / 2 - markWidth / 2 - markLeft;
|
||||
/// Placement of a mark relative to its base glyph. The default heuristic —
|
||||
/// centered over the base, raised clear of its top — suits Latin diacritics
|
||||
/// and Arabic harakat, but misplaces the Hebrew niqqud whose identity depends
|
||||
/// 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
|
||||
/// every left/width term inverts sign.
|
||||
constexpr int centerOverRotated90CW(int baseCursorPos, int baseLeft, int baseWidth, int markLeft, int markWidth) {
|
||||
return baseCursorPos - baseLeft - baseWidth / 2 + markWidth / 2 + markLeft;
|
||||
constexpr int anchorOverRotated90CW(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;
|
||||
}
|
||||
|
||||
/// 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
|
||||
/// 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).
|
||||
constexpr int raiseAboveBase(int markTop, int markHeight, int baseTop) {
|
||||
/// below the baseline (e.g. cedilla, dot-below, ogonek) and for anchors that
|
||||
/// 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;
|
||||
const int gap = markTop - markHeight - baseTop;
|
||||
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
|
||||
|
||||
/// 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:
|
||||
/// advanceX: 12.4 unsigned fixed-point in uint16_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.
|
||||
/// Class IDs are 1-based; codepoints not in the table have implicit class 0 (no kerning).
|
||||
EPD_PACKED_BEGIN
|
||||
typedef struct {
|
||||
uint16_t codepoint; ///< Unicode codepoint
|
||||
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.
|
||||
/// `pair` encodes (leftCodepoint << 16 | rightCodepoint) for single-key lookup.
|
||||
EPD_PACKED_BEGIN
|
||||
typedef struct {
|
||||
uint32_t pair; ///< Packed codepoint pair (left << 16 | right)
|
||||
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
|
||||
typedef struct {
|
||||
|
||||
@@ -369,6 +369,11 @@ int FontDecompressor::prewarmCache(const EpdFontData* fontData, const char* utf8
|
||||
}
|
||||
stats.pageBufferBytes += totalBytes;
|
||||
stats.pageGlyphsBytes += glyphCount * sizeof(PageGlyphEntry);
|
||||
// MEMFIX-PORT: page-slot address landmark for the heap map; portable
|
||||
// Landmark for the heap block map: page slots are the largest flash-font
|
||||
// allocations and otherwise show up as anonymous ~4-20 KB used blocks.
|
||||
LOG_DBG("FDC", "page slot buffer=%p bytes=%u glyphs=%u", static_cast<void*>(slot.buffer), (unsigned)totalBytes,
|
||||
(unsigned)glyphCount);
|
||||
|
||||
slot.fontData = fontData;
|
||||
slot.glyphCount = glyphCount;
|
||||
|
||||
@@ -1222,7 +1222,8 @@ int SdCardFont::fetchAdvancesForCodepoints(uint32_t* codepoints, uint32_t cpCoun
|
||||
}
|
||||
|
||||
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;
|
||||
styleMask = resolveStyleMask(styleMask);
|
||||
if (styleMask == 0) return 0;
|
||||
@@ -1242,6 +1243,9 @@ int SdCardFont::buildAdvanceTableRange(Iter begin, Iter end, bool includeSpace,
|
||||
for (auto it = begin; it != end && !hitCap; ++it) {
|
||||
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 == ' '; }))
|
||||
codepoints[cpCount++] = ' ';
|
||||
@@ -1259,12 +1263,13 @@ int SdCardFont::buildAdvanceTableRange(Iter begin, Iter end, bool includeSpace,
|
||||
return totalMissed;
|
||||
}
|
||||
|
||||
int SdCardFont::buildAdvanceTable(const char* utf8Text, uint8_t styleMask) {
|
||||
return buildAdvanceTableRange(&utf8Text, &utf8Text + 1, false, false, styleMask);
|
||||
int SdCardFont::buildAdvanceTable(const char* utf8Text, uint8_t styleMask, const char* extraText) {
|
||||
return buildAdvanceTableRange(&utf8Text, &utf8Text + 1, false, false, styleMask, extraText);
|
||||
}
|
||||
|
||||
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);
|
||||
int SdCardFont::buildAdvanceTable(const std::vector<std::string>& words, bool includeHyphen, uint8_t styleMask,
|
||||
const char* extraText) {
|
||||
return buildAdvanceTableRange(words.begin(), words.end(), words.size() > 1, includeHyphen, styleMask, extraText);
|
||||
}
|
||||
|
||||
// --- Stats ---
|
||||
@@ -1401,6 +1406,33 @@ const EpdGlyph* SdCardFont::onGlyphMiss(void* ctx, uint32_t codepoint) {
|
||||
return &self->overflow_[slot].glyph;
|
||||
}
|
||||
|
||||
size_t SdCardFont::reportMemory() const {
|
||||
size_t total = 0;
|
||||
for (uint8_t si = 0; si < MAX_STYLES; ++si) {
|
||||
const auto& s = styles_[si];
|
||||
if (!s.present) continue;
|
||||
size_t fixed = 0; // loaded once per family: interval/kern/lig tables
|
||||
if (s.fullIntervals) fixed += s.header.intervalCount * sizeof(EpdUnicodeInterval);
|
||||
if (s.bmpIntervals) fixed += s.header.intervalCount * sizeof(PerStyle::BmpInterval16);
|
||||
if (s.kernLeftClasses) fixed += s.header.kernLeftEntryCount * sizeof(EpdKernClassEntry);
|
||||
if (s.kernRightClasses) fixed += s.header.kernRightEntryCount * sizeof(EpdKernClassEntry);
|
||||
if (s.ligaturePairs) fixed += s.header.ligaturePairCount * sizeof(EpdLigaturePair);
|
||||
// kept-if-fits mini arenas: capacity (not count) is what stays resident
|
||||
size_t mini = s.miniIntervalCapacity * sizeof(EpdUnicodeInterval) + s.miniGlyphCapacity * sizeof(EpdGlyph) +
|
||||
s.miniBitmapCapacity + s.miniKernLeftCapacity * sizeof(EpdKernClassEntry) +
|
||||
s.miniKernRightCapacity * sizeof(EpdKernClassEntry) + s.miniKernMatrixCapacity;
|
||||
const size_t adv = advanceTableSize_[si] * sizeof(AdvanceEntry);
|
||||
LOG_DBG("SDCF", "mem style%u: fixed=%u mini=%u adv=%u", si, (unsigned)fixed, (unsigned)mini, (unsigned)adv);
|
||||
total += fixed + mini + adv;
|
||||
}
|
||||
size_t overflowBytes = 0;
|
||||
for (uint32_t i = 0; i < overflowCount_; ++i) {
|
||||
if (overflow_[i].bitmap) overflowBytes += overflow_[i].glyph.dataLength;
|
||||
}
|
||||
total += overflowBytes + overflowCount_ * sizeof(OverflowEntry);
|
||||
return total;
|
||||
}
|
||||
|
||||
bool SdCardFont::isOverflowGlyph(const EpdGlyph* glyph) const {
|
||||
for (uint32_t i = 0; i < overflowCount_; i++) {
|
||||
if (&overflow_[i].glyph == glyph) return true;
|
||||
|
||||
@@ -47,9 +47,12 @@ class SdCardFont {
|
||||
// Build a compact advance-only table for layout measurement.
|
||||
// Extracts ALL unique codepoints from words (no MAX_PAGE_GLYPHS cap),
|
||||
// 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.
|
||||
int buildAdvanceTable(const char* utf8Text, uint8_t styleMask = 0x0F);
|
||||
int buildAdvanceTable(const std::vector<std::string>& words, bool includeHyphen, 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,
|
||||
const char* extraText = nullptr);
|
||||
|
||||
// Look up advanceX for a codepoint from the advance table.
|
||||
// Returns the 12.4 fixed-point advance, or 0 if not found.
|
||||
@@ -101,6 +104,11 @@ class SdCardFont {
|
||||
uint32_t uniqueGlyphs = 0;
|
||||
uint32_t bitmapBytes = 0;
|
||||
};
|
||||
// MEMFIX-PORT: SD font resident-bytes audit; portable
|
||||
// Log per-style resident heap (full tables + kept-if-fits mini arenas +
|
||||
// advance tables + overflow bitmaps) and return the total in bytes. Pure
|
||||
// accounting — no allocation, no state change.
|
||||
size_t reportMemory() const;
|
||||
void logStats(const char* label = "SDCF");
|
||||
void resetStats();
|
||||
const Stats& getStats() const { return stats_; }
|
||||
@@ -140,11 +148,13 @@ class SdCardFont {
|
||||
|
||||
// Full intervals loaded from file (kept in RAM for codepoint lookup)
|
||||
EpdUnicodeInterval* fullIntervals = nullptr;
|
||||
EPD_PACKED_BEGIN
|
||||
struct BmpInterval16 {
|
||||
uint16_t first;
|
||||
uint16_t last;
|
||||
uint16_t offset;
|
||||
} __attribute__((packed));
|
||||
} EPD_PACKED_ATTR;
|
||||
EPD_PACKED_END
|
||||
static_assert(sizeof(BmpInterval16) == 6, "BmpInterval16 must remain compact");
|
||||
BmpInterval16* bmpIntervals = nullptr;
|
||||
bool intervalsAreBmp16 = false;
|
||||
@@ -262,7 +272,8 @@ class SdCardFont {
|
||||
int32_t findGlobalGlyphIndex(const PerStyle& s, uint32_t codepoint) const;
|
||||
int fetchAdvancesForCodepoints(uint32_t* codepoints, uint32_t cpCount, uint8_t styleMask);
|
||||
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);
|
||||
|
||||
// Global helpers
|
||||
|
||||
@@ -88,6 +88,14 @@ void SdCardFontManager::unloadAll(GfxRenderer& renderer) {
|
||||
loadedPointSize_ = 0;
|
||||
}
|
||||
|
||||
size_t SdCardFontManager::reportMemory() const {
|
||||
size_t total = 0;
|
||||
for (const auto& lf : loaded_) {
|
||||
if (lf.font) total += lf.font->reportMemory();
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
||||
int SdCardFontManager::getFontId(const std::string& familyName) const {
|
||||
if (familyName != loadedFamilyName_ || loaded_.empty()) return 0;
|
||||
return loaded_.front().fontId;
|
||||
|
||||
@@ -32,6 +32,10 @@ class SdCardFontManager {
|
||||
// Get name of currently loaded family (empty if none).
|
||||
const std::string& currentFamilyName() const { return loadedFamilyName_; };
|
||||
|
||||
// MEMFIX-PORT: font manager audit passthrough; portable
|
||||
// Sum of loaded fonts' resident heap (see SdCardFont::reportMemory).
|
||||
size_t reportMemory() const;
|
||||
|
||||
// Point size that was actually loaded.
|
||||
// 0 if nothing loaded.
|
||||
uint8_t currentPointSize() const { return loadedPointSize_; };
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -6,6 +6,8 @@
|
||||
!NotoSerif/**
|
||||
!NotoSans/
|
||||
!NotoSans/**
|
||||
!NotoSansArabic/
|
||||
!NotoSansArabic/**
|
||||
!NotoSansHebrew/
|
||||
!NotoSansHebrew/**
|
||||
!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
@@ -31,19 +31,48 @@ done
|
||||
UI_FONT_SIZES=(10 12)
|
||||
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 style in ${UI_FONT_STYLES[@]}; do
|
||||
font_name="ubuntu_${size}_$(echo $style | tr '[:upper:]' '[:lower:]')"
|
||||
font_path="../builtinFonts/source/Ubuntu/Ubuntu-${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
|
||||
# Vietnamese tone marks. Append a Vietnamese-only Ubuntu cut so those glyphs
|
||||
# are filled from it while every glyph Ubuntu already has stays unchanged
|
||||
# (fontstack is ordered by descending priority).
|
||||
viet_path="../builtinFonts/source/Ubuntu/Ubuntu-Vietnamese-${style}.ttf"
|
||||
output_path="../builtinFonts/${font_name}.h"
|
||||
python fontconvert.py $font_name $size $font_path $hebrew_path $viet_path \
|
||||
--additional-intervals 0x05D0,0x05EA > $output_path
|
||||
python fontconvert.py $font_name $size $font_path $hebrew_path $arabic_path $viet_path \
|
||||
--additional-intervals 0x05D0,0x05EA "${ARABIC_INTERVALS[@]}" > $output_path
|
||||
echo "Generated $output_path"
|
||||
done
|
||||
done
|
||||
@@ -51,7 +80,8 @@ done
|
||||
python fontconvert.py notosans_8_regular 8 \
|
||||
../builtinFonts/source/NotoSans/NotoSans-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 "Running compression verification..."
|
||||
|
||||
+19
-59
@@ -153,18 +153,11 @@ bool Epub::parseTocNcxFile() const {
|
||||
|
||||
LOG_DBG("EBP", "Parsing toc ncx file: %s", tocNcxItem.c_str());
|
||||
|
||||
const auto tmpNcxPath = getCachePath() + "/toc.ncx";
|
||||
HalFile tempNcxFile;
|
||||
if (!Storage.openFileForWrite("EBP", tmpNcxPath, tempNcxFile)) {
|
||||
size_t ncxSize;
|
||||
if (!getItemSize(tocNcxItem, &ncxSize)) {
|
||||
LOG_ERR("EBP", "Could not get size of toc ncx file");
|
||||
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());
|
||||
|
||||
@@ -173,29 +166,13 @@ bool Epub::parseTocNcxFile() const {
|
||||
return false;
|
||||
}
|
||||
|
||||
const auto ncxBuffer = static_cast<uint8_t*>(malloc(1024));
|
||||
if (!ncxBuffer) {
|
||||
LOG_ERR("EBP", "Could not allocate memory for toc ncx parser");
|
||||
// Stream the decompressed NCX straight into the parser instead of round-tripping
|
||||
// through a temp file on the SD card (decompress -> write -> reopen -> reread -> delete).
|
||||
if (!readItemContentsToStream(tocNcxItem, ncxParser, 1024)) {
|
||||
LOG_ERR("EBP", "Could not read toc ncx file");
|
||||
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");
|
||||
return true;
|
||||
}
|
||||
@@ -209,18 +186,11 @@ bool Epub::parseTocNavFile() const {
|
||||
|
||||
LOG_DBG("EBP", "Parsing toc nav file: %s", tocNavItem.c_str());
|
||||
|
||||
const auto tmpNavPath = getCachePath() + "/toc.nav";
|
||||
HalFile tempNavFile;
|
||||
if (!Storage.openFileForWrite("EBP", tmpNavPath, tempNavFile)) {
|
||||
size_t navSize;
|
||||
if (!getItemSize(tocNavItem, &navSize)) {
|
||||
LOG_ERR("EBP", "Could not get size of toc nav file");
|
||||
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
|
||||
// and the HTMLX nav file will have hrefs relative to itself
|
||||
@@ -232,28 +202,13 @@ bool Epub::parseTocNavFile() const {
|
||||
return false;
|
||||
}
|
||||
|
||||
const auto navBuffer = static_cast<uint8_t*>(malloc(1024));
|
||||
if (!navBuffer) {
|
||||
LOG_ERR("EBP", "Could not allocate memory for toc nav parser");
|
||||
// Stream the decompressed nav document straight into the parser instead of round-tripping
|
||||
// through a temp file on the SD card (decompress -> write -> reopen -> reread -> delete).
|
||||
if (!readItemContentsToStream(tocNavItem, navParser, 1024)) {
|
||||
LOG_ERR("EBP", "Could not read toc nav file");
|
||||
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");
|
||||
return true;
|
||||
}
|
||||
@@ -396,6 +351,11 @@ bool Epub::load(const bool buildIfMissing, const bool skipLoadingCss) {
|
||||
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());
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -44,6 +44,18 @@ class Epub {
|
||||
}
|
||||
~Epub() = default;
|
||||
std::string& getBasePath() { return contentBasePath; }
|
||||
// MEMFIX-PORT: epub resident-bytes audit accessor; portable
|
||||
// Approximate resident heap of the open book (audit): path strings, the CSS
|
||||
// file list, and the parsed stylesheet. BookMetadataCache is file-backed
|
||||
// (counts + HalFile handles) and contributes little.
|
||||
size_t residentBytes() const {
|
||||
size_t total = sizeof(Epub) + tocNcxItem.capacity() + tocNavItem.capacity() + filepath.capacity() +
|
||||
contentBasePath.capacity() + cachePath.capacity();
|
||||
for (const auto& f : cssFiles) total += sizeof(f) + (f.capacity() > 15 ? f.capacity() : 0);
|
||||
if (cssParser) total += cssParser->residentBytes();
|
||||
return total;
|
||||
}
|
||||
size_t cssRuleCount() const { return cssParser ? cssParser->ruleCount() : 0; }
|
||||
bool load(bool buildIfMissing = true, bool skipLoadingCss = false);
|
||||
bool clearCache() const;
|
||||
void setupCacheDir() const;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#include "BookMetadataCache.h"
|
||||
|
||||
#include <BufferedFile.h>
|
||||
#include <Logging.h>
|
||||
#include <Serialization.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 tmpSpineBinFile[] = "/spine.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
|
||||
|
||||
/* ============= WRITING / BUILDING FUNCTIONS ================ */
|
||||
@@ -30,13 +77,23 @@ bool BookMetadataCache::beginContentOpfPass() {
|
||||
LOG_DBG("BMC", "Beginning content opf pass");
|
||||
|
||||
// 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() {
|
||||
const bool flushed = !passOut || passOut->flush();
|
||||
passOut.reset();
|
||||
// Explicit close() required: member variable persists beyond function scope
|
||||
spineFile.close();
|
||||
return true;
|
||||
if (!flushed) {
|
||||
LOG_ERR("BMC", "Failed writing spine tmp file");
|
||||
}
|
||||
return flushed;
|
||||
}
|
||||
|
||||
bool BookMetadataCache::beginTocPass() {
|
||||
@@ -74,10 +131,17 @@ bool BookMetadataCache::beginTocPass() {
|
||||
useSpineHrefIndex = false;
|
||||
}
|
||||
|
||||
// Wrapper OOM is fine: createTocEntry falls back to unbuffered writes.
|
||||
passOut = makeUniqueNoThrow<serialization::BufferedFileWriter>(tocFile, BUILD_IO_BUFFER_SIZE);
|
||||
return true;
|
||||
}
|
||||
|
||||
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
|
||||
tocFile.close();
|
||||
spineFile.close();
|
||||
@@ -86,7 +150,7 @@ bool BookMetadataCache::endTocPass() {
|
||||
spineHrefIndex.shrink_to_fit();
|
||||
useSpineHrefIndex = false;
|
||||
|
||||
return true;
|
||||
return flushed;
|
||||
}
|
||||
|
||||
bool BookMetadataCache::endWrite() {
|
||||
@@ -119,6 +183,14 @@ bool BookMetadataCache::buildBookBin(const std::string& epubPath, const BookMeta
|
||||
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 =
|
||||
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() +
|
||||
@@ -128,31 +200,34 @@ bool BookMetadataCache::buildBookBin(const std::string& epubPath, const BookMeta
|
||||
const uint32_t lutOffset = headerASize + metadataSize;
|
||||
|
||||
// Header A
|
||||
serialization::writePod(bookFile, BOOK_CACHE_VERSION);
|
||||
serialization::writePod(bookFile, lutOffset);
|
||||
serialization::writePod(bookFile, spineCount);
|
||||
serialization::writePod(bookFile, tocCount);
|
||||
serialization::writePod(bookOut, BOOK_CACHE_VERSION);
|
||||
serialization::writePod(bookOut, lutOffset);
|
||||
serialization::writePod(bookOut, spineCount);
|
||||
serialization::writePod(bookOut, tocCount);
|
||||
// Metadata
|
||||
serialization::writeString(bookFile, metadata.title);
|
||||
serialization::writeString(bookFile, metadata.author);
|
||||
serialization::writeString(bookFile, metadata.language);
|
||||
serialization::writeString(bookFile, metadata.coverItemHref);
|
||||
serialization::writeString(bookFile, metadata.textReferenceHref);
|
||||
serialization::writeString(bookOut, metadata.title);
|
||||
serialization::writeString(bookOut, metadata.author);
|
||||
serialization::writeString(bookOut, metadata.language);
|
||||
serialization::writeString(bookOut, metadata.coverItemHref);
|
||||
serialization::writeString(bookOut, metadata.textReferenceHref);
|
||||
|
||||
// Loop through spine entries, writing LUT positions
|
||||
spineFile.seek(0);
|
||||
spineIn.seek(0);
|
||||
for (int i = 0; i < spineCount; i++) {
|
||||
uint32_t pos = spineFile.position();
|
||||
auto spineEntry = readSpineEntry(spineFile);
|
||||
serialization::writePod(bookFile, pos + lutOffset + lutSize);
|
||||
const uint32_t pos = spineIn.position();
|
||||
readSpineEntryFrom(spineIn);
|
||||
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
|
||||
tocFile.seek(0);
|
||||
tocIn.seek(0);
|
||||
for (int i = 0; i < tocCount; i++) {
|
||||
uint32_t pos = tocFile.position();
|
||||
auto tocEntry = readTocEntry(tocFile);
|
||||
serialization::writePod(bookFile, pos + lutOffset + lutSize + static_cast<uint32_t>(spineFile.position()));
|
||||
const uint32_t pos = tocIn.position();
|
||||
readTocEntryFrom(tocIn);
|
||||
serialization::writePod(bookOut, pos + lutOffset + lutSize + spineBytes);
|
||||
}
|
||||
|
||||
// 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))
|
||||
std::deque<int16_t> spineToTocIndex(spineCount, -1);
|
||||
tocFile.seek(0);
|
||||
tocIn.seek(0);
|
||||
for (int j = 0; j < tocCount; j++) {
|
||||
auto tocEntry = readTocEntry(tocFile);
|
||||
auto tocEntry = readTocEntryFrom(tocIn);
|
||||
if (tocEntry.spineIndex >= 0 && tocEntry.spineIndex < spineCount) {
|
||||
if (spineToTocIndex[tocEntry.spineIndex] == -1) {
|
||||
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;
|
||||
targets.resize(spineCount);
|
||||
|
||||
spineFile.seek(0);
|
||||
spineIn.seek(0);
|
||||
for (int i = 0; i < spineCount; i++) {
|
||||
auto entry = readSpineEntry(spineFile);
|
||||
auto entry = readSpineEntryFrom(spineIn);
|
||||
std::string path = FsHelpers::normalisePath(entry.href);
|
||||
|
||||
ZipFile::SizeTarget t;
|
||||
@@ -224,10 +299,10 @@ bool BookMetadataCache::buildBookBin(const std::string& epubPath, const BookMeta
|
||||
}
|
||||
|
||||
uint32_t cumSize = 0;
|
||||
spineFile.seek(0);
|
||||
spineIn.seek(0);
|
||||
int lastSpineTocIndex = -1;
|
||||
for (int i = 0; i < spineCount; i++) {
|
||||
auto spineEntry = readSpineEntry(spineFile);
|
||||
auto spineEntry = readSpineEntryFrom(spineIn);
|
||||
|
||||
spineEntry.tocIndex = spineToTocIndex[i];
|
||||
|
||||
@@ -260,23 +335,33 @@ bool BookMetadataCache::buildBookBin(const std::string& epubPath, const BookMeta
|
||||
spineEntry.cumulativeSize = cumSize;
|
||||
|
||||
// Write out spine data to book.bin
|
||||
writeSpineEntry(bookFile, spineEntry);
|
||||
writeSpineEntryTo(bookOut, spineEntry);
|
||||
}
|
||||
// Close opened zip file
|
||||
zip.close();
|
||||
|
||||
// Loop through toc entries from toc file writing to book.bin
|
||||
tocFile.seek(0);
|
||||
tocIn.seek(0);
|
||||
for (int i = 0; i < tocCount; i++) {
|
||||
auto tocEntry = readTocEntry(tocFile);
|
||||
writeTocEntry(bookFile, tocEntry);
|
||||
auto tocEntry = readTocEntryFrom(tocIn);
|
||||
writeTocEntryTo(bookOut, tocEntry);
|
||||
}
|
||||
|
||||
const bool written = bookOut.flush();
|
||||
|
||||
// Explicit close() required: member variables persist beyond function scope
|
||||
bookFile.close();
|
||||
spineFile.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");
|
||||
return true;
|
||||
}
|
||||
@@ -294,21 +379,11 @@ bool BookMetadataCache::cleanupTmpFiles() const {
|
||||
}
|
||||
|
||||
uint32_t BookMetadataCache::writeSpineEntry(HalFile& file, const SpineEntry& entry) const {
|
||||
const uint32_t pos = file.position();
|
||||
serialization::writeString(file, entry.href);
|
||||
serialization::writePod(file, entry.cumulativeSize);
|
||||
serialization::writePod(file, entry.tocIndex);
|
||||
return pos;
|
||||
return writeSpineEntryTo(file, entry);
|
||||
}
|
||||
|
||||
uint32_t BookMetadataCache::writeTocEntry(HalFile& file, const TocEntry& entry) const {
|
||||
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;
|
||||
return writeTocEntryTo(file, entry);
|
||||
}
|
||||
|
||||
// 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);
|
||||
writeSpineEntry(spineFile, entry);
|
||||
if (passOut) {
|
||||
writeSpineEntryTo(*passOut, entry);
|
||||
} else {
|
||||
writeSpineEntry(spineFile, entry);
|
||||
}
|
||||
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;
|
||||
// device fonts have no combining-mark positioning, so NFD titles render broken.
|
||||
const TocEntry entry(utf8ComposeNfc(title), href, anchor, level, spineIndex);
|
||||
writeTocEntry(tocFile, entry);
|
||||
if (passOut) {
|
||||
writeTocEntryTo(*passOut, entry);
|
||||
} else {
|
||||
writeTocEntry(tocFile, entry);
|
||||
}
|
||||
tocCount++;
|
||||
}
|
||||
|
||||
@@ -442,19 +525,7 @@ BookMetadataCache::TocEntry BookMetadataCache::getTocEntry(const int index) {
|
||||
}
|
||||
|
||||
BookMetadataCache::SpineEntry BookMetadataCache::readSpineEntry(HalFile& file) const {
|
||||
SpineEntry entry;
|
||||
serialization::readString(file, entry.href);
|
||||
serialization::readPod(file, entry.cumulativeSize);
|
||||
serialization::readPod(file, entry.tocIndex);
|
||||
return entry;
|
||||
return readSpineEntryFrom(file);
|
||||
}
|
||||
|
||||
BookMetadataCache::TocEntry BookMetadataCache::readTocEntry(HalFile& file) const {
|
||||
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;
|
||||
}
|
||||
BookMetadataCache::TocEntry BookMetadataCache::readTocEntry(HalFile& file) const { return readTocEntryFrom(file); }
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
#pragma once
|
||||
|
||||
#include <BufferedFile.h>
|
||||
#include <HalStorage.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <deque>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
class BookMetadataCache {
|
||||
@@ -54,6 +56,11 @@ class BookMetadataCache {
|
||||
// Temp file handles during build
|
||||
HalFile spineFile;
|
||||
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)
|
||||
struct SpineHrefIndexEntry {
|
||||
|
||||
@@ -57,6 +57,10 @@ void PageImage::render(GfxRenderer& renderer, const int fontId, const int xOffse
|
||||
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) {
|
||||
serialization::writePod(file, xPos);
|
||||
serialization::writePod(file, yPos);
|
||||
@@ -125,6 +129,17 @@ void Page::renderImages(GfxRenderer& renderer, const int fontId, const int xOffs
|
||||
[](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 {
|
||||
const uint16_t count = elements.size();
|
||||
serialization::writePod(file, count);
|
||||
|
||||
@@ -50,6 +50,7 @@ class PageImage final : public PageElement {
|
||||
PageImage(std::shared_ptr<ImageBlock> block, const int16_t xPos, const int16_t yPos)
|
||||
: PageElement(xPos, yPos), imageBlock(std::move(block)) {}
|
||||
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;
|
||||
PageElementTag getTag() const override { return TAG_PageImage; }
|
||||
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 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;
|
||||
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; });
|
||||
}
|
||||
|
||||
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)
|
||||
// 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 {
|
||||
|
||||
@@ -11,9 +11,13 @@
|
||||
#include "parsers/ChapterHtmlSlimParser.h"
|
||||
|
||||
namespace {
|
||||
// v28: text decoration bits now include line-through in serialized wordStyles.
|
||||
// v29: TextBlock word data stored as one flat arena (offset table + NUL-terminated
|
||||
// text blob) instead of length-prefixed strings and per-field arrays.
|
||||
constexpr uint8_t SECTION_FILE_VERSION = 29;
|
||||
// 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
|
||||
|
||||
@@ -72,6 +72,7 @@ class Section {
|
||||
// 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.
|
||||
@@ -131,6 +132,20 @@ class Section {
|
||||
// (covers finalized sections and partials from a previous session).
|
||||
std::optional<uint16_t> findAnchor(const std::string& anchor) const;
|
||||
|
||||
// MEMFIX-PORT: section resident-bytes audit accessor; portable
|
||||
// Approximate resident heap for the audit log. Steady state (no build) a
|
||||
// Section holds little beyond itself; during a build the page LUT and path
|
||||
// strings dominate (the parser's internal footprint is not walked here).
|
||||
size_t residentBytes() const {
|
||||
size_t total = sizeof(Section) + filePath.capacity();
|
||||
if (build_) {
|
||||
total += sizeof(BuildContext) + build_->lut.capacity() * sizeof(PageLutEntry) +
|
||||
build_->parsePath.capacity() + build_->contentBase.capacity() + build_->imageBasePath.capacity() +
|
||||
build_->htmlPath.capacity() + build_->tmpHtmlPath.capacity();
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
||||
// 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;
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
#include <Logging.h>
|
||||
#include <Serialization.h>
|
||||
|
||||
#include <cstdlib>
|
||||
|
||||
#include "Epub/converters/DirectPixelWriter.h"
|
||||
#include "Epub/converters/ImageDecoderFactory.h"
|
||||
|
||||
@@ -29,6 +31,54 @@ std::string getCachePath(const std::string& imagePath) {
|
||||
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,
|
||||
int expectedHeight) {
|
||||
HalFile cacheFile;
|
||||
@@ -37,16 +87,8 @@ bool renderFromCache(GfxRenderer& renderer, const std::string& cachePath, int x,
|
||||
}
|
||||
|
||||
uint16_t cachedWidth, cachedHeight;
|
||||
if (cacheFile.read(&cachedWidth, 2) != 2 || cacheFile.read(&cachedHeight, 2) != 2) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 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);
|
||||
if (!readValidCacheHeader(cacheFile, expectedWidth, expectedHeight, cachedWidth, cachedHeight)) {
|
||||
LOG_ERR("IMG", "Invalid image cache: %s", cachePath.c_str());
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -119,6 +161,28 @@ bool renderFromCache(GfxRenderer& renderer, const std::string& cachePath, int x,
|
||||
|
||||
} // 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) {
|
||||
// The font-prewarm scan pass only accumulates glyphs; an image contributes
|
||||
// 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;
|
||||
}
|
||||
|
||||
if (imageFailedThisSession(imagePath)) {
|
||||
renderPlaceholder(renderer, x, y);
|
||||
return;
|
||||
}
|
||||
|
||||
// Try to render from cache first
|
||||
std::string cachePath = getCachePath(imagePath);
|
||||
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;
|
||||
if (!Storage.openFileForRead("IMG", imagePath, file)) {
|
||||
LOG_ERR("IMG", "Image file not found: %s", imagePath.c_str());
|
||||
rememberImageFailure(imagePath);
|
||||
renderPlaceholder(renderer, x, y);
|
||||
return;
|
||||
}
|
||||
size_t fileSize = file.size();
|
||||
@@ -168,6 +239,8 @@ void ImageBlock::render(GfxRenderer& renderer, const int x, const int y) {
|
||||
|
||||
if (fileSize == 0) {
|
||||
LOG_ERR("IMG", "Image file is empty: %s", imagePath.c_str());
|
||||
rememberImageFailure(imagePath);
|
||||
renderPlaceholder(renderer, x, y);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -187,6 +260,8 @@ void ImageBlock::render(GfxRenderer& renderer, const int x, const int y) {
|
||||
ImageToFramebufferDecoder* decoder = ImageDecoderFactory::getDecoder(imagePath);
|
||||
if (!decoder) {
|
||||
LOG_ERR("IMG", "No decoder found for image: %s", imagePath.c_str());
|
||||
rememberImageFailure(imagePath);
|
||||
renderPlaceholder(renderer, x, y);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -195,6 +270,8 @@ void ImageBlock::render(GfxRenderer& renderer, const int x, const int y) {
|
||||
bool success = decoder->decodeToFramebuffer(imagePath, renderer, config);
|
||||
if (!success) {
|
||||
LOG_ERR("IMG", "Failed to decode image: %s", imagePath.c_str());
|
||||
rememberImageFailure(imagePath);
|
||||
renderPlaceholder(renderer, x, y);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -16,6 +16,10 @@ class ImageBlock final : public Block {
|
||||
int16_t getHeight() const { return height; }
|
||||
|
||||
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; }
|
||||
bool isEmpty() override { return false; }
|
||||
|
||||
@@ -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.
|
||||
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;
|
||||
}
|
||||
|
||||
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.
|
||||
// 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.
|
||||
// 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) {
|
||||
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;
|
||||
|
||||
case PNG_PIXEL_TRUECOLOR:
|
||||
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);
|
||||
}
|
||||
break;
|
||||
@@ -125,7 +159,7 @@ void convertLineToGray(uint8_t* pPixels, uint8_t* grayLine, int width, int pixel
|
||||
if (palette) {
|
||||
if (hasAlpha) {
|
||||
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 gray = (uint8_t)((p[0] * 77 + p[1] * 150 + p[2] * 29) >> 8);
|
||||
uint8_t alpha = palette[768 + idx];
|
||||
@@ -133,12 +167,15 @@ void convertLineToGray(uint8_t* pPixels, uint8_t* grayLine, int width, int pixel
|
||||
}
|
||||
} else {
|
||||
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);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
memcpy(grayLine, pPixels, width);
|
||||
for (int x = 0; x < width; x++) {
|
||||
grayLine[x] = expandSampleToByte(readPackedSample(pPixels, x, bitsPerSample), bitsPerSample);
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
@@ -152,7 +189,7 @@ void convertLineToGray(uint8_t* pPixels, uint8_t* grayLine, int width, int pixel
|
||||
|
||||
case PNG_PIXEL_TRUECOLOR_ALPHA:
|
||||
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 alpha = p[3];
|
||||
grayLine[x] = (uint8_t)((gray * alpha + 255 * (255 - alpha)) / 255);
|
||||
@@ -172,74 +209,83 @@ int pngDrawCallback(PNGDRAW* pDraw) {
|
||||
int srcY = pDraw->y;
|
||||
int srcWidth = ctx->srcWidth;
|
||||
|
||||
// Calculate destination Y with scaling
|
||||
int dstY = (int)(srcY * ctx->scale);
|
||||
// Map source rows with the exact output-height ratio. During downscaling,
|
||||
// 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 (dstY == ctx->lastDstY) return 1;
|
||||
ctx->lastDstY = dstY;
|
||||
|
||||
// Check bounds
|
||||
if (dstY >= ctx->dstHeight) return 1;
|
||||
|
||||
int outY = ctx->config->y + dstY;
|
||||
if (outY >= ctx->screenHeight) return 1;
|
||||
if (firstDstY <= ctx->lastDstY) firstDstY = ctx->lastDstY + 1;
|
||||
if (firstDstY >= endDstY || firstDstY >= ctx->dstHeight) return 1;
|
||||
if (endDstY > ctx->dstHeight) endDstY = ctx->dstHeight;
|
||||
|
||||
// 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);
|
||||
|
||||
// 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 outXBase = ctx->config->x;
|
||||
int screenWidth = ctx->screenWidth;
|
||||
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;
|
||||
pw.init(*ctx->renderer);
|
||||
pw.beginRow(outY);
|
||||
|
||||
// The cache streams to disk one row at a time. Flushing rows below this one
|
||||
// (PNGdec delivers scanlines top to bottom) repositions the single-row band.
|
||||
// A flush failure stops caching for the rest of the decode so we never write
|
||||
// past the band buffer; finalize() then drops the partial file.
|
||||
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);
|
||||
}
|
||||
}
|
||||
for (int dstY = firstDstY; dstY < endDstY; dstY++) {
|
||||
ctx->lastDstY = dstY;
|
||||
int outY = ctx->config->y + dstY;
|
||||
if (outY >= ctx->screenHeight) continue;
|
||||
|
||||
int srcX = 0;
|
||||
int error = 0;
|
||||
pw.beginRow(outY);
|
||||
|
||||
for (int dstX = 0; dstX < dstWidth; dstX++) {
|
||||
int outX = outXBase + dstX;
|
||||
if (outX < screenWidth) {
|
||||
uint8_t gray = ctx->grayLineBuffer[srcX];
|
||||
|
||||
uint8_t ditheredGray;
|
||||
if (useDithering) {
|
||||
ditheredGray = applyBayerDither4Level(gray, outX, outY);
|
||||
// The cache streams to disk one row at a time. Flushing rows below this one
|
||||
// (PNGdec delivers scanlines top to bottom) repositions the single-row band.
|
||||
// A flush failure stops caching for the rest of the decode so we never write
|
||||
// past the band buffer; finalize() then drops the partial file.
|
||||
bool caching = ctx->caching;
|
||||
DirectCacheWriter cw;
|
||||
if (caching) {
|
||||
if (!ctx->cache.advanceTo(dstY)) {
|
||||
caching = false;
|
||||
ctx->caching = false;
|
||||
} else {
|
||||
ditheredGray = gray / 85;
|
||||
if (ditheredGray > 3) ditheredGray = 3;
|
||||
cw.init(ctx->cache.buffer, ctx->cache.bytesPerRow, ctx->cache.bandRows, ctx->cache.originX);
|
||||
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
|
||||
error += srcWidth;
|
||||
while (error >= dstWidth) {
|
||||
error -= dstWidth;
|
||||
srcX++;
|
||||
int srcX = 0;
|
||||
int error = 0;
|
||||
|
||||
for (int dstX = 0; dstX < dstWidth; dstX++) {
|
||||
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
|
||||
|
||||
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 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) {
|
||||
LOG_ERR("PNG",
|
||||
"PNG row buffer too small: need %d bytes for width=%d type=%d, configured PNG_MAX_BUFFERED_PIXELS=%d",
|
||||
requiredInternal, ctx.srcWidth, pixelType, PNG_MAX_BUFFERED_PIXELS);
|
||||
LOG_ERR(
|
||||
"PNG",
|
||||
"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");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (png->getBpp() != 8) {
|
||||
warnUnsupportedFeature("bit depth (" + std::to_string(png->getBpp()) + "bpp)", imagePath);
|
||||
if (!isSupportedBitDepth(pixelType, bitsPerSample)) {
|
||||
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
|
||||
const size_t grayBufSize = PNG_MAX_BUFFERED_PIXELS / 2;
|
||||
ctx.grayLineBuffer = static_cast<uint8_t*>(malloc(grayBufSize));
|
||||
if (!ctx.grayLineBuffer) {
|
||||
// The converter expands each source row to 8-bit grayscale before dithering,
|
||||
// so this scratch buffer is sized by source pixels even when PNGdec reads a
|
||||
// packed 1/2/4-bit row internally.
|
||||
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");
|
||||
return false;
|
||||
}
|
||||
ctx.grayLineBuffer = grayLineBuffer.get();
|
||||
|
||||
// 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
|
||||
@@ -375,7 +435,6 @@ bool PngToFramebufferConverter::decodeToFramebuffer(const std::string& imagePath
|
||||
rc = png->decode(&ctx, 0);
|
||||
unsigned long decodeTime = millis() - decodeStart;
|
||||
|
||||
free(ctx.grayLineBuffer);
|
||||
ctx.grayLineBuffer = nullptr;
|
||||
|
||||
if (rc != PNG_SUCCESS) {
|
||||
|
||||
@@ -803,6 +803,9 @@ bool CssParser::loadFromCache() {
|
||||
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 {
|
||||
return static_cast<size_t>(file.available()) >= neededBytes;
|
||||
};
|
||||
|
||||
@@ -67,6 +67,20 @@ class CssParser {
|
||||
*/
|
||||
[[nodiscard]] static CssStyle parseInlineStyle(std::string_view styleValue);
|
||||
|
||||
// MEMFIX-PORT: stylesheet resident-bytes audit accessor; portable
|
||||
// Approximate resident heap of the parsed stylesheet, for the audit log.
|
||||
// unordered_map cost model: bucket array + one node per rule (libstdc++ node
|
||||
// overhead ~= 2 pointers + hash) + key string capacity when it exceeds SSO.
|
||||
size_t residentBytes() const {
|
||||
size_t total = rulesBySelector_.bucket_count() * sizeof(void*);
|
||||
for (const auto& kv : rulesBySelector_) {
|
||||
total += sizeof(void*) * 2 + sizeof(size_t); // node overhead
|
||||
total += sizeof(kv);
|
||||
if (kv.first.capacity() > 15) total += kv.first.capacity(); // beyond SSO
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if any rules have been loaded
|
||||
*/
|
||||
|
||||
@@ -24,7 +24,7 @@ struct Iso639Mapping {
|
||||
};
|
||||
static constexpr Iso639Mapping kIso639Mappings[] = {{"eng", "en"}, {"fra", "fr"}, {"fre", "fr"}, {"deu", "de"},
|
||||
{"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.
|
||||
const LanguageHyphenator* hyphenatorForLanguage(const std::string& langTag) {
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
#include "generated/hyph-de.trie.h"
|
||||
#include "generated/hyph-en.trie.h"
|
||||
#include "generated/hyph-es.trie.h"
|
||||
#include "generated/hyph-fi.trie.h"
|
||||
#include "generated/hyph-fr.trie.h"
|
||||
#include "generated/hyph-it.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 ukrainianHyphenator(uk_patterns, isCyrillicLetter, toLowerCyrillic);
|
||||
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() {
|
||||
static const EntryArray kEntries = {{{"english", "en", &englishHyphenator},
|
||||
@@ -38,7 +40,8 @@ const EntryArray& entries() {
|
||||
{"italian", "it", &italianHyphenator},
|
||||
{"polish", "pl", &polishHyphenator},
|
||||
{"swedish", "sv", &swedishHyphenator},
|
||||
{"ukrainian", "uk", &ukrainianHyphenator}}};
|
||||
{"ukrainian", "uk", &ukrainianHyphenator},
|
||||
{"finnish", "fi", &finnishHyphenator}}};
|
||||
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),
|
||||
};
|
||||
@@ -35,7 +35,7 @@ constexpr const char* BOLD_TAGS[] = {"b", "strong"};
|
||||
constexpr const char* ITALIC_TAGS[] = {"i", "em"};
|
||||
constexpr const char* UNDERLINE_TAGS[] = {"u", "ins"};
|
||||
constexpr const char* LINETHROUGH_TAGS[] = {"del", "s", "strike"};
|
||||
constexpr const char* IMAGE_TAGS[] = {"img"};
|
||||
constexpr const char* IMAGE_TAGS[] = {"img", "image"};
|
||||
constexpr const char* SKIP_TAGS[] = {"head"};
|
||||
|
||||
bool isWhitespace(const char c) { return c == ' ' || c == '\r' || c == '\n' || c == '\t'; }
|
||||
@@ -225,6 +225,7 @@ void ChapterHtmlSlimParser::flushPartWordBuffer() {
|
||||
currentTextBlock->addWord(partWordBuffer, fontStyle, false, nextWordContinues);
|
||||
partWordBufferIndex = 0;
|
||||
nextWordContinues = false;
|
||||
listItemBulletOnly = false;
|
||||
}
|
||||
|
||||
// start a new text block if needed
|
||||
@@ -254,6 +255,17 @@ void ChapterHtmlSlimParser::startNewTextBlock(const BlockStyle& blockStyle) {
|
||||
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();
|
||||
return;
|
||||
}
|
||||
|
||||
makePages();
|
||||
}
|
||||
// If the pending anchor is a TOC chapter boundary, force a page break after the previous
|
||||
@@ -261,6 +273,7 @@ void ChapterHtmlSlimParser::startNewTextBlock(const BlockStyle& blockStyle) {
|
||||
flushPendingAnchor();
|
||||
currentTextBlock.reset(new ParsedText(extraParagraphSpacing, hyphenationEnabled, focusReadingEnabled, blockStyle));
|
||||
wordsExtractedInBlock = 0;
|
||||
listItemBulletOnly = false;
|
||||
}
|
||||
|
||||
void ChapterHtmlSlimParser::emitHorizontalRule(const BlockStyle& blockStyle) {
|
||||
@@ -495,11 +508,18 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char*
|
||||
for (int i = 0; atts[i]; i += 2) {
|
||||
if (strcmp(atts[i], "src") == 0) {
|
||||
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) {
|
||||
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
|
||||
if (self->imageRendering == 2) {
|
||||
self->skipUntilDepth = self->depth;
|
||||
@@ -507,19 +527,6 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char*
|
||||
return;
|
||||
}
|
||||
|
||||
// Skip image if CSS display:none
|
||||
if (self->cssParser) {
|
||||
CssStyle imgDisplayStyle = self->cssParser->resolveStyle("img", classAttr);
|
||||
if (!styleAttr.empty()) {
|
||||
imgDisplayStyle.applyOver(CssParser::parseInlineStyle(styleAttr));
|
||||
}
|
||||
if (imgDisplayStyle.hasDisplay() && imgDisplayStyle.display == CssDisplay::None) {
|
||||
self->skipUntilDepth = self->depth;
|
||||
self->depth += 1;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (!src.empty() && self->imageRendering != 1) {
|
||||
LOG_DBG("EHP", "Found image: src=%s", src.c_str());
|
||||
|
||||
@@ -543,24 +550,28 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char*
|
||||
extractSuccess = self->epub->readItemContentsToStream(resolvedPath, cachedImageFile, 4096);
|
||||
cachedImageFile.flush();
|
||||
cachedImageFile.close();
|
||||
delay(50); // Give SD card time to sync
|
||||
}
|
||||
|
||||
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};
|
||||
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);
|
||||
|
||||
int displayWidth = 0;
|
||||
int displayHeight = 0;
|
||||
const float emSize = static_cast<float>(self->renderer.getFontAscenderSize(self->fontId));
|
||||
CssStyle imgStyle = self->cssParser ? self->cssParser->resolveStyle("img", classAttr) : CssStyle{};
|
||||
// Merge inline style (e.g. style="height: 2em") so it overrides stylesheet rules
|
||||
if (!styleAttr.empty()) {
|
||||
imgStyle.applyOver(CssParser::parseInlineStyle(styleAttr));
|
||||
}
|
||||
const CssStyle& imgStyle = cssStyle;
|
||||
const bool hasCssHeight = imgStyle.hasImageHeight();
|
||||
const bool hasCssWidth = imgStyle.hasImageWidth();
|
||||
|
||||
@@ -882,6 +893,7 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char*
|
||||
|
||||
if (strcmp(name, "li") == 0) {
|
||||
self->currentTextBlock->addWord("\xe2\x80\xa2", EpdFontFamily::REGULAR);
|
||||
self->listItemBulletOnly = true;
|
||||
}
|
||||
}
|
||||
} else if (matches(name, UNDERLINE_TAGS, std::size(UNDERLINE_TAGS))) {
|
||||
@@ -1288,6 +1300,13 @@ void XMLCALL ChapterHtmlSlimParser::endElement(void* userData, const XML_Char* n
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -80,6 +80,7 @@ class ChapterHtmlSlimParser {
|
||||
int tableDepth = 0;
|
||||
int tableRowIndex = 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
|
||||
int completedPageCount = 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.");
|
||||
}
|
||||
|
||||
// Sort item index for binary search if we have enough items
|
||||
if (self->itemIndex.size() >= LARGE_SPINE_THRESHOLD) {
|
||||
// Sort the (unconditionally-built) item index so every idref lookup uses binary
|
||||
// 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) {
|
||||
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;
|
||||
}
|
||||
} else {
|
||||
// Slow path: linear scan (for small manifests, keeps original behavior)
|
||||
// TODO: This lookup is slow as need to scan through all items each time.
|
||||
// It can take up to 200ms per item when getting to 1500 items.
|
||||
// Fallback linear scan, only reached when the index is empty (no manifest
|
||||
// items). The fast binary-search path above is used for all real manifests.
|
||||
self->tempItemStore.seek(0);
|
||||
std::string itemId;
|
||||
while (self->tempItemStore.available()) {
|
||||
|
||||
@@ -32,7 +32,7 @@ class ContentOpfParser final : public Print {
|
||||
HalFile tempItemStore;
|
||||
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 {
|
||||
uint32_t idHash; // FNV-1a hash of itemId
|
||||
uint16_t idLen; // length for collision reduction
|
||||
@@ -41,8 +41,6 @@ class ContentOpfParser final : public Print {
|
||||
std::deque<ItemIndexEntry> itemIndex;
|
||||
bool useItemIndex = false;
|
||||
|
||||
static constexpr uint16_t LARGE_SPINE_THRESHOLD = 400;
|
||||
|
||||
// FNV-1a hash function
|
||||
static uint32_t fnvHash(const std::string& s) {
|
||||
uint32_t hash = 2166136261u;
|
||||
|
||||
+128
-26
@@ -1,6 +1,7 @@
|
||||
#include "GfxRenderer.h"
|
||||
|
||||
#include <BidiUtils.h>
|
||||
#include <BuildScratch.h>
|
||||
#include <FontDecompressor.h>
|
||||
#include <HalGPIO.h>
|
||||
#include <Logging.h>
|
||||
@@ -24,6 +25,36 @@ uint8_t resolveSdCardStyle(const SdCardFont& font, const EpdFontFamily::Style st
|
||||
|
||||
namespace {
|
||||
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
|
||||
|
||||
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 {
|
||||
auto it = sdCardFonts_.find(fontId);
|
||||
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) {
|
||||
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.
|
||||
// The table survives across paragraphs/sections (capped per font), so
|
||||
// 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) {
|
||||
LOG_DBG("GFX", "ensureSdCardFontReady: %d glyph(s) not found", missed);
|
||||
}
|
||||
@@ -92,19 +130,46 @@ void GfxRenderer::begin() {
|
||||
}
|
||||
|
||||
void GfxRenderer::releaseFrameBufferForBuild() {
|
||||
display.releaseFrameBuffers();
|
||||
// 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() {
|
||||
if (!display.reallocFrameBuffers()) {
|
||||
LOG_ERR("GFX", "Framebuffer realloc failed after build");
|
||||
return false;
|
||||
}
|
||||
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(); }
|
||||
|
||||
void GfxRenderer::insertFont(const int fontId, EpdFontFamily font) {
|
||||
@@ -432,18 +497,21 @@ void GfxRenderer::drawText(const int fontId, const int x, const int y, const cha
|
||||
uint32_t cp;
|
||||
uint32_t prevCp = 0;
|
||||
while ((cp = utf8NextCodepoint(reinterpret_cast<const uint8_t**>(&textCursor)))) {
|
||||
// Skip Hebrew Niqqud (vowel marks)
|
||||
// Temporary: avoid adding Niqqud to built-in fonts. Remove when custom fonts are supported.
|
||||
if (cp >= 0x0591 && cp <= 0x05C7) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (utf8IsCombiningMark(cp)) {
|
||||
// RTL vowel marks (Hebrew niqqud, Arabic harakat) ride the combining-mark
|
||||
// path: zero-advance overlays on the preceding base glyph (applyBidiVisual
|
||||
// emits base-then-marks per UAX#9 L3). anchorFor pins position-sensitive
|
||||
// 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
|
||||
// the getGlyph lookup and skip them, as before.
|
||||
if (utf8IsCombiningMark(cp) || BidiUtils::isTransparentMark(cp)) {
|
||||
const EpdGlyph* combiningGlyph = font.getGlyph(cp, style);
|
||||
if (!combiningGlyph) continue;
|
||||
const int raiseBy = combiningMark::raiseAboveBase(combiningGlyph->top, combiningGlyph->height, lastBaseTop);
|
||||
const int combiningX = combiningMark::centerOver(lastBaseX, lastBaseLeft, lastBaseWidth, combiningGlyph->left,
|
||||
combiningGlyph->width);
|
||||
const auto anchor = combiningMark::anchorFor(cp);
|
||||
const int raiseBy =
|
||||
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);
|
||||
continue;
|
||||
}
|
||||
@@ -1383,6 +1451,20 @@ void GfxRenderer::displayBuffer(const HalDisplay::RefreshMode refreshMode) const
|
||||
display.displayBuffer(refreshMode, fadingFix);
|
||||
}
|
||||
|
||||
void GfxRenderer::displayBufferAsync(const HalDisplay::RefreshMode refreshMode) const {
|
||||
// The async path has no turn-off-screen hook, which the sunlight fading fix
|
||||
// relies on; keep those users on the blocking path.
|
||||
if (fadingFix) {
|
||||
display.displayBuffer(refreshMode, fadingFix);
|
||||
return;
|
||||
}
|
||||
display.displayBufferAsync(refreshMode);
|
||||
}
|
||||
|
||||
void GfxRenderer::waitRefreshComplete() const { display.waitRefreshComplete(); }
|
||||
|
||||
bool GfxRenderer::supportsAsyncRefresh() const { return !fadingFix && display.supportsAsyncRefresh(); }
|
||||
|
||||
std::string GfxRenderer::truncatedText(const int fontId, const char* text, const int maxWidth,
|
||||
const EpdFontFamily::Style style) const {
|
||||
if (!text || maxWidth <= 0) return "";
|
||||
@@ -1629,6 +1711,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 {
|
||||
// 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.
|
||||
// No kerning/ligature lookup — consistent with previous metadataOnly behavior
|
||||
// where kern/lig data was not loaded.
|
||||
@@ -1644,6 +1735,10 @@ int GfxRenderer::getTextAdvanceX(const int fontId, const char* text, EpdFontFami
|
||||
}
|
||||
const auto& font = fontIt->second;
|
||||
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);
|
||||
if (advFP == 0 && !utf8IsCombiningMark(cp)) {
|
||||
const EpdGlyph* glyph = font.getGlyph(cp, style);
|
||||
@@ -1666,6 +1761,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
|
||||
const auto& font = fontIt->second;
|
||||
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)) {
|
||||
continue;
|
||||
}
|
||||
@@ -1742,18 +1841,21 @@ void GfxRenderer::drawTextRotated90CW(const int fontId, const int x, const int y
|
||||
uint32_t cp;
|
||||
uint32_t prevCp = 0;
|
||||
while ((cp = utf8NextCodepoint(reinterpret_cast<const uint8_t**>(&text)))) {
|
||||
// Skip Hebrew Niqqud (vowel marks)
|
||||
// Temporary: avoid adding Niqqud to built-in fonts. Remove when custom fonts are supported.
|
||||
if (cp >= 0x0591 && cp <= 0x05C7) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (utf8IsCombiningMark(cp)) {
|
||||
// RTL vowel marks (Hebrew niqqud, Arabic harakat) ride the combining-mark
|
||||
// path: zero-advance overlays on the preceding base glyph (applyBidiVisual
|
||||
// emits base-then-marks per UAX#9 L3). anchorFor pins position-sensitive
|
||||
// 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
|
||||
// the getGlyph lookup and skip them, as before.
|
||||
if (utf8IsCombiningMark(cp) || BidiUtils::isTransparentMark(cp)) {
|
||||
const EpdGlyph* combiningGlyph = font.getGlyph(cp, style);
|
||||
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 combiningY = combiningMark::centerOverRotated90CW(lastBaseY, lastBaseLeft, lastBaseWidth,
|
||||
const int combiningY = combiningMark::anchorOverRotated90CW(anchor, lastBaseY, lastBaseLeft, lastBaseWidth,
|
||||
combiningGlyph->left, combiningGlyph->width);
|
||||
renderCharImpl<TextRotation::Rotated90CW>(*this, renderMode, font, cp, combiningX, combiningY, black, style);
|
||||
continue;
|
||||
|
||||
@@ -135,6 +135,17 @@ class GfxRenderer {
|
||||
int getScreenWidth() const;
|
||||
int getScreenHeight() const;
|
||||
void displayBuffer(HalDisplay::RefreshMode refreshMode = HalDisplay::FAST_REFRESH) const;
|
||||
// Non-blocking refresh: starts the waveform and returns so CPU work (e.g.
|
||||
// grayscale strip rendering) can overlap the panel's refresh time. The
|
||||
// framebuffer must stay untouched until waitRefreshComplete(). Falls back to
|
||||
// a blocking refresh when fadingFix is enabled or the panel lacks deferral
|
||||
// support. See HalDisplay::displayBufferAsync for the baseline contract.
|
||||
void displayBufferAsync(HalDisplay::RefreshMode refreshMode = HalDisplay::FAST_REFRESH) const;
|
||||
void waitRefreshComplete() const;
|
||||
// True when displayBufferAsync() genuinely overlaps: panel defers and
|
||||
// fadingFix isn't forcing the blocking path. Callers can skip overlap
|
||||
// scaffolding (e.g. whole-plane grayscale buffers) when false.
|
||||
bool supportsAsyncRefresh() const;
|
||||
// EXPERIMENTAL: Windowed update - display only a rectangular region
|
||||
// void displayWindow(int x, int y, int width, int height) const;
|
||||
void invertScreen() const;
|
||||
@@ -250,13 +261,34 @@ class GfxRenderer {
|
||||
// Font helpers
|
||||
const uint8_t* getGlyphBitmap(const EpdFontData* fontData, const EpdGlyph* glyph) const;
|
||||
|
||||
// Lend the framebuffer to memory-hungry phases such as section pagination.
|
||||
// Nothing may draw/display while it is released. restore returns the buffer
|
||||
// white, so callers must redraw the full screen afterward.
|
||||
// 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
|
||||
uint8_t* getFrameBuffer() const;
|
||||
size_t getBufferSize() const;
|
||||
|
||||
@@ -87,6 +87,7 @@ STR_USERNAME: "Імя карыстальніка"
|
||||
STR_PASSWORD: "Пароль"
|
||||
STR_SYNC_SERVER_URL: "URL сервера сінхранізацыі"
|
||||
STR_DOCUMENT_MATCHING: "Супастаўленне дакументаў"
|
||||
STR_SEND_METADATA: "Адпраўляць метаданыя дакумента"
|
||||
STR_AUTHENTICATE: "Аўтарызацыя"
|
||||
STR_KOREADER_USERNAME: "Імя карыстальніка KOReader"
|
||||
STR_KOREADER_PASSWORD: "Пароль KOReader"
|
||||
@@ -300,3 +301,5 @@ STR_STEP_HINT_SIDE: "Бакавыя кнопкі:"
|
||||
STR_AUTO_TURN_ENABLED: "Аўтаперагортванне: "
|
||||
STR_AUTO_TURN_PAGES_PER_MIN: "Аўтаперагортванне (старонак за хвіліну)"
|
||||
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_ENTERING_SLEEP: "Entrant en repòs"
|
||||
STR_BROWSE_FILES: "Explora fitxers"
|
||||
STR_FILE_TRANSFER: "Transferència"
|
||||
STR_FILE_TRANSFER: "Transferència de fitxers"
|
||||
STR_SETTINGS_TITLE: "Configuració"
|
||||
STR_CONTINUE_READING: "Continua llegint"
|
||||
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_EMPTY_CHAPTER: "Capítol buit"
|
||||
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_PAGE_LOAD_ERROR: "Error en carregar la pàgina"
|
||||
STR_EMPTY_FILE: "Fitxer buit"
|
||||
@@ -28,58 +29,63 @@ STR_WIFI_NETWORKS: "Xarxes Wi-Fi"
|
||||
STR_NO_NETWORKS: "No s'han trobat xarxes"
|
||||
STR_NETWORKS_FOUND: "%zu xarxes trobades"
|
||||
STR_SCANNING: "S'està escanejant..."
|
||||
STR_FINDING_SAVED_WIFI: "S'estan cercant xarxes Wi-Fi desades..."
|
||||
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_CONNECTION_FAILED: "Error de connexió"
|
||||
STR_FORGET_NETWORK: "Voleu oblidar aquesta xarxa?"
|
||||
STR_SAVE_PASSWORD: "Voleu desar la contrasenya per a la propera vegada?"
|
||||
STR_PRESS_OK_SCAN: "Premeu OK per tornar a escanejar"
|
||||
STR_FORGET_NETWORK: "Vols oblidar aquesta xarxa?"
|
||||
STR_SAVE_PASSWORD: "Vols desar la contrasenya per a la propera vegada?"
|
||||
STR_PRESS_OK_SCAN: "Prem OK per tornar a escanejar"
|
||||
STR_JOIN_NETWORK: "Uneix-te a una xarxa"
|
||||
STR_CREATE_HOTSPOT: "Crea un punt d'accés"
|
||||
STR_JOIN_DESC: "Connecta't a una xarxa Wi-Fi existent"
|
||||
STR_HOTSPOT_DESC: "Crea una xarxa Wi-Fi per unir-s'hi"
|
||||
STR_STARTING_HOTSPOT: "S'està iniciant el 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_OPEN_URL_HINT: "Obriu aquest URL al navegador"
|
||||
STR_CONNECT_WIFI_HINT: "Connecta el dispositiu a aquesta xarxa Wi-Fi"
|
||||
STR_OPEN_URL_HINT: "Obre aquest URL al navegador"
|
||||
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_NETWORK_LEGEND: "* = Encriptat | + = Desat"
|
||||
STR_MAC_ADDRESS: "Adreça MAC:"
|
||||
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_CALIBRE_RECEIVING: "S'està rebent: "
|
||||
STR_CALIBRE_RECEIVED: "S'ha rebut: "
|
||||
STR_CALIBRE_INSTRUCTION_1: "1) Instal·leu el connector CrossPoint Reader"
|
||||
STR_CALIBRE_INSTRUCTION_2: "2) Estigueu a la mateixa xarxa Wi-Fi"
|
||||
STR_CALIBRE_INSTRUCTION_1: "1) Instal·la el connector CrossPoint Reader"
|
||||
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_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_READER: "Lector"
|
||||
STR_CAT_CONTROLS: "Controls"
|
||||
STR_CAT_SYSTEM: "Sistema"
|
||||
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_EXTRA_SPACING: "Espaiat de paràgraf extra"
|
||||
STR_TEXT_AA: "Antialiàsing del text"
|
||||
STR_IMAGES: "Imatges"
|
||||
STR_IMAGES_DISPLAY: "Mostrar"
|
||||
STR_IMAGES_PLACEHOLDER: "Text de mostra"
|
||||
STR_IMAGES_PLACEHOLDER: "Text alternatiu"
|
||||
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_SIDE_BTN_LAYOUT: "Disposició botons laterals"
|
||||
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_SKIP: "Saltar capítols"
|
||||
STR_LONG_PRESS_BEHAVIOR_ORIENTATION: "Canvi d'orientació"
|
||||
STR_LONG_PRESS_MENU: "Funció de pulsació llarga"
|
||||
STR_FONT_FAMILY: "Tipus de lletra"
|
||||
STR_FONT_SIZE: "Mida de la lletra (UI)"
|
||||
STR_FONT_FAMILY: "Font del lector"
|
||||
STR_FONT_SIZE: "Cos de lletra del lector"
|
||||
STR_LINE_SPACING: "Interlineat del lector"
|
||||
STR_SCREEN_MARGIN: "Marge de pantalla 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_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_REFRESH_FREQ: "Freqüència de refresc"
|
||||
STR_REFRESH_FREQ: "Freqüència d'actualització"
|
||||
STR_KOREADER_SYNC: "Sincronització del KOReader"
|
||||
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_USERNAME: "Nom d'usuari"
|
||||
STR_PASSWORD: "Contrasenya"
|
||||
STR_SYNC_SERVER_URL: "URL del servidor de sincronització"
|
||||
STR_DOCUMENT_MATCHING: "Coincidència de documents"
|
||||
STR_SEND_METADATA: "Envia metadades del document"
|
||||
STR_AUTHENTICATE: "Autentica"
|
||||
STR_KOREADER_USERNAME: "Nom d'usuari 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_KOSYNC: "KOSync"
|
||||
STR_BOOKMARK_OPTION: "Punt de llibre"
|
||||
STR_DISABLED: "Desactivats"
|
||||
STR_DISABLED: "Sense funció"
|
||||
STR_NOTO_SERIF: "Noto Serif"
|
||||
STR_NOTO_SANS: "Noto Sans"
|
||||
STR_SMALL: "Petita"
|
||||
@@ -170,16 +177,16 @@ STR_UPDATING: "S'està actualitzant..."
|
||||
STR_NO_UPDATE: "No hi ha actualitzacions disponibles"
|
||||
STR_UPDATE_FAILED: "Ha fallat l'actualització"
|
||||
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_DOWNLOADING: "S'està baixant..."
|
||||
STR_DOWNLOAD_FAILED: "Ha fallat la baixada"
|
||||
STR_ERROR_MSG: "Error:"
|
||||
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_FETCH_FEED_FAILED: "Ha fallat l'obtenció del feed"
|
||||
STR_PARSE_FEED_FAILED: "Ha fallat l'anàlisi del feed"
|
||||
STR_FETCH_FEED_FAILED: "Ha fallat l'obtenció del canal de continguts"
|
||||
STR_PARSE_FEED_FAILED: "Ha fallat l'anàlisi del canal de continguts"
|
||||
STR_NETWORK_PREFIX: "Xarxa: "
|
||||
STR_IP_ADDRESS_PREFIX: "Adreça IP: "
|
||||
STR_ERROR_GENERAL_FAILURE: "Error: Fallada general"
|
||||
@@ -192,7 +199,7 @@ STR_HOME: "« Inici"
|
||||
STR_SELECT: "Selecciona"
|
||||
STR_SELECTED: "Seleccionat"
|
||||
STR_TOGGLE: "Canvia"
|
||||
STR_TOGGLE_BOOKMARK: "Commuta punt de llibre"
|
||||
STR_TOGGLE_BOOKMARK: "Afegeix o elimina el punt de llibre"
|
||||
STR_CONFIRM: "Confirma"
|
||||
STR_CANCEL: "Cancel·la"
|
||||
STR_CONNECT: "Connecta"
|
||||
@@ -211,7 +218,7 @@ STR_DIR_RIGHT: "Dreta"
|
||||
STR_DIR_UP: "Amunt"
|
||||
STR_DIR_DOWN: "Avall"
|
||||
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_CUSTOMISE_STATUS_BAR: "Personalitza la barra d'estat"
|
||||
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_EXTENDED: "Lyra Ampliat"
|
||||
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_BOOKMARKS: "Punts 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_QUICK_RESUME: "Represa ràpida"
|
||||
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_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_CALIBRE_STARTING: "S'està iniciant el Calibre..."
|
||||
STR_CALIBRE_SETUP: "Configura"
|
||||
STR_CALIBRE_STATUS: "Estat"
|
||||
STR_CLEAR_BUTTON: "Esborra"
|
||||
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_ALREADY_ASSIGNED: "Ja assignat"
|
||||
STR_REMAP_RESET_HINT: "Botó lateral Amunt: Restableix la disposició per defecte"
|
||||
@@ -266,19 +273,19 @@ STR_GO_HOME_BUTTON: "Ves a l'inici"
|
||||
STR_SYNC_PROGRESS: "Sincronitza el progrés"
|
||||
STR_DELETE_CACHE: "Esborra la memòria cau del llibre"
|
||||
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_CHAPTER_PREFIX: "Capítol: "
|
||||
STR_PAGES_SEPARATOR: " pàgines | "
|
||||
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_SYNCING_TIME: "S'està sincronitzant el temps..."
|
||||
STR_CALC_HASH: "S'està calculant el hash del document..."
|
||||
STR_HASH_FAILED: "No s'ha pogut calcular el hash del document"
|
||||
STR_CALC_HASH: "S'està calculant l'empremta electrònica 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_UPLOAD_PROGRESS: "S'està pujant el progrés..."
|
||||
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_REMOTE_LABEL: "Remot:"
|
||||
STR_LOCAL_LABEL: "Local:"
|
||||
@@ -288,7 +295,7 @@ STR_DEVICE_FROM_FORMAT: " De: %s"
|
||||
STR_APPLY_REMOTE: "Aplica el progrés remot"
|
||||
STR_UPLOAD_LOCAL: "Puja el progrés local"
|
||||
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_SYNC_FAILED_MSG: "Sincronització fallida"
|
||||
STR_SAVE_PROGRESS_FAILED: "No s'ha pogut desar el progrés"
|
||||
@@ -307,11 +314,11 @@ STR_SLEEP_NEVER: "Mai"
|
||||
STR_STEP_HINT_FRONT: "Botons frontals:"
|
||||
STR_STEP_HINT_SIDE: "Botons laterals:"
|
||||
STR_SCREENSHOT_BUTTON: "Fes una captura de pantalla"
|
||||
STR_AUTO_TURN_ENABLED: "Passar automàtic activat: "
|
||||
STR_AUTO_TURN_PAGES_PER_MIN: "Passar automàtic (pàgines per minut)"
|
||||
STR_AUTO_TURN_ENABLED: "Pas automàtic de pàgina activat"
|
||||
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_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_PREV_PAGE: "« Pàgina anterior"
|
||||
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_DELETE_SERVER: "Suprimeix el servidor"
|
||||
STR_OPDS_SERVERS: "Servidors OPDS"
|
||||
STR_MANAGE_FONTS: "Gestiona els tipus de lletra"
|
||||
STR_FONT_BROWSER: "Navegador de tipus de lletra"
|
||||
STR_LOADING_FONT_LIST: "S'està carregant la llista de tipus de lletra..."
|
||||
STR_NO_FONTS_AVAILABLE: "No hi ha tipus de lletra disponibles"
|
||||
STR_MANAGE_FONTS: "Gestiona les fonts"
|
||||
STR_FONT_BROWSER: "Navegador de fonts"
|
||||
STR_LOADING_FONT_LIST: "S'està carregant la llista de fonts..."
|
||||
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_INSTALLED: "Tipus de lletra instal·lat!"
|
||||
STR_FONT_INSTALL_FAILED: "Ha fallat la instal·lació del tipus de lletra"
|
||||
STR_FONT_INSTALLED: "Font instal·lada!"
|
||||
STR_FONT_INSTALL_FAILED: "Ha fallat la instal·lació de la font"
|
||||
STR_INSTALLED: "Instal·lat"
|
||||
STR_DOWNLOAD_ALL: "Descarrega-ho tot"
|
||||
STR_UPDATE_ALL: "Actualitza-ho tot"
|
||||
STR_UPDATE_AVAILABLE: "Actualitza"
|
||||
STR_CRASH_TITLE: "Bloqueig 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_REASON: "Motiu del bloqueig:"
|
||||
STR_CRASH_TITLE: "Fallada del sistema"
|
||||
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 de la fallada"
|
||||
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_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_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_SECONDARY_CHAR: "Mantén premut SELECT per al caràcter secundari"
|
||||
STR_KB_HINT_UPPER_SECONDARY: "Mantén premut SELECT per 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_SECONDARY_CHAR: "Mantén premut SELECT: 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: minúscules o caràcter secundari"
|
||||
STR_KB_HINT_URL_SNIPPETS: "Prem URL per inserir fragments"
|
||||
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_VALIDATING_FIRMWARE: "S'està validant el firmware..."
|
||||
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_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_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_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_CHECKING_WIFI: "Kontrola 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_RECEIVED: "Přijato:"
|
||||
STR_CALIBRE_INSTRUCTION_1: "1) Nainstalujte plugin CrossPoint Reader"
|
||||
@@ -92,6 +92,7 @@ STR_USERNAME: "Uživatelské jméno"
|
||||
STR_PASSWORD: "Heslo"
|
||||
STR_SYNC_SERVER_URL: "URL synch. serveru"
|
||||
STR_DOCUMENT_MATCHING: "Párování dokumentů"
|
||||
STR_SEND_METADATA: "Odesílat metadata dokumentu"
|
||||
STR_AUTHENTICATE: "Ověření"
|
||||
STR_KOREADER_USERNAME: "Uživ. jméno KOReaderu"
|
||||
STR_KOREADER_PASSWORD: "Heslo KOReaderu"
|
||||
@@ -275,3 +276,5 @@ STR_SLEEP_NEVER: "Nikdy"
|
||||
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_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_SYNC_SERVER_URL: "Synkroniseringsserver-URL"
|
||||
STR_DOCUMENT_MATCHING: "Dokumentsammenkobling"
|
||||
STR_SEND_METADATA: "Send dokumentmetadata"
|
||||
STR_AUTHENTICATE: "Godkend"
|
||||
STR_KOREADER_USERNAME: "KOReader brugernavn"
|
||||
STR_KOREADER_PASSWORD: "KOReader adgangskode"
|
||||
@@ -303,3 +304,5 @@ STR_SCREENSHOT_BUTTON: "Tag skærmbillede"
|
||||
STR_AUTO_TURN_ENABLED: "Automatisk sidevendning aktiveret: "
|
||||
STR_AUTO_TURN_PAGES_PER_MIN: "Automatisk vending (sider per minut)"
|
||||
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_SYNC_SERVER_URL: "Sync-server URL"
|
||||
STR_DOCUMENT_MATCHING: "Documentkoppeling"
|
||||
STR_SEND_METADATA: "Documentmetadata versturen"
|
||||
STR_AUTHENTICATE: "Authenticatie"
|
||||
STR_KOREADER_USERNAME: "KOReader gebruikersnaam"
|
||||
STR_KOREADER_PASSWORD: "KOReader wachtwoord"
|
||||
@@ -303,3 +304,5 @@ STR_SCREENSHOT_BUTTON: "Screenshot maken"
|
||||
STR_AUTO_TURN_ENABLED: "Automatisch omslaan ingeschakeld: "
|
||||
STR_AUTO_TURN_PAGES_PER_MIN: "Autom. omslaan (pagina's per minuut)"
|
||||
STR_TILT_PAGE_TURN: "Kantel om te bladeren"
|
||||
STR_ADD_HIDDEN_NETWORK: "Verborgen netwerk toevoegen..."
|
||||
STR_ENTER_WIFI_SSID: "Voer netwerknaam in (SSID)"
|
||||
|
||||
@@ -29,7 +29,10 @@ STR_WIFI_NETWORKS: "Wi-Fi Networks"
|
||||
STR_NO_NETWORKS: "No networks found"
|
||||
STR_NETWORKS_FOUND: "%zu networks found"
|
||||
STR_SCANNING: "Scanning..."
|
||||
STR_FINDING_SAVED_WIFI: "Finding saved Wi-Fi..."
|
||||
STR_CONNECTING: "Connecting..."
|
||||
STR_CONNECTING_SAVED_WIFI: "Connecting to saved Wi-Fi..."
|
||||
STR_SHOW_NETWORKS: "Show"
|
||||
STR_CONNECTED: "Connected!"
|
||||
STR_CONNECTION_FAILED: "Connection Failed"
|
||||
STR_FORGET_NETWORK: "Forget Network?"
|
||||
@@ -50,6 +53,8 @@ STR_NETWORK_LEGEND: "* = Encrypted | + = Saved"
|
||||
STR_MAC_ADDRESS: "MAC address:"
|
||||
STR_CHECKING_WIFI: "Checking Wi-Fi..."
|
||||
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_CALIBRE_RECEIVING: "Receiving: "
|
||||
STR_CALIBRE_RECEIVED: "Received: "
|
||||
@@ -102,6 +107,7 @@ STR_USERNAME: "Username"
|
||||
STR_PASSWORD: "Password"
|
||||
STR_SYNC_SERVER_URL: "Sync Server URL"
|
||||
STR_DOCUMENT_MATCHING: "Document Matching"
|
||||
STR_SEND_METADATA: "Send Document Metadata"
|
||||
STR_AUTHENTICATE: "Authenticate"
|
||||
STR_KOREADER_USERNAME: "KOReader Username"
|
||||
STR_KOREADER_PASSWORD: "KOReader Password"
|
||||
|
||||
@@ -92,6 +92,7 @@ STR_USERNAME: "Käyttäjänimi"
|
||||
STR_PASSWORD: "Salasana"
|
||||
STR_SYNC_SERVER_URL: "Synkronointipalvelimen osoite"
|
||||
STR_DOCUMENT_MATCHING: "Dokumenttien tunnistus"
|
||||
STR_SEND_METADATA: "Lähetä asiakirjan metatiedot"
|
||||
STR_AUTHENTICATE: "Tunnistaudu"
|
||||
STR_KOREADER_USERNAME: "KOReader-käyttäjänimi"
|
||||
STR_KOREADER_PASSWORD: "KOReader-salasana"
|
||||
@@ -273,3 +274,5 @@ STR_SLEEP_NEVER: "Ei koskaan"
|
||||
STR_STEP_HINT_FRONT: "Etupainikkeet:"
|
||||
STR_STEP_HINT_SIDE: "Sivupainikkeet:"
|
||||
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_SYNC_SERVER_URL: "URL du serveur"
|
||||
STR_DOCUMENT_MATCHING: "Correspondance"
|
||||
STR_SEND_METADATA: "Envoyer métadonnées"
|
||||
STR_AUTHENTICATE: "Connexion"
|
||||
STR_KOREADER_USERNAME: "Utilisateur"
|
||||
STR_KOREADER_PASSWORD: "Mot de passe"
|
||||
@@ -304,3 +305,5 @@ STR_SCREENSHOT_BUTTON: "Capture d'écran"
|
||||
STR_AUTO_TURN_ENABLED: "Tourne-page auto : "
|
||||
STR_AUTO_TURN_PAGES_PER_MIN: "Tourne-page auto (pages par minute)"
|
||||
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_SYNC_SERVER_URL: "Sync-Server-URL"
|
||||
STR_DOCUMENT_MATCHING: "Dateizuordnung"
|
||||
STR_SEND_METADATA: "Metadaten senden"
|
||||
STR_AUTHENTICATE: "Authentifizieren"
|
||||
STR_KOREADER_USERNAME: "KOReader-Benutzername"
|
||||
STR_KOREADER_PASSWORD: "KOReader-Passwort"
|
||||
@@ -380,3 +381,5 @@ STR_FIRMWARE_WRITE_FAILED: "Schreiben der Firmware-Datei ist fehlgeschlagen"
|
||||
STR_FIRMWARE_UPDATE_DO_NOT_POWER_OFF: "Nicht ausschalten!"
|
||||
STR_RECOVERY_MODE: "Wiederherstellungsmodus"
|
||||
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_SYNC_SERVER_URL: "כתובת שרת סנכרון"
|
||||
STR_DOCUMENT_MATCHING: "התאמת מסמכים"
|
||||
STR_SEND_METADATA: "שלח מטא-נתוני מסמך"
|
||||
STR_AUTHENTICATE: "התחבר"
|
||||
STR_KOREADER_USERNAME: "שם משתמש ב-KOReader"
|
||||
STR_KOREADER_PASSWORD: "סיסמת KOReader"
|
||||
@@ -391,3 +392,5 @@ STR_SLEEP_TIMER_VALUE_FORMAT: "%u דקות"
|
||||
STR_SLEEP_NEVER: "אף פעם"
|
||||
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_CHECKING_WIFI: "Wi-Fi ellenőrzése..."
|
||||
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_CALIBRE_RECEIVING: "Fogadás: "
|
||||
STR_CALIBRE_RECEIVED: "Fogadva: "
|
||||
@@ -94,6 +96,7 @@ STR_USERNAME: "Felhasználónév"
|
||||
STR_PASSWORD: "Jelszó"
|
||||
STR_SYNC_SERVER_URL: "Szinkronizálás szerver URL"
|
||||
STR_DOCUMENT_MATCHING: "Dokumentum egyeztetés"
|
||||
STR_SEND_METADATA: "Dokumentum metaadatok küldése"
|
||||
STR_AUTHENTICATE: "Hitelesítés"
|
||||
STR_KOREADER_USERNAME: "KOReader felhasználónév"
|
||||
STR_KOREADER_PASSWORD: "KOReader jelszó"
|
||||
@@ -300,3 +303,93 @@ STR_SCREENSHOT_BUTTON: "Képernyőkép készítése"
|
||||
STR_AUTO_TURN_ENABLED: "Automatikus lapozás bekapcsolva: "
|
||||
STR_AUTO_TURN_PAGES_PER_MIN: "Automatikus lapozás (oldal/perc)"
|
||||
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_SYNC_SERVER_URL: "URL server di sincronizzazione"
|
||||
STR_DOCUMENT_MATCHING: "Corrispondenza documenti"
|
||||
STR_SEND_METADATA: "Invia metadati documento"
|
||||
STR_AUTHENTICATE: "Autentica"
|
||||
STR_KOREADER_USERNAME: "Nome utente KOReader"
|
||||
STR_KOREADER_PASSWORD: "Password KOReader"
|
||||
@@ -385,4 +386,6 @@ STR_KOSYNC: "KOSync"
|
||||
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_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_SYNC_SERVER_URL: "Синхрондау сервері URL"
|
||||
STR_DOCUMENT_MATCHING: "Құжат сәйкестендіру"
|
||||
STR_SEND_METADATA: "Құжат метадеректерін жіберу"
|
||||
STR_AUTHENTICATE: "Аутентификация"
|
||||
STR_KOREADER_USERNAME: "KOReader пайдаланушы аты"
|
||||
STR_KOREADER_PASSWORD: "KOReader құпия сөзі"
|
||||
@@ -299,3 +300,5 @@ STR_SCREENSHOT_BUTTON: "Скриншот түсіру"
|
||||
STR_AUTO_TURN_ENABLED: "Автоматты бет аудару қосулы: "
|
||||
STR_AUTO_TURN_PAGES_PER_MIN: "Автоматты бет аудару (минутына бет саны)"
|
||||
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_SYNC_SERVER_URL: "Serverio URL"
|
||||
STR_DOCUMENT_MATCHING: "Atpažinimas"
|
||||
STR_SEND_METADATA: "Siųsti dokumento metaduomenis"
|
||||
STR_AUTHENTICATE: "Prisijungti"
|
||||
STR_KOREADER_USERNAME: "KOReader vartotojas"
|
||||
STR_KOREADER_PASSWORD: "KOReader slaptažodis"
|
||||
@@ -300,3 +301,5 @@ STR_SCREENSHOT_BUTTON: "Ekrano nuotrauka"
|
||||
STR_AUTO_TURN_ENABLED: "Auto-vertimas: "
|
||||
STR_AUTO_TURN_PAGES_PER_MIN: "Auto-vertimas (psl/min)"
|
||||
STR_TILT_PAGE_TURN: "Puslapio vertimas pakreipiant"
|
||||
STR_ADD_HIDDEN_NETWORK: "Pridėti paslėptą tinklą..."
|
||||
STR_ENTER_WIFI_SSID: "Įveskite tinklo pavadinimą (SSID)"
|
||||
|
||||
@@ -97,6 +97,7 @@ STR_USERNAME: "Użytkownik"
|
||||
STR_PASSWORD: "Hasło"
|
||||
STR_SYNC_SERVER_URL: "Serwer URL synchronizacji"
|
||||
STR_DOCUMENT_MATCHING: "Dopasowanie dokumentów"
|
||||
STR_SEND_METADATA: "Wyślij metadane dokumentu"
|
||||
STR_AUTHENTICATE: "Uwierzytelnianie"
|
||||
STR_KOREADER_USERNAME: "Użytkownik KOReader"
|
||||
STR_KOREADER_PASSWORD: "Hasło KOReader"
|
||||
@@ -360,3 +361,5 @@ STR_FIRMWARE_WRITE_FAILED: "Zapis oprogramowania nieudany"
|
||||
STR_FIRMWARE_UPDATE_DO_NOT_POWER_OFF: "Nie wyłączać!"
|
||||
STR_RECOVERY_MODE: "Tryb przywracania"
|
||||
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)"
|
||||
|
||||
@@ -99,6 +99,7 @@ STR_USERNAME: "Nome de usuário"
|
||||
STR_PASSWORD: "Senha"
|
||||
STR_SYNC_SERVER_URL: "URL servidor sincronização"
|
||||
STR_DOCUMENT_MATCHING: "Documento correspondente"
|
||||
STR_SEND_METADATA: "Enviar metadados do doc."
|
||||
STR_AUTHENTICATE: "Autenticar"
|
||||
STR_KOREADER_USERNAME: "Usuário do KOReader"
|
||||
STR_KOREADER_PASSWORD: "Senha do KOReader"
|
||||
@@ -383,3 +384,5 @@ 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_SYNC_SERVER_URL: "URL server sincronizare"
|
||||
STR_DOCUMENT_MATCHING: "Corespondenţă document"
|
||||
STR_SEND_METADATA: "Trimite metadate document"
|
||||
STR_AUTHENTICATE: "Autentificare"
|
||||
STR_KOREADER_USERNAME: "Nume utilizator KOReader"
|
||||
STR_KOREADER_PASSWORD: "Parolă KOReader"
|
||||
@@ -303,3 +304,5 @@ STR_SCREENSHOT_BUTTON: "Captură ecran"
|
||||
STR_AUTO_TURN_ENABLED: "Răsfoire automată: "
|
||||
STR_AUTO_TURN_PAGES_PER_MIN: "Pagini pe minut"
|
||||
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_SYNC_SERVER_URL: "URL сервера синхронизации"
|
||||
STR_DOCUMENT_MATCHING: "Сопоставление документов"
|
||||
STR_SEND_METADATA: "Отправлять метаданные"
|
||||
STR_AUTHENTICATE: "Авторизация"
|
||||
STR_KOREADER_USERNAME: "Имя пользователя KOReader"
|
||||
STR_KOREADER_PASSWORD: "Пароль KOReader"
|
||||
@@ -383,3 +384,5 @@ STR_FIRMWARE_WRITE_FAILED: "Ошибка записи прошивки"
|
||||
STR_FIRMWARE_UPDATE_DO_NOT_POWER_OFF: "Не выключайте питание!"
|
||||
STR_RECOVERY_MODE: "Режим восстановления"
|
||||
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_PASSWORD: "Heslo"
|
||||
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_KOREADER_USERNAME: "Používateľské meno KOReader"
|
||||
STR_KOREADER_PASSWORD: "Heslo KOReader"
|
||||
@@ -327,8 +328,8 @@ STR_LINK: "[link]"
|
||||
STR_SCREENSHOT_BUTTON: "Urobiť snímku obrazovky"
|
||||
STR_SLEEP_TIMER_VALUE_FORMAT: "%u min"
|
||||
STR_SLEEP_NEVER: "Nikdy"
|
||||
STR_STEP_HINT_FRONT: "Predné tlačidlá:"
|
||||
STR_STEP_HINT_SIDE: "Bočné tlačidlá:"
|
||||
STR_STEP_HINT_FRONT: "Predné tlačidlá:"
|
||||
STR_STEP_HINT_SIDE: "Bočné tlačidlá:"
|
||||
STR_ADD_SERVER: "Pridať server"
|
||||
STR_SERVER_NAME: "Názov servera"
|
||||
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_RECOVERY_MODE: "Režim obnovenia"
|
||||
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_SYNC_SERVER_URL: "URL strežnika za sinhronizacijo"
|
||||
STR_DOCUMENT_MATCHING: "Ujemanje dokumentov"
|
||||
STR_SEND_METADATA: "Pošlji metapodatke dokumenta"
|
||||
STR_AUTHENTICATE: "Avtentikacija"
|
||||
STR_KOREADER_USERNAME: "KOReader uporabnik"
|
||||
STR_KOREADER_PASSWORD: "KOReader geslo"
|
||||
@@ -300,3 +301,5 @@ STR_SCREENSHOT_BUTTON: "Naredi posnetek zaslona"
|
||||
STR_AUTO_TURN_ENABLED: "Samodejno obračanje: "
|
||||
STR_AUTO_TURN_PAGES_PER_MIN: "Samodejno obračanje (strani na minuto)"
|
||||
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_EMPTY_CHAPTER: "Capítulo vacío"
|
||||
STR_INDEXING: "Indexando"
|
||||
STR_INDEX_FAILED: "No se pudo indexar: libro no válido"
|
||||
STR_MEMORY_ERROR: "Error de memoria"
|
||||
STR_PAGE_LOAD_ERROR: "Error al cargar la página"
|
||||
STR_EMPTY_FILE: "Archivo vacío"
|
||||
@@ -28,7 +29,10 @@ STR_WIFI_NETWORKS: "Redes Wi-Fi"
|
||||
STR_NO_NETWORKS: "No hay redes disponibles"
|
||||
STR_NETWORKS_FOUND: "%zu red(es) encontrada(s)"
|
||||
STR_SCANNING: "Buscando..."
|
||||
STR_FINDING_SAVED_WIFI: "Buscando redes Wi-Fi guardadas..."
|
||||
STR_CONNECTING: "Conectando..."
|
||||
STR_CONNECTING_SAVED_WIFI: "Conectando a una red Wi-Fi guardada..."
|
||||
STR_SHOW_NETWORKS: "Mostrar"
|
||||
STR_CONNECTED: "¡Conectado!"
|
||||
STR_CONNECTION_FAILED: "Error de conexión"
|
||||
STR_FORGET_NETWORK: "¿Olvidar la red?"
|
||||
@@ -69,6 +73,8 @@ STR_IMAGES: "Imágenes"
|
||||
STR_IMAGES_DISPLAY: "Mostrar"
|
||||
STR_IMAGES_PLACEHOLDER: "Reemplazar"
|
||||
STR_IMAGES_SUPPRESS: "Ocultar"
|
||||
STR_EOB_HOME: "Inicio"
|
||||
STR_EOB_CONTINUE_WITH: "Continuar con"
|
||||
STR_SHORT_PWR_BTN: "Toque corto del encendido"
|
||||
STR_ORIENTATION: "Orientación"
|
||||
STR_SIDE_BTN_LAYOUT: "Función botones laterales (lector)"
|
||||
@@ -98,6 +104,7 @@ STR_USERNAME: "Usuario"
|
||||
STR_PASSWORD: "Contraseña"
|
||||
STR_SYNC_SERVER_URL: "URL del servidor de sinc."
|
||||
STR_DOCUMENT_MATCHING: "Coincidencia de doc."
|
||||
STR_SEND_METADATA: "Enviar metadatos doc."
|
||||
STR_AUTHENTICATE: "Autenticar"
|
||||
STR_KOREADER_USERNAME: "Usuario de KOReader"
|
||||
STR_KOREADER_PASSWORD: "Contraseña de KOReader"
|
||||
@@ -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_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_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_EMPTY_CHAPTER: "Tomt kapitel"
|
||||
STR_INDEXING: "Indexerar"
|
||||
STR_INDEX_FAILED: "Misslyckades att indexera - ogiltig bok"
|
||||
STR_MEMORY_ERROR: "Minnesfel"
|
||||
STR_PAGE_LOAD_ERROR: "Sidladdningsfel"
|
||||
STR_EMPTY_FILE: "Tom fil"
|
||||
@@ -61,7 +62,7 @@ STR_CAT_READER: "Läsare"
|
||||
STR_CAT_CONTROLS: "Kontroller"
|
||||
STR_CAT_SYSTEM: "System"
|
||||
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_HIDE_BATTERY: "Dölj batteriprocent"
|
||||
STR_EXTRA_SPACING: "Extra paragrafmellanrum"
|
||||
@@ -70,6 +71,8 @@ STR_IMAGES: "Bilder"
|
||||
STR_IMAGES_DISPLAY: "Visa"
|
||||
STR_IMAGES_PLACEHOLDER: "Platshållare"
|
||||
STR_IMAGES_SUPPRESS: "Dölj"
|
||||
STR_EOB_HOME: "Hem"
|
||||
STR_EOB_CONTINUE_WITH: "Fortsätt med"
|
||||
STR_SHORT_PWR_BTN: "Kort strömknappsklick"
|
||||
STR_ORIENTATION: "Läsrikting"
|
||||
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_SKIP: "Hoppa över kapitel"
|
||||
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_FAMILY: "Eboksläsarens typsnittsfamilj"
|
||||
STR_FONT_SIZE: "Eboksläsarens typsnittsstorlek"
|
||||
@@ -98,6 +102,7 @@ STR_USERNAME: "Användarnamn"
|
||||
STR_PASSWORD: "Lösenord"
|
||||
STR_SYNC_SERVER_URL: "Synkronisera serveradress"
|
||||
STR_DOCUMENT_MATCHING: "Dokumentmatchning"
|
||||
STR_SEND_METADATA: "Skicka dokumentmetadata"
|
||||
STR_AUTHENTICATE: "Autentisera "
|
||||
STR_KOREADER_USERNAME: "KOReader användarnamn"
|
||||
STR_KOREADER_PASSWORD: "KOReader lösenord"
|
||||
@@ -142,6 +147,8 @@ STR_ORIENTATION_INVERTED: "Porträtt 180°"
|
||||
STR_LANDSCAPE_CCW: "Landskap moturs"
|
||||
STR_PREV_NEXT: "Förra/Nästa"
|
||||
STR_NEXT_PREV: "Nästa/Förra"
|
||||
STR_KOSYNC: "KOSync"
|
||||
STR_BOOKMARK_OPTION: "Bokmärke"
|
||||
STR_DISABLED: "Inaktiverad"
|
||||
STR_NOTO_SERIF: "Noto Serif"
|
||||
STR_NOTO_SANS: "Noto Sans"
|
||||
@@ -255,7 +262,6 @@ STR_THEME_LYRA: "Lyra"
|
||||
STR_THEME_ROUNDEDRAFF: "RoundedRaff"
|
||||
STR_THEME_LYRA_EXTENDED: "Lyra utökad"
|
||||
STR_SUNLIGHT_FADING_FIX: "Fix för solskensmattning"
|
||||
STR_QUICK_RESUME_TIMEOUT: "Snabb återupptagning efter timeout"
|
||||
STR_REMAP_FRONT_BUTTONS: "Ändra frontknappar"
|
||||
STR_BOOKMARKS: "Bokmärken"
|
||||
STR_BOOKMARK_ADDED: "Bokmärke tillagt."
|
||||
@@ -321,15 +327,16 @@ STR_BOOK_S_STYLE: "Bokstil"
|
||||
STR_EMBEDDED_STYLE: "Inbäddad stil"
|
||||
STR_FOCUS_READING: "Fokusläsning"
|
||||
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_FOOTNOTES: "Fotnoter"
|
||||
STR_NO_FOOTNOTES: "Inga fotnoter på den här sidan"
|
||||
STR_LINK: "[länk]"
|
||||
STR_SCREENSHOT_BUTTON: "Ta en skärmdump"
|
||||
STR_SLEEP_TIMER_VALUE_FORMAT: "%u min"
|
||||
STR_SLEEP_NEVER: "Aldrig"
|
||||
STR_STEP_HINT_FRONT: "Framknappar:"
|
||||
STR_STEP_HINT_SIDE: "Sidoknappar:"
|
||||
STR_SCREENSHOT_BUTTON: "Ta en skärmdump"
|
||||
STR_ADD_SERVER: "Lägg till server"
|
||||
STR_SERVER_NAME: "Servernamn"
|
||||
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_RECOVERY_MODE: "Återställningsläge"
|
||||
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)"
|
||||
|
||||
@@ -92,6 +92,7 @@ STR_USERNAME: "Kullanıcı Adı"
|
||||
STR_PASSWORD: "Şifre"
|
||||
STR_SYNC_SERVER_URL: "Senkronizasyon Sunucu Adresi"
|
||||
STR_DOCUMENT_MATCHING: "Belge Eşleştirme"
|
||||
STR_SEND_METADATA: "Belge üst verisi gönder"
|
||||
STR_AUTHENTICATE: "Kimlik Doğrula"
|
||||
STR_KOREADER_USERNAME: "KOReader Kullanıcı Adı"
|
||||
STR_KOREADER_PASSWORD: "KOReader Şifresi"
|
||||
@@ -303,3 +304,5 @@ STR_SELECTED: "Seçili"
|
||||
STR_SHOW: "Göster"
|
||||
STR_TITLE: "Başlık"
|
||||
STR_TILT_PAGE_TURN: "Eğerek sayfa çevirme"
|
||||
STR_ADD_HIDDEN_NETWORK: "Gizli ağ ekle..."
|
||||
STR_ENTER_WIFI_SSID: "Ağ adını girin (SSID)"
|
||||
|
||||
@@ -98,6 +98,7 @@ STR_USERNAME: "Ім'я користувача"
|
||||
STR_PASSWORD: "Пароль"
|
||||
STR_SYNC_SERVER_URL: "URL для синхронізації"
|
||||
STR_DOCUMENT_MATCHING: "Порівняння документів"
|
||||
STR_SEND_METADATA: "Надсилати метадані"
|
||||
STR_AUTHENTICATE: "Автентифікувати"
|
||||
STR_KOREADER_USERNAME: "Ім'я користувача KOReader"
|
||||
STR_KOREADER_PASSWORD: "Пароль KOReader"
|
||||
@@ -380,3 +381,5 @@ STR_FIRMWARE_WRITE_FAILED: "Помилка запису прошивки"
|
||||
STR_FIRMWARE_UPDATE_DO_NOT_POWER_OFF: "Не вимикайте пристрій!"
|
||||
STR_RECOVERY_MODE: "Режим відновлення"
|
||||
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_ENTERING_SLEEP: "Entrant en repòs"
|
||||
STR_BROWSE_FILES: "Explora arxius"
|
||||
STR_FILE_TRANSFER: "Transferència"
|
||||
STR_FILE_TRANSFER: "Transferència d'arxius"
|
||||
STR_SETTINGS_TITLE: "Configuració"
|
||||
STR_CONTINUE_READING: "Continua llegint"
|
||||
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_EMPTY_CHAPTER: "Capítol buit"
|
||||
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_PAGE_LOAD_ERROR: "Error en carregar la pàgina"
|
||||
STR_EMPTY_FILE: "Arxiu buit"
|
||||
@@ -28,60 +29,65 @@ STR_WIFI_NETWORKS: "Xarxes Wi-Fi"
|
||||
STR_NO_NETWORKS: "No s'han trobat xarxes"
|
||||
STR_NETWORKS_FOUND: "%zu xarxes trobades"
|
||||
STR_SCANNING: "S'està escanejant..."
|
||||
STR_FINDING_SAVED_WIFI: "S'estan buscant xarxes Wi-Fi guardades..."
|
||||
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_CONNECTION_FAILED: "Error de connexió"
|
||||
STR_FORGET_NETWORK: "Voleu oblidar esta xarxa?"
|
||||
STR_SAVE_PASSWORD: "Voleu guardar la contrasenya per a la pròxima vegada?"
|
||||
STR_PRESS_OK_SCAN: "Premeu OK per tornar a escanejar"
|
||||
STR_JOIN_NETWORK: "Uneix-te a una xarxa"
|
||||
STR_FORGET_NETWORK: "Vols oblidar esta xarxa?"
|
||||
STR_SAVE_PASSWORD: "Vols guardar la contrasenya per a la pròxima vegada?"
|
||||
STR_PRESS_OK_SCAN: "Prem OK per tornar a escanejar"
|
||||
STR_JOIN_NETWORK: "Unix-te a una xarxa"
|
||||
STR_CREATE_HOTSPOT: "Crea un punt d'accés"
|
||||
STR_JOIN_DESC: "Connecta't a una xarxa Wi-Fi existent"
|
||||
STR_HOTSPOT_DESC: "Crea una xarxa Wi-Fi per unir-s'hi"
|
||||
STR_STARTING_HOTSPOT: "S'està iniciant el 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_OPEN_URL_HINT: "Obriu este URL al navegador"
|
||||
STR_CONNECT_WIFI_HINT: "Connecta el dispositiu a esta xarxa Wi-Fi"
|
||||
STR_OPEN_URL_HINT: "Obri este URL al navegador"
|
||||
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_NETWORK_LEGEND: "* = Encriptat | + = Guardat"
|
||||
STR_MAC_ADDRESS: "Adreça MAC:"
|
||||
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_CALIBRE_RECEIVING: "S'està rebent: "
|
||||
STR_CALIBRE_RECEIVED: "S'ha rebut: "
|
||||
STR_CALIBRE_INSTRUCTION_1: "1) Instal·leu el connector CrossPoint Reader"
|
||||
STR_CALIBRE_INSTRUCTION_2: "2) Estigueu a la mateixa xarxa Wi-Fi"
|
||||
STR_CALIBRE_INSTRUCTION_1: "1) Instal·la el connector CrossPoint Reader"
|
||||
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_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_READER: "Lector"
|
||||
STR_CAT_CONTROLS: "Controls"
|
||||
STR_CAT_SYSTEM: "Sistema"
|
||||
STR_SLEEP_SCREEN: "Pantalla de repòs"
|
||||
STR_QUICK_RESUME_TIMEOUT: "Represa ràpida després del temps"
|
||||
STR_SLEEP_COVER_MODE: "Mode de pantalla de repòs"
|
||||
STR_QUICK_RESUME_TIMEOUT: "Represa ràpida per inactivitat"
|
||||
STR_SLEEP_COVER_MODE: "Ajust de la portada en repòs"
|
||||
STR_HIDE_BATTERY: "Oculta el % de bateria"
|
||||
STR_EXTRA_SPACING: "Espaiat de paràgraf extra"
|
||||
STR_TEXT_AA: "Antialiàsing del text"
|
||||
STR_IMAGES: "Imatges"
|
||||
STR_IMAGES_DISPLAY: "Mostrar"
|
||||
STR_IMAGES_PLACEHOLDER: "Text de mostra"
|
||||
STR_IMAGES_PLACEHOLDER: "Text alternatiu"
|
||||
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_SIDE_BTN_LAYOUT: "Disposició botons laterals"
|
||||
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_SKIP: "Saltar capítols"
|
||||
STR_LONG_PRESS_BEHAVIOR_ORIENTATION: "Canvi d'orientació"
|
||||
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_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_SCREEN_MARGIN: "Marge de pantalla del lector"
|
||||
STR_PARA_ALIGNMENT: "Alineació de paràgrafs del lector"
|
||||
@@ -94,21 +100,22 @@ STR_STEP_HINT_SIDE: "Botons laterals:"
|
||||
STR_SHOW_HIDDEN_FILES: "Mostra arxius ocults"
|
||||
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_REFRESH_FREQ: "Freqüència de refresc"
|
||||
STR_REFRESH_FREQ: "Freqüència d'actualització"
|
||||
STR_KOREADER_SYNC: "Sincronització del KOReader"
|
||||
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_USERNAME: "Nom d'usuari"
|
||||
STR_PASSWORD: "Contrasenya"
|
||||
STR_SYNC_SERVER_URL: "URL del servidor de sincronització"
|
||||
STR_DOCUMENT_MATCHING: "Coincidència de documents"
|
||||
STR_SEND_METADATA: "Envia metadades del document"
|
||||
STR_AUTHENTICATE: "Autentica"
|
||||
STR_KOREADER_USERNAME: "Nom d'usuari del KOReader"
|
||||
STR_KOREADER_PASSWORD: "Contrasenya del KOReader"
|
||||
STR_FILENAME: "Nom d'arxiu"
|
||||
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_AUTHENTICATING: "S'està autenticant..."
|
||||
STR_AUTH_SUCCESS: "Autenticació correcta!"
|
||||
@@ -148,7 +155,7 @@ STR_PREV_NEXT: "Anterior/Següent"
|
||||
STR_NEXT_PREV: "Següent/Anterior"
|
||||
STR_KOSYNC: "KOSync"
|
||||
STR_BOOKMARK_OPTION: "Punt de llibre"
|
||||
STR_DISABLED: "Desactivats"
|
||||
STR_DISABLED: "Sense funció"
|
||||
STR_NOTO_SERIF: "Noto Serif"
|
||||
STR_NOTO_SANS: "Noto Sans"
|
||||
STR_SMALL: "Petita"
|
||||
@@ -176,16 +183,16 @@ STR_UPDATING: "S'està actualitzant..."
|
||||
STR_NO_UPDATE: "No hi ha actualitzacions disponibles"
|
||||
STR_UPDATE_FAILED: "Ha fallat l'actualització"
|
||||
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_DOWNLOADING: "S'està baixant..."
|
||||
STR_DOWNLOAD_FAILED: "Ha fallat la baixada"
|
||||
STR_ERROR_MSG: "Error:"
|
||||
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_FETCH_FEED_FAILED: "Ha fallat l'obtenció del feed"
|
||||
STR_PARSE_FEED_FAILED: "Ha fallat l'anàlisi del feed"
|
||||
STR_FETCH_FEED_FAILED: "Ha fallat l'obtenció del canal de continguts"
|
||||
STR_PARSE_FEED_FAILED: "Ha fallat l'anàlisi del canal de continguts"
|
||||
STR_NETWORK_PREFIX: "Xarxa: "
|
||||
STR_IP_ADDRESS_PREFIX: "Adreça IP: "
|
||||
STR_ERROR_GENERAL_FAILURE: "Error: Fallada general"
|
||||
@@ -198,11 +205,11 @@ STR_HOME: "« Inici"
|
||||
STR_SELECT: "Selecciona"
|
||||
STR_SELECTED: "Seleccionat"
|
||||
STR_TOGGLE: "Canvia"
|
||||
STR_TOGGLE_BOOKMARK: "Canvia punt de llibre"
|
||||
STR_TOGGLE_BOOKMARK: "Afig o elimina el punt de llibre"
|
||||
STR_CONFIRM: "Confirma"
|
||||
STR_CANCEL: "Cancel·la"
|
||||
STR_CONNECT: "Connecta"
|
||||
STR_OPEN: "Obre"
|
||||
STR_OPEN: "Obri"
|
||||
STR_DOWNLOAD: "Descarrega"
|
||||
STR_RETRY: "Reintenta"
|
||||
STR_YES: "Sí"
|
||||
@@ -217,7 +224,7 @@ STR_DIR_RIGHT: "Dreta"
|
||||
STR_DIR_UP: "Amunt"
|
||||
STR_DIR_DOWN: "Avall"
|
||||
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_CUSTOMISE_STATUS_BAR: "Personalitza la barra d'estat"
|
||||
STR_CHAPTER_PAGE_COUNT: "Comptador de pàgines del capítol"
|
||||
@@ -247,20 +254,20 @@ STR_OPDS_BROWSER: "Navegador OPDS"
|
||||
STR_COVER_CUSTOM: "Portada + Personalitzat"
|
||||
STR_QUICK_RESUME: "Represa ràpida"
|
||||
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_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_CALIBRE_STARTING: "S'està iniciant el Calibre..."
|
||||
STR_CALIBRE_SETUP: "Configura"
|
||||
STR_CALIBRE_STATUS: "Estat"
|
||||
STR_CLEAR_BUTTON: "Esborra"
|
||||
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_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_HW_BACK_LABEL: "Arrere (1r botó)"
|
||||
STR_HW_CONFIRM_LABEL: "Confirma (2n botó)"
|
||||
@@ -271,19 +278,19 @@ STR_GO_HOME_BUTTON: "Ves a l'inici"
|
||||
STR_SYNC_PROGRESS: "Sincronitza el progrés"
|
||||
STR_DELETE_CACHE: "Esborra la memòria cau del llibre"
|
||||
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_CHAPTER_PREFIX: "Capítol: "
|
||||
STR_PAGES_SEPARATOR: " pàgines | "
|
||||
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_SYNCING_TIME: "S'està sincronitzant el temps..."
|
||||
STR_CALC_HASH: "S'està calculant el hash del document..."
|
||||
STR_HASH_FAILED: "No s'ha pogut calcular el hash del document"
|
||||
STR_CALC_HASH: "S'està calculant l'empremta electrònica 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_UPLOAD_PROGRESS: "S'està pujant el progrés..."
|
||||
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_REMOTE_LABEL: "Remot:"
|
||||
STR_LOCAL_LABEL: "Local:"
|
||||
@@ -293,7 +300,7 @@ STR_DEVICE_FROM_FORMAT: " De: %s"
|
||||
STR_APPLY_REMOTE: "Aplica el progrés remot"
|
||||
STR_UPLOAD_LOCAL: "Puja el progrés local"
|
||||
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_SYNC_FAILED_MSG: "Sincronització fallida"
|
||||
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_LINK: "[enllaç]"
|
||||
STR_SCREENSHOT_BUTTON: "Fes una captura de pantalla"
|
||||
STR_AUTO_TURN_ENABLED: "Passar automàtic activat: "
|
||||
STR_AUTO_TURN_PAGES_PER_MIN: "Passar automàtic (pàgines per minut)"
|
||||
STR_AUTO_TURN_ENABLED: "Pas automàtic de pàgina activat"
|
||||
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_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_PREV_PAGE: "« Pàgina anterior"
|
||||
STR_XTC_STATUS_BAR: "Barra d'estat XTC"
|
||||
@@ -351,35 +358,37 @@ STR_INSTALLED: "Instal·lat"
|
||||
STR_DOWNLOAD_ALL: "Descarrega-ho tot"
|
||||
STR_UPDATE_ALL: "Actualitza-ho tot"
|
||||
STR_UPDATE_AVAILABLE: "Actualitza"
|
||||
STR_CRASH_TITLE: "Bloqueig 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_REASON: "Motiu del bloqueig:"
|
||||
STR_CRASH_TITLE: "Fallada del sistema"
|
||||
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 de la fallada"
|
||||
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_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_SHOW_PASSWORD: "Mantén premut Dreta i prem [abc] per mostrar 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_TOGGLE_HIDE_PASSWORD: "Prem [***] per ocultar 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_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_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_UPPER_SECONDARY: "Mantén premut SELECT per 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_CLEAR_TEXT: "Mantín premut DEL per esborrar tot el text"
|
||||
STR_KB_HINT_SECONDARY_CHAR: "Mantín premut SELECT: 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: minúscules o caràcter secundari"
|
||||
STR_KB_HINT_URL_SNIPPETS: "Prem URL per inserir fragments"
|
||||
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_VALIDATING_FIRMWARE: "S'està validant el firmware..."
|
||||
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_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_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_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_SYNC_SERVER_URL: "URL máy chủ đồng bộ"
|
||||
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_KOREADER_USERNAME: "Tên đăng nhập KOReader"
|
||||
STR_KOREADER_PASSWORD: "Mật khẩu KOReader"
|
||||
@@ -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_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_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.
|
||||
//
|
||||
// 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:
|
||||
// init(false) — one-shot: input is a contiguous buffer, call read() once.
|
||||
// init(true) — streaming: allocates a 32KB ring buffer for back-references
|
||||
|
||||
@@ -14,6 +14,7 @@ void KOReaderCredentialStore::toJson(JsonDocument& doc) const {
|
||||
doc["password_obf"] = obfuscation::obfuscateToBase64(getPassword());
|
||||
doc["serverUrl"] = getServerUrl();
|
||||
doc["matchMethod"] = static_cast<uint8_t>(getMatchMethod());
|
||||
doc["sendMetadata"] = getSendMetadata();
|
||||
}
|
||||
|
||||
bool KOReaderCredentialStore::fromJson(JsonVariantConst doc) {
|
||||
@@ -32,6 +33,7 @@ bool KOReaderCredentialStore::fromJson(JsonVariantConst doc) {
|
||||
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");
|
||||
@@ -98,3 +100,8 @@ void KOReaderCredentialStore::setMatchMethod(DocumentMatchMethod method) {
|
||||
matchMethod = method;
|
||||
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");
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@ class KOReaderCredentialStore : public PersistableStore<KOReaderCredentialStore>
|
||||
std::string password;
|
||||
std::string serverUrl; // Custom sync server URL (empty = default)
|
||||
DocumentMatchMethod matchMethod = DocumentMatchMethod::FILENAME; // Default to filename for compatibility
|
||||
bool sendMetadata = false; // Send document metadata with progress sync
|
||||
|
||||
// Private constructor for singleton
|
||||
KOReaderCredentialStore() = default;
|
||||
@@ -60,6 +61,10 @@ class KOReaderCredentialStore : public PersistableStore<KOReaderCredentialStore>
|
||||
// Document matching method
|
||||
void setMatchMethod(DocumentMatchMethod method);
|
||||
DocumentMatchMethod getMatchMethod() const { return matchMethod; }
|
||||
|
||||
// Send metadata setting
|
||||
void setSendMetadata(bool enabled);
|
||||
bool getSendMetadata() const { return sendMetadata; }
|
||||
};
|
||||
|
||||
// Helper macro to access credential store
|
||||
|
||||
@@ -2,10 +2,10 @@
|
||||
|
||||
#include <ArduinoJson.h>
|
||||
#include <Logging.h>
|
||||
#include <esp_crt_bundle.h>
|
||||
#include <esp_http_client.h>
|
||||
#include <SecureHttpClient.h>
|
||||
#include <base64.h>
|
||||
|
||||
#include <ctime>
|
||||
#include <string>
|
||||
|
||||
#include "KOReaderCredentialStore.h"
|
||||
|
||||
@@ -16,82 +16,49 @@ namespace {
|
||||
constexpr char DEVICE_NAME[] = "CrossPoint";
|
||||
constexpr char DEVICE_ID[] = "crosspoint-reader";
|
||||
|
||||
// Small TLS buffers to fit in ESP32-C3's limited heap (~46KB free after WiFi).
|
||||
// KOSync payloads are tiny JSON (<1KB), so 2KB buffers are sufficient.
|
||||
// Default 16KB buffers cause OOM during TLS handshake.
|
||||
constexpr int HTTP_BUF_SIZE = 2048;
|
||||
// KOSync's TLS-1.3 servers can't be reached through the precompiled system
|
||||
// mbedTLS (TLS 1.3 is stubbed out), so requests run over wolfSSL via
|
||||
// SecureHttpClient. The handshake still needs working heap; gate on it. wolfSSL's
|
||||
// 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
|
||||
// heap does not fall through into a failed TLS allocation path.
|
||||
// MEMFIX-PORT: TLS heap gate; portable
|
||||
// Field data (July 2026): launching sync from a reader session lands at
|
||||
// 51.9-58.2 KB free / 42-53 KB maxAlloc after WiFi comes up. wolfSSL handles
|
||||
// allocation failure by returning MEMORY_E (no abort under -fno-exceptions),
|
||||
// so an optimistic attempt degrades to the same clean "sync failed" as the
|
||||
// gate — the gate only needs to keep out states where a doomed handshake
|
||||
// would waste tens of seconds, not guarantee success.
|
||||
//
|
||||
// Free and largest-block have separate requirements: with SP ECC
|
||||
// (WOLFSSL_HAVE_SP_ECC) the handshake's crypto uses fixed 256-bit arrays, so
|
||||
// the largest single TLS allocation is the ~17 KB wolfSSL record buffer, not
|
||||
// a run of fast-math bignums. A handshake was measured succeeding inside a
|
||||
// 43 KB largest block; requiring 50 KB contiguous refused syncs that fit.
|
||||
constexpr uint32_t MIN_FREE_FOR_TLS = 50000;
|
||||
constexpr uint32_t MIN_BLOCK_FOR_TLS = 20000;
|
||||
|
||||
// Cloudflare tunnels send a 3-cert Google Trust Services chain. During the TLS handshake
|
||||
// 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;
|
||||
|
||||
// Response buffer for reading HTTP body
|
||||
struct ResponseBuffer {
|
||||
char* data = nullptr;
|
||||
int len = 0;
|
||||
int capacity = 0;
|
||||
|
||||
~ResponseBuffer() { free(data); }
|
||||
|
||||
bool ensure(int size) {
|
||||
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;
|
||||
// Apply the shared KOSync auth headers after begin(). x-auth-* is the native
|
||||
// KOSync scheme; Basic auth is added for Calibre-Web-Automated compatibility.
|
||||
void applyAuthHeaders(freeink::SecureHttpClient& http) {
|
||||
http.addHeader("Accept", "application/vnd.koreader.v1+json");
|
||||
http.addHeader("x-auth-user", KOREADER_STORE.getUsername());
|
||||
http.addHeader("x-auth-key", KOREADER_STORE.getMd5Password());
|
||||
const std::string credentials = KOREADER_STORE.getUsername() + ":" + KOREADER_STORE.getPassword();
|
||||
const String encoded = base64::encode(credentials.c_str());
|
||||
http.addHeader("Authorization", std::string("Basic ") + encoded.c_str());
|
||||
}
|
||||
|
||||
// Create configured esp_http_client with small TLS buffers
|
||||
esp_http_client_handle_t createClient(const char* url, ResponseBuffer* buf,
|
||||
esp_http_client_method_t method = HTTP_METHOD_GET) {
|
||||
esp_http_client_config_t config = {};
|
||||
config.url = url;
|
||||
config.event_handler = httpEventHandler;
|
||||
config.user_data = buf;
|
||||
config.method = method;
|
||||
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;
|
||||
// True when free heap is too low to risk a TLS handshake.
|
||||
bool insufficientHeap() {
|
||||
const uint32_t freeHeap = ESP.getFreeHeap();
|
||||
const uint32_t maxAllocHeap = ESP.getMaxAllocHeap();
|
||||
if (freeHeap < MIN_FREE_FOR_TLS || maxAllocHeap < MIN_BLOCK_FOR_TLS) {
|
||||
LOG_ERR("KOSync", "Insufficient heap for TLS handshake: %u bytes free (need %u), %u max alloc (need %u)", freeHeap,
|
||||
MIN_FREE_FOR_TLS, maxAllocHeap, MIN_BLOCK_FOR_TLS);
|
||||
return true;
|
||||
}
|
||||
|
||||
return client;
|
||||
return false;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
@@ -102,26 +69,24 @@ KOReaderSyncClient::Error KOReaderSyncClient::authenticate() {
|
||||
return NO_CREDENTIALS;
|
||||
}
|
||||
|
||||
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)freeHeap);
|
||||
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;
|
||||
const std::string url = KOREADER_STORE.getBaseUrl() + "/users/auth";
|
||||
LOG_DBG("KOSync", "Authenticating: %s (heap: %u)", url.c_str(), (unsigned)ESP.getFreeHeap());
|
||||
if (insufficientHeap()) return LOW_MEMORY;
|
||||
|
||||
freeink::SecureHttpClient http;
|
||||
http.setInsecure();
|
||||
if (!http.begin(url)) {
|
||||
LOG_ERR("KOSync", "Bad URL: %s", url.c_str());
|
||||
return NETWORK_ERROR;
|
||||
}
|
||||
|
||||
ResponseBuffer buf;
|
||||
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);
|
||||
applyAuthHeaders(http);
|
||||
const int httpCode = http.GET();
|
||||
http.end();
|
||||
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 == 401) return AUTH_FAILED;
|
||||
return SERVER_ERROR;
|
||||
@@ -135,30 +100,31 @@ KOReaderSyncClient::Error KOReaderSyncClient::getProgress(const std::string& doc
|
||||
return NO_CREDENTIALS;
|
||||
}
|
||||
|
||||
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)freeHeap);
|
||||
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;
|
||||
const std::string url = KOREADER_STORE.getBaseUrl() + "/syncs/progress/" + documentHash;
|
||||
LOG_DBG("KOSync", "Getting progress: %s (heap: %u)", url.c_str(), (unsigned)ESP.getFreeHeap());
|
||||
if (insufficientHeap()) return LOW_MEMORY;
|
||||
|
||||
freeink::SecureHttpClient http;
|
||||
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;
|
||||
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) {
|
||||
if (httpCode == 200) {
|
||||
JsonDocument doc;
|
||||
const DeserializationError error = deserializeJson(doc, buf.data);
|
||||
const DeserializationError error = deserializeJson(doc, http.getString().c_str());
|
||||
http.end();
|
||||
|
||||
if (error) {
|
||||
LOG_ERR("KOSync", "JSON parse failed: %s", error.c_str());
|
||||
@@ -176,6 +142,7 @@ KOReaderSyncClient::Error KOReaderSyncClient::getProgress(const std::string& doc
|
||||
return OK;
|
||||
}
|
||||
|
||||
http.end();
|
||||
if (httpCode == 401) return AUTH_FAILED;
|
||||
if (httpCode == 404) return NOT_FOUND;
|
||||
return SERVER_ERROR;
|
||||
@@ -188,17 +155,19 @@ KOReaderSyncClient::Error KOReaderSyncClient::updateProgress(const KOReaderProgr
|
||||
return NO_CREDENTIALS;
|
||||
}
|
||||
|
||||
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)freeHeap);
|
||||
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;
|
||||
}
|
||||
const std::string url = KOREADER_STORE.getBaseUrl() + "/syncs/progress";
|
||||
LOG_DBG("KOSync", "Updating progress: %s (heap: %u)", url.c_str(), (unsigned)ESP.getFreeHeap());
|
||||
if (insufficientHeap()) return LOW_MEMORY;
|
||||
|
||||
// Build JSON body
|
||||
JsonDocument doc;
|
||||
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["percentage"] = progress.percentage;
|
||||
doc["device"] = DEVICE_NAME;
|
||||
@@ -209,25 +178,21 @@ KOReaderSyncClient::Error KOReaderSyncClient::updateProgress(const KOReaderProgr
|
||||
|
||||
LOG_DBG("KOSync", "Request body: %s", body.c_str());
|
||||
|
||||
ResponseBuffer buf;
|
||||
esp_http_client_handle_t client = createClient(url.c_str(), &buf, HTTP_METHOD_PUT);
|
||||
if (!client) return NETWORK_ERROR;
|
||||
|
||||
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);
|
||||
freeink::SecureHttpClient http;
|
||||
http.setInsecure();
|
||||
if (!http.begin(url)) {
|
||||
LOG_ERR("KOSync", "Bad URL: %s", url.c_str());
|
||||
return NETWORK_ERROR;
|
||||
}
|
||||
|
||||
esp_err_t err = esp_http_client_perform(client);
|
||||
const int httpCode = esp_http_client_get_status_code(client);
|
||||
applyAuthHeaders(http);
|
||||
http.addHeader("Content-Type", "application/json");
|
||||
const int httpCode = http.sendRequest("PUT", body);
|
||||
http.end();
|
||||
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 == 401) return AUTH_FAILED;
|
||||
return SERVER_ERROR;
|
||||
|
||||
@@ -1,16 +1,30 @@
|
||||
#pragma once
|
||||
#include <cstdint>
|
||||
#include <optional>
|
||||
#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.
|
||||
*/
|
||||
struct KOReaderProgress {
|
||||
std::string document; // Document hash
|
||||
std::string progress; // XPath-like progress string
|
||||
float percentage; // Progress percentage (0.0 to 1.0)
|
||||
std::string device; // Device name
|
||||
std::string deviceId; // Device ID
|
||||
int64_t timestamp; // Unix timestamp of last update
|
||||
std::string document; // Document hash
|
||||
std::string progress; // XPath-like progress string
|
||||
float percentage; // Progress percentage (0.0 to 1.0)
|
||||
std::string device; // Device name
|
||||
std::string deviceId; // Device ID
|
||||
int64_t timestamp; // Unix timestamp of last update
|
||||
std::optional<KOReaderMetadata> metadata; // Optional document metadata
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -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 <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 {
|
||||
|
||||
@@ -68,11 +75,25 @@ int detectParagraphLevel(const char* utf8, const int fallbackLevel, const int ma
|
||||
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) {
|
||||
if (!utf8 || !*utf8) return false;
|
||||
const std::lock_guard<std::mutex> lock(bidiMutex);
|
||||
|
||||
static bidi_char line[BIDI_MAX_LINE];
|
||||
static bidi_char shaped[BIDI_MAX_LINE];
|
||||
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);
|
||||
while (*p) {
|
||||
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;
|
||||
line[count].origwc = line[count].wc = cp;
|
||||
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++;
|
||||
}
|
||||
if (!count) return false;
|
||||
|
||||
const bool autodir = (paragraphLevel < 0);
|
||||
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_shape(line, shaped, count);
|
||||
|
||||
out.clear();
|
||||
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++) {
|
||||
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;
|
||||
}
|
||||
@@ -105,6 +174,7 @@ bool computeVisualWordOrder(const std::vector<std::string>& words, bool paragrap
|
||||
visualOrder.clear();
|
||||
const size_t nWords = words.size();
|
||||
if (nWords <= 1 || nWords > BIDI_MAX_LINE) return false;
|
||||
const std::lock_guard<std::mutex> lock(bidiMutex);
|
||||
|
||||
static bidi_char line[BIDI_MAX_LINE];
|
||||
int count = 0;
|
||||
@@ -121,6 +191,7 @@ bool computeVisualWordOrder(const std::vector<std::string>& words, bool paragrap
|
||||
if (!cp || cp == REPLACEMENT_GLYPH) break;
|
||||
line[count].origwc = line[count].wc = cp;
|
||||
line[count].index = static_cast<uint16_t>(w);
|
||||
line[count].joiners = 0;
|
||||
count++;
|
||||
}
|
||||
|
||||
@@ -131,6 +202,7 @@ bool computeVisualWordOrder(const std::vector<std::string>& words, bool paragrap
|
||||
}
|
||||
line[count].origwc = line[count].wc = ' ';
|
||||
line[count].index = static_cast<uint16_t>(nWords);
|
||||
line[count].joiners = 0;
|
||||
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);
|
||||
|
||||
// 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
|
||||
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:
|
||||
* Hebrew + English is the primary target. However, CrossPoint renders
|
||||
* Latin and Cyrillic scripts for many other languages, so these MUST be
|
||||
* classified as L (not fall through to ON) to avoid regression when they
|
||||
* appear adjacent to Hebrew runs.
|
||||
* Hebrew and Arabic-script languages (Arabic, Farsi, Urdu, Sindhi, Pashto,
|
||||
* Kurdish) are the RTL targets. CrossPoint also renders Latin and Cyrillic
|
||||
* scripts for many other languages, so these MUST be classified as L (not
|
||||
* 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 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.
|
||||
*
|
||||
* 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).
|
||||
*/
|
||||
|
||||
@@ -84,6 +88,36 @@
|
||||
{0x05D0, 0x05EA, R}, /* alef … tav */
|
||||
{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) ─────────────────────────────────── */
|
||||
/* Covers accented chars for Vietnamese, Welsh, Romanian, etc.
|
||||
Not currently rendered by CrossPoint fonts, but costs only 2 table rows. */
|
||||
@@ -110,6 +144,14 @@
|
||||
{0x2069, 0x2069, PDI},
|
||||
{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 ────────────────────────────────────────────────── */
|
||||
{0xFEFF, 0xFEFF, BN},
|
||||
|
||||
@@ -127,6 +127,345 @@ ucschar mirror(ucschar c) {
|
||||
return p ? p->to : c;
|
||||
}
|
||||
|
||||
/* ═══════════════════════════════════════════════════════════════════════
|
||||
* Arabic contextual shaping — do_shape()
|
||||
*
|
||||
* Ported from mintty src/minibidi.c (https://github.com/mintty/mintty),
|
||||
* original author Ahmad Khalifa (www.arabeyes.org), maintained by
|
||||
* Thomas Wolff. MIT licence.
|
||||
*
|
||||
* CrossPoint deviations from the upstream code, each marked inline:
|
||||
* 1. Joining-context lookups skip NSM marks (harakat). mintty stores
|
||||
* combining marks out-of-line in terminal cells, so they never appear
|
||||
* in its bidi_char array; in CrossPoint they are real array entries
|
||||
* and must be transparent for joining (Unicode joining type T).
|
||||
* 2. The Alef absorbed into a Lam-Alef ligature is overwritten with
|
||||
* LIGATURE_PLACEHOLDER instead of a space: a terminal must keep the
|
||||
* cell, a proportional-text renderer must drop the character.
|
||||
* 3. The STYPE/SISOLATED macros became functions backed by a second
|
||||
* lookup table covering Perso-Arabic letters outside mintty's native
|
||||
* U+0621–U+064A range (Farsi پ چ ژ گ, Urdu ٹ ڈ ڑ ں ہ ے, plus Sindhi/
|
||||
* Pashto/Kurdish letters). Joining types are sourced from Unicode
|
||||
* ArabicShaping.txt and presentation forms from UnicodeData.txt
|
||||
* (Arabic Presentation Forms-A), both Unicode 17.0.0. Letters with a
|
||||
* joining type but no presentation-form codepoints keep their base
|
||||
* codepoint (neighbours still shape correctly around them).
|
||||
* U+200C/U+200D also get their ArabicShaping.txt types (U and C) so
|
||||
* that in-stream ZWJ/ZWNJ — which mintty never has in its array —
|
||||
* affect adjacency the same way the joiners flags do.
|
||||
* ═══════════════════════════════════════════════════════════════════════ */
|
||||
|
||||
/* Shaping Types (mintty) */
|
||||
enum {
|
||||
SL, /* Left-Joining, doesn't exist in U+0600 - U+06FF */
|
||||
SR, /* Right-Joining, i.e. has Isolated, Final */
|
||||
SD, /* Dual-Joining, i.e. has Isolated, Final, Initial, Medial */
|
||||
SU, /* Non-Joining */
|
||||
SC /* Join-Causing, like U+0640 (TATWEEL) */
|
||||
};
|
||||
|
||||
typedef struct {
|
||||
uchar type;
|
||||
uchar form_b; /* isolated form = 0xFE00 + form_b (Presentation Forms-B) */
|
||||
} shape_node;
|
||||
|
||||
/* Kept near the actual table, for verification. (mintty) */
|
||||
enum { SHAPE_FIRST = 0x621, SHAPE_LAST = 0x64A };
|
||||
|
||||
/* mintty's shapetypes[] — verbatim */
|
||||
static const shape_node shapetypes[] = {
|
||||
/* index, Typ, Iso, Ligature Index */
|
||||
/* 621 */ {SU, 0x80},
|
||||
/* 622 */ {SR, 0x81},
|
||||
/* 623 */ {SR, 0x83},
|
||||
/* 624 */ {SR, 0x85},
|
||||
/* 625 */ {SR, 0x87},
|
||||
/* 626 */ {SD, 0x89},
|
||||
/* 627 */ {SR, 0x8D},
|
||||
/* 628 */ {SD, 0x8F},
|
||||
/* 629 */ {SR, 0x93},
|
||||
/* 62A */ {SD, 0x95},
|
||||
/* 62B */ {SD, 0x99},
|
||||
/* 62C */ {SD, 0x9D},
|
||||
/* 62D */ {SD, 0xA1},
|
||||
/* 62E */ {SD, 0xA5},
|
||||
/* 62F */ {SR, 0xA9},
|
||||
/* 630 */ {SR, 0xAB},
|
||||
/* 631 */ {SR, 0xAD},
|
||||
/* 632 */ {SR, 0xAF},
|
||||
/* 633 */ {SD, 0xB1},
|
||||
/* 634 */ {SD, 0xB5},
|
||||
/* 635 */ {SD, 0xB9},
|
||||
/* 636 */ {SD, 0xBD},
|
||||
/* 637 */ {SD, 0xC1},
|
||||
/* 638 */ {SD, 0xC5},
|
||||
/* 639 */ {SD, 0xC9},
|
||||
/* 63A */ {SD, 0xCD},
|
||||
/* 63B */ {SU, 0x0},
|
||||
/* 63C */ {SU, 0x0},
|
||||
/* 63D */ {SU, 0x0},
|
||||
/* 63E */ {SU, 0x0},
|
||||
/* 63F */ {SU, 0x0},
|
||||
/* 640 */ {SC, 0x0},
|
||||
/* 641 */ {SD, 0xD1},
|
||||
/* 642 */ {SD, 0xD5},
|
||||
/* 643 */ {SD, 0xD9},
|
||||
/* 644 */ {SD, 0xDD},
|
||||
/* 645 */ {SD, 0xE1},
|
||||
/* 646 */ {SD, 0xE5},
|
||||
/* 647 */ {SD, 0xE9},
|
||||
/* 648 */ {SR, 0xED},
|
||||
/* 649 */ {SR, 0xEF},
|
||||
/* 64A */ {SD, 0xF1}};
|
||||
|
||||
/* ── CrossPoint extension: joining types outside U+0621–U+064A ──────────
|
||||
* Source: Unicode ArabicShaping.txt (joining type column). Ranges of
|
||||
* letters sharing one type are collapsed. Sorted ascending (binary
|
||||
* search). Anything not listed here or in shapetypes[] is SU. */
|
||||
static const struct {
|
||||
ucschar first, last;
|
||||
uchar type;
|
||||
} xjointypes[] = {
|
||||
{0x0620, 0x0620, SD}, /* kashmiri yeh */
|
||||
{0x066E, 0x066F, SD}, /* dotless beh/qaf */
|
||||
{0x0671, 0x0673, SR}, /* alef wasla + wavy-hamza alefs */
|
||||
{0x0675, 0x0677, SR}, /* high-hamza alef/waw */
|
||||
{0x0678, 0x0687, SD}, /* high-hamza yeh, beh group (ٹ ٺ ٻ … پ), hah group (چ ڇ) */
|
||||
{0x0688, 0x0699, SR}, /* dal group (ڈ ڊ ڌ …), reh group (ڑ ړ ژ …) */
|
||||
{0x069A, 0x06BF, SD}, /* seen/sad/feh/qaf/kaf/gaf/lam/noon groups (ک گ ں ھ …) */
|
||||
{0x06C0, 0x06C0, SR}, /* heh with yeh above */
|
||||
{0x06C1, 0x06C2, SD}, /* heh goal (ہ) */
|
||||
{0x06C3, 0x06CB, SR}, /* teh marbuta goal, waw group (ۆ ۇ ۋ …) */
|
||||
{0x06CC, 0x06CC, SD}, /* farsi yeh (ی) */
|
||||
{0x06CD, 0x06CD, SR}, /* yeh with tail */
|
||||
{0x06CE, 0x06CE, SD}, /* farsi yeh with V (Kurdish ێ) */
|
||||
{0x06CF, 0x06CF, SR}, /* waw with dot above */
|
||||
{0x06D0, 0x06D1, SD}, /* e (Pashto ې), yeh three dots below */
|
||||
{0x06D2, 0x06D3, SR}, /* yeh barree (ے ۓ) */
|
||||
{0x06D5, 0x06D5, SR}, /* ae (Kurdish ە) */
|
||||
{0x06EE, 0x06EF, SR}, /* dal/reh with inverted V */
|
||||
{0x06FA, 0x06FC, SD}, /* sheen/dad/ghain with dot below */
|
||||
{0x06FF, 0x06FF, SD}, /* heh with inverted V */
|
||||
{0x200C, 0x200C, SU}, /* ZWNJ — joining type U per ArabicShaping.txt */
|
||||
{0x200D, 0x200D, SC}, /* ZWJ — joining type C per ArabicShaping.txt */
|
||||
};
|
||||
|
||||
/* ── CrossPoint extension: presentation forms outside U+0621–U+064A ─────
|
||||
* Source: UnicodeData.txt, Arabic Presentation Forms-A (U+FB50–U+FBFF).
|
||||
* forms = number of consecutive presentation forms allocated for the
|
||||
* letter, always in the order isolated, final, initial, medial (matching
|
||||
* the SHAPE_* offsets below): 2 = isolated+final, 4 = all.
|
||||
* Letters absent here have no presentation forms and keep their base
|
||||
* codepoint. Sorted ascending (binary search). */
|
||||
static const struct {
|
||||
ucschar cp;
|
||||
ucschar isolated;
|
||||
uchar forms;
|
||||
} xshapeforms[] = {
|
||||
{0x0671, 0xFB50, 2}, /* alef wasla */
|
||||
{0x0679, 0xFB66, 4}, /* tteh (Urdu ٹ) */
|
||||
{0x067A, 0xFB5E, 4}, /* tteheh */
|
||||
{0x067B, 0xFB52, 4}, /* beeh (Sindhi ٻ) */
|
||||
{0x067E, 0xFB56, 4}, /* peh (Farsi پ) */
|
||||
{0x067F, 0xFB62, 4}, /* teheh */
|
||||
{0x0680, 0xFB5A, 4}, /* beheh (Sindhi ڀ) */
|
||||
{0x0683, 0xFB76, 4}, /* nyeh (Sindhi ڃ) */
|
||||
{0x0684, 0xFB72, 4}, /* dyeh (Sindhi ڄ) */
|
||||
{0x0686, 0xFB7A, 4}, /* tcheh (Farsi چ) */
|
||||
{0x0687, 0xFB7E, 4}, /* tcheheh (Sindhi ڇ) */
|
||||
{0x0688, 0xFB88, 2}, /* ddal (Urdu ڈ) */
|
||||
{0x068C, 0xFB84, 2}, /* dahal (Sindhi ڌ) */
|
||||
{0x068D, 0xFB82, 2}, /* ddahal (Sindhi ڍ) */
|
||||
{0x068E, 0xFB86, 2}, /* dul (Sindhi ڎ) */
|
||||
{0x0691, 0xFB8C, 2}, /* rreh (Urdu ڑ) */
|
||||
{0x0698, 0xFB8A, 2}, /* jeh (Farsi ژ) */
|
||||
{0x06A4, 0xFB6A, 4}, /* veh (Kurdish ڤ) */
|
||||
{0x06A6, 0xFB6E, 4}, /* peheh (Sindhi ڦ) */
|
||||
{0x06A9, 0xFB8E, 4}, /* keheh (Farsi/Urdu ک) */
|
||||
{0x06AD, 0xFBD3, 4}, /* ng */
|
||||
{0x06AF, 0xFB92, 4}, /* gaf (Farsi/Urdu گ) */
|
||||
{0x06B1, 0xFB9A, 4}, /* ngoeh (Sindhi ڱ) */
|
||||
{0x06B3, 0xFB96, 4}, /* gueh (Sindhi ڳ) */
|
||||
{0x06BA, 0xFB9E, 2}, /* noon ghunna (Urdu ں) — dual-joining but only
|
||||
isolated+final forms exist; initial/medial
|
||||
contexts keep the base codepoint */
|
||||
{0x06BB, 0xFBA0, 4}, /* rnoon (Sindhi ڻ) */
|
||||
{0x06BE, 0xFBAA, 4}, /* heh doachashmee (Urdu ھ) */
|
||||
{0x06C0, 0xFBA4, 2}, /* heh with yeh above */
|
||||
{0x06C1, 0xFBA6, 4}, /* heh goal (Urdu ہ) */
|
||||
{0x06C5, 0xFBE0, 2}, /* kirghiz oe */
|
||||
{0x06C6, 0xFBD9, 2}, /* oe (Kurdish ۆ) */
|
||||
{0x06C7, 0xFBD7, 2}, /* u (ۇ) */
|
||||
{0x06C8, 0xFBDB, 2}, /* yu */
|
||||
{0x06C9, 0xFBE2, 2}, /* kirghiz yu */
|
||||
{0x06CB, 0xFBDE, 2}, /* ve */
|
||||
{0x06CC, 0xFBFC, 4}, /* farsi yeh (Farsi/Urdu ی) */
|
||||
{0x06D0, 0xFBE4, 4}, /* e (Pashto ې) */
|
||||
{0x06D2, 0xFBAE, 2}, /* yeh barree (Urdu ے) */
|
||||
{0x06D3, 0xFBB0, 2}, /* yeh barree with hamza above (ۓ) */
|
||||
};
|
||||
|
||||
/* Contextual form offsets from the isolated form — identical ordering in
|
||||
Presentation Forms-A and -B, matching mintty's SFINAL/SINITIAL/SMEDIAL
|
||||
(+1/+2/+3) macros. */
|
||||
enum { SHAPE_ISOLATED = 0, SHAPE_FINAL = 1, SHAPE_INITIAL = 2, SHAPE_MEDIAL = 3 };
|
||||
|
||||
/* STYPE equivalent (mintty macro → function to add the extended table) */
|
||||
static uchar stype(ucschar c) {
|
||||
if (c >= SHAPE_FIRST && c <= SHAPE_LAST) return shapetypes[c - SHAPE_FIRST].type;
|
||||
|
||||
int i = -1, j = lengthof(xjointypes);
|
||||
while (j - i > 1) {
|
||||
int k = (i + j) / 2;
|
||||
if (c < xjointypes[k].first)
|
||||
j = k;
|
||||
else if (c > xjointypes[k].last)
|
||||
i = k;
|
||||
else
|
||||
return xjointypes[k].type;
|
||||
}
|
||||
return SU;
|
||||
}
|
||||
|
||||
/* SISOLATED/SFINAL/SINITIAL/SMEDIAL equivalent.
|
||||
form is one of the SHAPE_* offsets; returns c unchanged when the letter
|
||||
has no presentation form allocated for that context. */
|
||||
static ucschar shape_form(ucschar c, uchar form) {
|
||||
if (c >= SHAPE_FIRST && c <= SHAPE_LAST) {
|
||||
const uchar form_b = shapetypes[c - SHAPE_FIRST].form_b;
|
||||
return form_b ? 0xFE00 + form_b + form : c;
|
||||
}
|
||||
|
||||
int i = -1, j = lengthof(xshapeforms);
|
||||
while (j - i > 1) {
|
||||
int k = (i + j) / 2;
|
||||
if (c < xshapeforms[k].cp)
|
||||
j = k;
|
||||
else if (c > xshapeforms[k].cp)
|
||||
i = k;
|
||||
else
|
||||
return form < xshapeforms[k].forms ? xshapeforms[k].isolated + form : c;
|
||||
}
|
||||
return c;
|
||||
}
|
||||
|
||||
/* CrossPoint deviation 1: NSM marks (harakat) sit between letters in our
|
||||
array but are transparent for joining (Unicode joining type T). These
|
||||
helpers find the effective joining neighbour. */
|
||||
static int next_non_nsm(const bidi_char* line, int i, int count) {
|
||||
int j = i + 1;
|
||||
while (j < count && bidi_class(line[j].wc) == NSM) j++;
|
||||
return j;
|
||||
}
|
||||
|
||||
static int prev_non_nsm(const bidi_char* line, int i) {
|
||||
int j = i - 1;
|
||||
while (j >= 0 && bidi_class(line[j].wc) == NSM) j--;
|
||||
return j;
|
||||
}
|
||||
|
||||
/* The Main shaping function (mintty, structure preserved).
|
||||
*
|
||||
* line: visual-order buffer — must have been passed through do_bidi() first
|
||||
* to: output buffer for the shaped data
|
||||
* count: number of characters in line
|
||||
*/
|
||||
int do_shape(bidi_char* line, bidi_char* to, int count) {
|
||||
for (int i = 0; i < count; i++) {
|
||||
to[i] = line[i];
|
||||
int tempShape = stype(line[i].wc);
|
||||
switch (tempShape) {
|
||||
case SR: { /* Right-Joining, i.e. has Isolated, Final */
|
||||
const int nx = next_non_nsm(line, i, count); /* deviation 1: was i + 1 */
|
||||
tempShape = (nx < count) ? stype(line[nx].wc) : SU;
|
||||
if (tempShape == SL || tempShape == SD || tempShape == SC)
|
||||
to[i].wc = shape_form(line[i].wc, SHAPE_FINAL);
|
||||
else
|
||||
to[i].wc = shape_form(line[i].wc, SHAPE_ISOLATED);
|
||||
break;
|
||||
}
|
||||
case SD: { /* Dual-Joining, i.e. has Isolated, Final, Initial, Medial */
|
||||
const int nx = next_non_nsm(line, i, count); /* deviation 1: was i + 1 */
|
||||
const int pv = prev_non_nsm(line, i); /* deviation 1: was i - 1 */
|
||||
|
||||
/* Make Ligatures */
|
||||
tempShape = (nx < count) ? stype(line[nx].wc) : SU;
|
||||
if (line[i].wc == 0x644) { /* Lam: the Alef variant is at pv (visually left) */
|
||||
int ligFlag = 0;
|
||||
switch (pv >= 0 ? line[pv].wc : 0) {
|
||||
case 0x622: /* Alef with Madda above → لآ */
|
||||
ligFlag = 1;
|
||||
to[i].wc = (tempShape == SL || tempShape == SD || tempShape == SC) ? 0xFEF6 : 0xFEF5;
|
||||
break;
|
||||
case 0x623: /* Alef with Hamza above → لأ */
|
||||
ligFlag = 1;
|
||||
to[i].wc = (tempShape == SL || tempShape == SD || tempShape == SC) ? 0xFEF8 : 0xFEF7;
|
||||
break;
|
||||
case 0x625: /* Alef with Hamza below → لإ */
|
||||
ligFlag = 1;
|
||||
to[i].wc = (tempShape == SL || tempShape == SD || tempShape == SC) ? 0xFEFA : 0xFEF9;
|
||||
break;
|
||||
case 0x627: /* Alef → لا */
|
||||
ligFlag = 1;
|
||||
to[i].wc = (tempShape == SL || tempShape == SD || tempShape == SC) ? 0xFEFC : 0xFEFB;
|
||||
break;
|
||||
}
|
||||
if (ligFlag) {
|
||||
to[pv].wc = LIGATURE_PLACEHOLDER; /* deviation 2: mintty writes 0x20 */
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/* Arabic joining formatters: adapt forms (mintty) */
|
||||
const uchar joiners = line[i].joiners & 0xF;
|
||||
const uchar prevjoiners = line[i].joiners >> 4;
|
||||
if (prevjoiners == ZWNJ) {
|
||||
/* backward join blocked; initial only if the visually-right
|
||||
(logically next) neighbour joins, else isolated */
|
||||
tempShape = (pv >= 0) ? stype(line[pv].wc) : SU;
|
||||
if (tempShape == SR || tempShape == SD || tempShape == SC)
|
||||
to[i].wc = shape_form(line[i].wc, SHAPE_INITIAL);
|
||||
else
|
||||
to[i].wc = shape_form(line[i].wc, SHAPE_ISOLATED);
|
||||
} else if (prevjoiners == (ZWJ | ZWNJ)) {
|
||||
to[i].wc = shape_form(line[i].wc, SHAPE_MEDIAL);
|
||||
} else if (prevjoiners == ZWJ) {
|
||||
to[i].wc = shape_form(line[i].wc, SHAPE_FINAL);
|
||||
} else if (joiners & ZWNJ) {
|
||||
/* forward join blocked; final only if the visually-left
|
||||
(logically previous) neighbour joins, else isolated —
|
||||
tempShape still holds stype(nx) from above */
|
||||
if (tempShape == SL || tempShape == SD || tempShape == SC)
|
||||
to[i].wc = shape_form(line[i].wc, SHAPE_FINAL);
|
||||
else
|
||||
to[i].wc = shape_form(line[i].wc, SHAPE_ISOLATED);
|
||||
} else if (tempShape == SL || tempShape == SD || tempShape == SC) {
|
||||
/* visually-right neighbour joins → final, or medial if the
|
||||
visually-left neighbour joins too */
|
||||
tempShape = (pv >= 0) ? stype(line[pv].wc) : SU;
|
||||
if (tempShape == SR || tempShape == SD || tempShape == SC)
|
||||
to[i].wc = shape_form(line[i].wc, SHAPE_MEDIAL);
|
||||
else
|
||||
to[i].wc = shape_form(line[i].wc, SHAPE_FINAL);
|
||||
} else {
|
||||
/* visually-right neighbour doesn't join → isolated, or initial
|
||||
if the visually-left neighbour joins */
|
||||
tempShape = (pv >= 0) ? stype(line[pv].wc) : SU;
|
||||
if (tempShape == SR || tempShape == SD || tempShape == SC)
|
||||
to[i].wc = shape_form(line[i].wc, SHAPE_INITIAL);
|
||||
else
|
||||
to[i].wc = shape_form(line[i].wc, SHAPE_ISOLATED);
|
||||
}
|
||||
break;
|
||||
}
|
||||
default:
|
||||
/* SU, SL, SC and non-Arabic characters pass through unchanged */
|
||||
break;
|
||||
}
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* ═══════════════════════════════════════════════════════════════════════
|
||||
* Directional Status Stack
|
||||
* (replaces GCC nested functions — ESP32C3 has no executable stack)
|
||||
|
||||
+52
-5
@@ -5,8 +5,10 @@
|
||||
* minibidi.h — standalone header for ESP32C3 BiDi calculations
|
||||
*
|
||||
* Derived from [mintty](https://github.com/mintty/mintty/) (Thomas Wolff, MIT licence).
|
||||
* Stripped of: Arabic shaping, box-drawing mirror, terminal dependencies,
|
||||
* GCC nested functions, VLAs, and non-Hebrew/English Unicode data.
|
||||
* Includes: UAX#9 bidi (do_bidi) and Arabic contextual shaping (do_shape),
|
||||
* both ported from mintty src/minibidi.c (Ahmad Khalifa, Thomas Wolff).
|
||||
* Stripped of: box-drawing mirror, terminal dependencies, GCC nested
|
||||
* functions, VLAs, and Unicode data for scripts CrossPoint doesn't render.
|
||||
*/
|
||||
|
||||
#include <stdbool.h>
|
||||
@@ -34,13 +36,18 @@ typedef uint32_t ucschar; /* Unicode codepoint; BMP-only content fits uint16_t
|
||||
#define BIDI_MAX_LINE 128
|
||||
|
||||
/* ── bidi_char ───────────────────────────────────────────────────────── */
|
||||
/* origwc: the codepoint as it came from the epub text stream
|
||||
wc: working codepoint (may be replaced by mirrored form after do_bidi)
|
||||
index: original logical position, so the caller can reorder glyphs */
|
||||
/* origwc: the codepoint as it came from the epub text stream
|
||||
wc: working codepoint (may be replaced by mirrored form after
|
||||
do_bidi, or by an Arabic contextual form after do_shape)
|
||||
index: original logical position, so the caller can reorder glyphs
|
||||
joiners: ZWJ/ZWNJ context for Arabic shaping, mintty layout:
|
||||
low nibble = joiners that logically FOLLOW this character,
|
||||
high nibble = joiners that logically PRECEDE this character */
|
||||
typedef struct {
|
||||
ucschar origwc;
|
||||
ucschar wc;
|
||||
uint16_t index;
|
||||
uint8_t joiners;
|
||||
} bidi_char;
|
||||
|
||||
/* ── Bidi character classes (UAX #9) ────────────────────────────────── */
|
||||
@@ -71,6 +78,20 @@ enum {
|
||||
PDI, /* Pop Directional Isolate */
|
||||
};
|
||||
|
||||
/* ── Arabic joining formatter flags (bidi_char.joiners nibbles) ─────── */
|
||||
/* Values match mintty's minibidi.h: ZWNJ 0x01, ZWJ 0x02. do_shape()
|
||||
compares nibble values directly, so these must not be changed. */
|
||||
enum {
|
||||
ZWNJ = 0x01, /* U+200C ZERO WIDTH NON-JOINER */
|
||||
ZWJ = 0x02, /* U+200D ZERO WIDTH JOINER */
|
||||
};
|
||||
|
||||
/* Sentinel written by do_shape() over the Alef absorbed into a Lam-Alef
|
||||
ligature. Callers must filter it out when emitting shaped text.
|
||||
(Upstream mintty writes a space instead — a terminal must keep the cell;
|
||||
a proportional-text renderer must drop the character entirely.) */
|
||||
#define LIGATURE_PLACEHOLDER 0xFFFFu
|
||||
|
||||
/* ── Public API ──────────────────────────────────────────────────────── */
|
||||
|
||||
/*
|
||||
@@ -94,6 +115,32 @@ bool is_rtl_class(uchar bc);
|
||||
*/
|
||||
ucschar mirror(ucschar ch);
|
||||
|
||||
/*
|
||||
* do_shape(line, to, count)
|
||||
*
|
||||
* Applies Arabic contextual shaping (and Lam-Alef ligation) to
|
||||
* `line[0..count-1]`, writing the result to `to[0..count-1]`.
|
||||
*
|
||||
* MUST be called AFTER do_bidi(): the algorithm resolves joining from
|
||||
* VISUAL adjacency (line[i-1] is the visually-left neighbour, line[i+1]
|
||||
* the visually-right one), exactly like upstream mintty.
|
||||
*
|
||||
* line: visual-order input; the joiners field must be populated by the
|
||||
* caller (in logical order, before do_bidi) for ZWJ/ZWNJ support
|
||||
* to: output buffer, same size as line; non-Arabic entries are copied
|
||||
* through unchanged. An Alef absorbed by a Lam-Alef ligature is
|
||||
* replaced with LIGATURE_PLACEHOLDER — filter it on emission.
|
||||
* count: number of characters (≤ BIDI_MAX_LINE)
|
||||
*
|
||||
* Returns 1.
|
||||
*
|
||||
* Ported from mintty src/minibidi.c (Ahmad Khalifa, Thomas Wolff,
|
||||
* MIT licence), https://github.com/mintty/mintty — with CrossPoint
|
||||
* extensions for Perso-Arabic letters and in-stream diacritics, see
|
||||
* minibidi.c for details.
|
||||
*/
|
||||
int do_shape(bidi_char* line, bidi_char* to, int count);
|
||||
|
||||
/*
|
||||
* do_bidi(autodir, paragraphLevel, line, count)
|
||||
*
|
||||
|
||||
@@ -5,6 +5,17 @@
|
||||
|
||||
#include <cstring>
|
||||
|
||||
namespace {
|
||||
constexpr size_t ENTRY_STORAGE_CAPACITY = 64;
|
||||
constexpr size_t MAX_ENTRIES = ENTRY_STORAGE_CAPACITY - 2;
|
||||
constexpr size_t MAX_TITLE_CHARS = 160;
|
||||
constexpr size_t MAX_AUTHOR_CHARS = 120;
|
||||
constexpr size_t MAX_ID_CHARS = 128;
|
||||
constexpr size_t MAX_HREF_CHARS = 768;
|
||||
constexpr size_t MAX_SEARCH_TEMPLATE_CHARS = 768;
|
||||
constexpr size_t MAX_PAGE_URL_CHARS = 768;
|
||||
} // namespace
|
||||
|
||||
OpdsParser::OpdsParser() {
|
||||
parser = XML_ParserCreate(nullptr);
|
||||
if (!parser) {
|
||||
@@ -12,6 +23,7 @@ OpdsParser::OpdsParser() {
|
||||
LOG_DBG("OPDS", "Couldn't allocate memory for parser");
|
||||
return;
|
||||
}
|
||||
entries.reserve(ENTRY_STORAGE_CAPACITY);
|
||||
XML_SetUserData(parser, this);
|
||||
XML_SetElementHandler(parser, startElement, endElement);
|
||||
XML_SetCharacterDataHandler(parser, characterData);
|
||||
@@ -71,6 +83,8 @@ void OpdsParser::clear() {
|
||||
currentEntry = OpdsEntry{};
|
||||
currentText.clear();
|
||||
inEntry = inTitle = inAuthor = inAuthorName = inId = false;
|
||||
collectCurrentEntry = false;
|
||||
feedTruncated = false;
|
||||
}
|
||||
|
||||
std::vector<OpdsEntry> OpdsParser::getBooks() const {
|
||||
@@ -88,9 +102,33 @@ const char* OpdsParser::findAttribute(const XML_Char** atts, const char* name) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void OpdsParser::assignBounded(std::string& target, const char* value, const size_t maxLen) {
|
||||
if (!value) {
|
||||
target.clear();
|
||||
return;
|
||||
}
|
||||
target.assign(value, strnlen(value, maxLen));
|
||||
}
|
||||
|
||||
void OpdsParser::appendBounded(std::string& target, const char* value, const size_t len, const size_t maxLen) {
|
||||
if (target.size() >= maxLen) return;
|
||||
const size_t remaining = maxLen - target.size();
|
||||
target.append(value, len < remaining ? len : remaining);
|
||||
}
|
||||
|
||||
void XMLCALL OpdsParser::startElement(void* userData, const XML_Char* name, const XML_Char** atts) {
|
||||
auto* self = static_cast<OpdsParser*>(userData);
|
||||
|
||||
if (strcmp(name, "entry") == 0 || strstr(name, ":entry") != nullptr) {
|
||||
self->inEntry = true;
|
||||
self->collectCurrentEntry = self->entries.size() < MAX_ENTRIES;
|
||||
self->feedTruncated = self->feedTruncated || !self->collectCurrentEntry;
|
||||
self->currentEntry = OpdsEntry{};
|
||||
self->currentText.clear();
|
||||
self->inTitle = self->inAuthor = self->inAuthorName = self->inId = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (strcmp(name, "link") == 0 || strstr(name, ":link") != nullptr) {
|
||||
const char* href = findAttribute(atts, "href");
|
||||
if (href) {
|
||||
@@ -98,17 +136,16 @@ void XMLCALL OpdsParser::startElement(void* userData, const XML_Char* name, cons
|
||||
const char* type = findAttribute(atts, "type");
|
||||
|
||||
if (rel && strcmp(rel, "search") == 0) {
|
||||
std::string sHref(href);
|
||||
if (sHref.find("{searchTerms}") != std::string::npos) {
|
||||
self->searchTemplate = sHref;
|
||||
if (strstr(href, "{searchTerms}") != nullptr) {
|
||||
assignBounded(self->searchTemplate, href, MAX_SEARCH_TEMPLATE_CHARS);
|
||||
}
|
||||
} else if (rel && strcmp(rel, "next") == 0 && !self->inEntry) {
|
||||
self->nextPageUrl = href;
|
||||
assignBounded(self->nextPageUrl, href, MAX_PAGE_URL_CHARS);
|
||||
} else if (rel && strcmp(rel, "previous") == 0 && !self->inEntry) {
|
||||
self->prevPageUrl = href;
|
||||
assignBounded(self->prevPageUrl, href, MAX_PAGE_URL_CHARS);
|
||||
}
|
||||
|
||||
if (self->inEntry) {
|
||||
if (self->inEntry && self->collectCurrentEntry) {
|
||||
if (rel && type && strstr(rel, "opds-spec.org/acquisition") != nullptr &&
|
||||
strcmp(type, "application/epub+zip") == 0) {
|
||||
// Prefer plain EPUB links over derived formats when multiple
|
||||
@@ -119,25 +156,19 @@ void XMLCALL OpdsParser::startElement(void* userData, const XML_Char* name, cons
|
||||
self->currentEntry.href.find("/epub/") != std::string::npos);
|
||||
if (self->currentEntry.type != OpdsEntryType::BOOK || (isPlainEpub && !alreadyHasPlainEpub)) {
|
||||
self->currentEntry.type = OpdsEntryType::BOOK;
|
||||
self->currentEntry.href = href;
|
||||
assignBounded(self->currentEntry.href, href, MAX_HREF_CHARS);
|
||||
}
|
||||
} else if (type && strstr(type, "application/atom+xml") != nullptr) {
|
||||
if (self->currentEntry.type != OpdsEntryType::BOOK) {
|
||||
self->currentEntry.type = OpdsEntryType::NAVIGATION;
|
||||
self->currentEntry.href = href;
|
||||
assignBounded(self->currentEntry.href, href, MAX_HREF_CHARS);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (strcmp(name, "entry") == 0 || strstr(name, ":entry") != nullptr) {
|
||||
self->inEntry = true;
|
||||
self->currentEntry = OpdsEntry{};
|
||||
return;
|
||||
}
|
||||
|
||||
if (!self->inEntry) return;
|
||||
if (!self->inEntry || !self->collectCurrentEntry) return;
|
||||
|
||||
if (strcmp(name, "title") == 0 || strstr(name, ":title") != nullptr) {
|
||||
self->inTitle = true;
|
||||
@@ -157,10 +188,11 @@ void XMLCALL OpdsParser::endElement(void* userData, const XML_Char* name) {
|
||||
auto* self = static_cast<OpdsParser*>(userData);
|
||||
|
||||
if (strcmp(name, "entry") == 0 || strstr(name, ":entry") != nullptr) {
|
||||
if (!self->currentEntry.title.empty() && !self->currentEntry.href.empty()) {
|
||||
if (self->collectCurrentEntry && !self->currentEntry.title.empty() && !self->currentEntry.href.empty()) {
|
||||
self->entries.push_back(self->currentEntry);
|
||||
}
|
||||
self->inEntry = false;
|
||||
self->collectCurrentEntry = false;
|
||||
} else if (self->inEntry) {
|
||||
if (strcmp(name, "title") == 0 || strstr(name, ":title") != nullptr) {
|
||||
if (self->inTitle) self->currentEntry.title = self->currentText;
|
||||
@@ -179,7 +211,12 @@ void XMLCALL OpdsParser::endElement(void* userData, const XML_Char* name) {
|
||||
|
||||
void XMLCALL OpdsParser::characterData(void* userData, const XML_Char* s, const int len) {
|
||||
auto* self = static_cast<OpdsParser*>(userData);
|
||||
if (self->inTitle || self->inAuthorName || self->inId) {
|
||||
self->currentText.append(s, len);
|
||||
if (!self->collectCurrentEntry) return;
|
||||
if (self->inTitle) {
|
||||
appendBounded(self->currentText, s, len, MAX_TITLE_CHARS);
|
||||
} else if (self->inAuthorName) {
|
||||
appendBounded(self->currentText, s, len, MAX_AUTHOR_CHARS);
|
||||
} else if (self->inId) {
|
||||
appendBounded(self->currentText, s, len, MAX_ID_CHARS);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,6 +61,7 @@ class OpdsParser final : public Print {
|
||||
void flush() override;
|
||||
|
||||
bool error() const;
|
||||
bool truncated() const { return feedTruncated; }
|
||||
|
||||
operator bool() { return !error(); }
|
||||
|
||||
@@ -93,6 +94,8 @@ class OpdsParser final : public Print {
|
||||
std::string prevPageUrl;
|
||||
// Helper to find attribute value
|
||||
static const char* findAttribute(const XML_Char** atts, const char* name);
|
||||
static void assignBounded(std::string& target, const char* value, size_t maxLen);
|
||||
static void appendBounded(std::string& target, const char* value, size_t len, size_t maxLen);
|
||||
|
||||
XML_Parser parser = nullptr;
|
||||
std::vector<OpdsEntry> entries;
|
||||
@@ -105,6 +108,8 @@ class OpdsParser final : public Print {
|
||||
bool inAuthor = false;
|
||||
bool inAuthorName = false;
|
||||
bool inId = false;
|
||||
bool collectCurrentEntry = false;
|
||||
|
||||
bool errorOccured = false;
|
||||
bool feedTruncated = false;
|
||||
};
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
#include <HalDisplay.h>
|
||||
#include <HalStorage.h>
|
||||
#include <InflateReader.h>
|
||||
#include <InflateStream.h>
|
||||
#include <Logging.h>
|
||||
|
||||
#include <cstdio>
|
||||
@@ -174,9 +174,8 @@ void writeBmpHeader2bit(Print& bmpOut, const int width, const int height) {
|
||||
} // namespace
|
||||
|
||||
// Context for streaming PNG decompression
|
||||
// IMPORTANT: reader must be the first field - the uzlib callback casts uzlib_uncomp* to PngDecodeContext*
|
||||
struct PngDecodeContext {
|
||||
InflateReader reader; // Must be first — callback casts uzlib_uncomp* to PngDecodeContext*
|
||||
InflateStream reader;
|
||||
HalFile* file;
|
||||
|
||||
// PNG image properties
|
||||
@@ -195,7 +194,7 @@ struct PngDecodeContext {
|
||||
uint32_t chunkBytesRemaining; // bytes left in current IDAT chunk
|
||||
bool idatFinished; // no more IDAT chunks
|
||||
|
||||
// File read buffer for feeding uzlib
|
||||
// File read buffer for feeding the inflate stream
|
||||
uint8_t readBuf[2048];
|
||||
|
||||
// Palette for indexed color (type 3)
|
||||
@@ -229,21 +228,21 @@ static bool findNextIdatChunk(PngDecodeContext& ctx) {
|
||||
}
|
||||
}
|
||||
|
||||
// uzlib callback: reads the next batch of IDAT data from the file
|
||||
static int pngIdatReadCallback(uzlib_uncomp* uncomp) {
|
||||
auto* ctx = reinterpret_cast<PngDecodeContext*>(uncomp);
|
||||
// Fill callback: reads the next batch of IDAT data from the file
|
||||
static size_t pngIdatFillCallback(void* vctx, const uint8_t** data) {
|
||||
auto* ctx = static_cast<PngDecodeContext*>(vctx);
|
||||
|
||||
if (ctx->idatFinished) return -1;
|
||||
if (ctx->idatFinished) return 0;
|
||||
|
||||
// Skip 4-byte CRC and find next IDAT chunk when current chunk is exhausted
|
||||
while (ctx->chunkBytesRemaining == 0) {
|
||||
if (!ctx->file->seekCur(4)) { // skip 4-byte CRC of previous IDAT
|
||||
ctx->idatFinished = true;
|
||||
return -1;
|
||||
return 0;
|
||||
}
|
||||
if (!findNextIdatChunk(*ctx)) {
|
||||
ctx->idatFinished = true;
|
||||
return -1;
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -251,18 +250,15 @@ static int pngIdatReadCallback(uzlib_uncomp* uncomp) {
|
||||
size_t toRead = sizeof(ctx->readBuf);
|
||||
if (toRead > ctx->chunkBytesRemaining) toRead = ctx->chunkBytesRemaining;
|
||||
|
||||
int bytesRead = ctx->file->read(ctx->readBuf, toRead);
|
||||
const int bytesRead = ctx->file->read(ctx->readBuf, toRead);
|
||||
if (bytesRead <= 0) {
|
||||
ctx->idatFinished = true;
|
||||
return -1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
ctx->chunkBytesRemaining -= bytesRead;
|
||||
|
||||
// Give uzlib the buffer (skip first byte since we return it directly)
|
||||
uncomp->source = ctx->readBuf + 1;
|
||||
uncomp->source_limit = ctx->readBuf + bytesRead;
|
||||
return ctx->readBuf[0];
|
||||
*data = ctx->readBuf;
|
||||
return static_cast<size_t>(bytesRead);
|
||||
}
|
||||
|
||||
// Decode one scanline: decompress filter byte + raw bytes, then unfilter
|
||||
@@ -555,16 +551,16 @@ bool PngToBmpConverter::pngFileToBmpStreamInternal(HalFile& pngFile, Print& bmpO
|
||||
return false;
|
||||
}
|
||||
|
||||
// Initialize streaming decompressor with 32KB ring buffer for back-reference history
|
||||
// Initialize streaming decompressor with 32KB window for back-reference history
|
||||
if (!ctx.reader.init(true)) {
|
||||
LOG_ERR("PNG", "Failed to init inflate reader");
|
||||
LOG_ERR("PNG", "Failed to init inflate stream");
|
||||
free(ctx.currentRow);
|
||||
free(ctx.previousRow);
|
||||
return false;
|
||||
}
|
||||
ctx.reader.setReadCallback(pngIdatReadCallback);
|
||||
// PNG IDAT data is zlib-wrapped: consume the 2-byte zlib header (CMF + FLG)
|
||||
ctx.reader.skipZlibHeader();
|
||||
ctx.reader.setFill(pngIdatFillCallback, &ctx);
|
||||
// PNG IDAT data is zlib-wrapped (2-byte header + trailing adler32)
|
||||
ctx.reader.setZlibWrapped();
|
||||
|
||||
// Calculate output dimensions (same logic as JpegToBmpConverter)
|
||||
int outWidth = width;
|
||||
|
||||
@@ -0,0 +1,161 @@
|
||||
#pragma once
|
||||
#include <HalStorage.h>
|
||||
#include <Memory.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
|
||||
namespace serialization {
|
||||
|
||||
// Sequential buffered wrappers over HalFile.
|
||||
//
|
||||
// SdFat keeps ONE shared 512-byte sector cache per volume, so interleaving small
|
||||
// reads/writes across two or more files evicts and reloads that sector on nearly
|
||||
// every call -- each 4-byte pod becomes a full SD transaction (measured: 31s to
|
||||
// stream ~200KB through BookMetadataCache::buildBookBin on a 1,732-spine EPUB).
|
||||
// Batching into chunk-sized transfers keeps each file at sequential SD speed.
|
||||
//
|
||||
// Heap: one fixed buffer per wrapper, allocated once at construction and freed at
|
||||
// scope exit. If the allocation fails the wrapper degrades to unbuffered
|
||||
// passthrough -- correct, just slow -- so callers never need an OOM path.
|
||||
//
|
||||
// Constraint: the wrapper must be the file's ONLY accessor while alive (it tracks
|
||||
// the underlying position itself); mixing direct HalFile calls in desynchronizes it.
|
||||
|
||||
class BufferedFileWriter {
|
||||
public:
|
||||
BufferedFileWriter(HalFile& file, const size_t capacity)
|
||||
: file(file), buf(makeUniqueNoThrow<uint8_t[]>(capacity)), cap(buf ? capacity : 0), pos(file.position()) {}
|
||||
~BufferedFileWriter() { flush(); }
|
||||
BufferedFileWriter(const BufferedFileWriter&) = delete;
|
||||
BufferedFileWriter& operator=(const BufferedFileWriter&) = delete;
|
||||
|
||||
void write(const void* src, const size_t len) {
|
||||
pos += len;
|
||||
const auto* p = static_cast<const uint8_t*>(src);
|
||||
if (fill + len > cap) {
|
||||
flushBuffer();
|
||||
}
|
||||
if (len >= cap) { // also the cap == 0 passthrough
|
||||
okFlag &= file.write(p, len) == len;
|
||||
return;
|
||||
}
|
||||
// Typed local: cppcheck misreads unique_ptr<uint8_t[]>::get() arithmetic as void*.
|
||||
uint8_t* const data = buf.get();
|
||||
memcpy(data + fill, p, len);
|
||||
fill += len;
|
||||
}
|
||||
|
||||
// Logical write position (bytes written since the file was opened).
|
||||
size_t position() const { return pos; }
|
||||
|
||||
// Flush buffered bytes; returns false if any write so far has failed short.
|
||||
bool flush() {
|
||||
flushBuffer();
|
||||
return okFlag;
|
||||
}
|
||||
|
||||
private:
|
||||
void flushBuffer() {
|
||||
if (fill == 0) return;
|
||||
okFlag &= file.write(buf.get(), fill) == fill;
|
||||
fill = 0;
|
||||
}
|
||||
|
||||
HalFile& file;
|
||||
std::unique_ptr<uint8_t[]> buf;
|
||||
const size_t cap;
|
||||
size_t fill = 0;
|
||||
size_t pos;
|
||||
bool okFlag = true;
|
||||
};
|
||||
|
||||
class BufferedFileReader {
|
||||
public:
|
||||
BufferedFileReader(HalFile& file, const size_t capacity)
|
||||
: file(file), buf(makeUniqueNoThrow<uint8_t[]>(capacity)), cap(buf ? capacity : 0), bufStart(file.position()) {}
|
||||
BufferedFileReader(const BufferedFileReader&) = delete;
|
||||
BufferedFileReader& operator=(const BufferedFileReader&) = delete;
|
||||
|
||||
size_t read(void* dst, size_t len) {
|
||||
auto* p = static_cast<uint8_t*>(dst);
|
||||
if (cap == 0) { // passthrough
|
||||
const int n = file.read(p, len);
|
||||
const size_t got = n < 0 ? 0 : static_cast<size_t>(n);
|
||||
bufStart += got;
|
||||
return got;
|
||||
}
|
||||
size_t total = 0;
|
||||
while (len > 0) {
|
||||
if (off == fill) {
|
||||
bufStart += fill;
|
||||
off = 0;
|
||||
const int n = file.read(buf.get(), cap);
|
||||
fill = n < 0 ? 0 : static_cast<size_t>(n);
|
||||
if (fill == 0) break; // EOF or error
|
||||
}
|
||||
const size_t chunk = std::min(len, fill - off);
|
||||
// Typed local: cppcheck misreads unique_ptr<uint8_t[]>::get() arithmetic as void*.
|
||||
const uint8_t* const data = buf.get();
|
||||
memcpy(p, data + off, chunk);
|
||||
p += chunk;
|
||||
off += chunk;
|
||||
len -= chunk;
|
||||
total += chunk;
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
||||
// Logical read position.
|
||||
size_t position() const { return bufStart + off; }
|
||||
|
||||
bool seek(const size_t target) {
|
||||
// Within the buffered window: just move the cursor.
|
||||
if (cap != 0 && target >= bufStart && target < bufStart + fill) {
|
||||
off = target - bufStart;
|
||||
return true;
|
||||
}
|
||||
if (!file.seek(target)) return false;
|
||||
bufStart = target;
|
||||
fill = 0;
|
||||
off = 0;
|
||||
return true;
|
||||
}
|
||||
|
||||
private:
|
||||
HalFile& file;
|
||||
std::unique_ptr<uint8_t[]> buf;
|
||||
const size_t cap;
|
||||
size_t fill = 0;
|
||||
size_t off = 0;
|
||||
size_t bufStart;
|
||||
};
|
||||
|
||||
// serialization:: overloads mirroring the HalFile ones in Serialization.h.
|
||||
template <typename T>
|
||||
void writePod(BufferedFileWriter& out, const T& value) {
|
||||
out.write(&value, sizeof(T));
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
void readPod(BufferedFileReader& in, T& value) {
|
||||
in.read(&value, sizeof(T));
|
||||
}
|
||||
|
||||
inline void writeString(BufferedFileWriter& out, const std::string& s) {
|
||||
const uint32_t len = s.size();
|
||||
writePod(out, len);
|
||||
out.write(s.data(), len);
|
||||
}
|
||||
|
||||
inline void readString(BufferedFileReader& in, std::string& s) {
|
||||
uint32_t len;
|
||||
readPod(in, len);
|
||||
s.resize(len);
|
||||
if (len > 0) {
|
||||
in.read(&s[0], len);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace serialization
|
||||
+22
-22
@@ -1,13 +1,12 @@
|
||||
#include "ZipFile.h"
|
||||
|
||||
#include <HalStorage.h>
|
||||
#include <InflateReader.h>
|
||||
#include <InflateStream.h>
|
||||
#include <Logging.h>
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
struct ZipInflateCtx {
|
||||
InflateReader reader; // Must be first — callback casts uzlib_uncomp* to ZipInflateCtx*
|
||||
HalFile* file = nullptr;
|
||||
size_t fileRemaining = 0;
|
||||
uint8_t* readBuf = nullptr;
|
||||
@@ -40,19 +39,16 @@ class ScopedOpenClose final {
|
||||
bool ok = true; // true when zip was already open (no open() call needed)
|
||||
};
|
||||
|
||||
int zipReadCallback(uzlib_uncomp* uncomp) {
|
||||
auto* ctx = reinterpret_cast<ZipInflateCtx*>(uncomp);
|
||||
if (ctx->fileRemaining == 0) return -1;
|
||||
size_t zipFillCallback(void* vctx, const uint8_t** data) {
|
||||
auto* ctx = static_cast<ZipInflateCtx*>(vctx);
|
||||
if (ctx->fileRemaining == 0) return 0;
|
||||
|
||||
const size_t toRead = ctx->fileRemaining < ctx->readBufSize ? ctx->fileRemaining : ctx->readBufSize;
|
||||
const size_t bytesRead = ctx->file->read(ctx->readBuf, toRead);
|
||||
ctx->fileRemaining -= bytesRead;
|
||||
|
||||
if (bytesRead == 0) return -1;
|
||||
|
||||
uncomp->source = ctx->readBuf + 1;
|
||||
uncomp->source_limit = ctx->readBuf + bytesRead;
|
||||
return ctx->readBuf[0];
|
||||
*data = ctx->readBuf;
|
||||
return bytesRead;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
@@ -410,15 +406,18 @@ uint8_t* ZipFile::readFileToMemory(const char* filename, size_t* size, const boo
|
||||
ctx.readBuf = fileReadBuffer;
|
||||
ctx.readBufSize = 1024;
|
||||
|
||||
if (!ctx.reader.init(true)) {
|
||||
LOG_ERR("ZIP", "Failed to init inflate reader");
|
||||
// One-shot mode: `data` holds the entire output, so back-references
|
||||
// resolve inside it and no 32KB window is allocated.
|
||||
InflateStream inflate;
|
||||
if (!inflate.init(false)) {
|
||||
LOG_ERR("ZIP", "Failed to init inflate stream");
|
||||
free(fileReadBuffer);
|
||||
free(data);
|
||||
return nullptr;
|
||||
}
|
||||
ctx.reader.setReadCallback(zipReadCallback);
|
||||
inflate.setFill(zipFillCallback, &ctx);
|
||||
|
||||
if (!ctx.reader.read(data, inflatedDataSize)) {
|
||||
if (!inflate.read(data, inflatedDataSize)) {
|
||||
LOG_ERR("ZIP", "Failed to inflate file");
|
||||
free(fileReadBuffer);
|
||||
free(data);
|
||||
@@ -501,20 +500,21 @@ bool ZipFile::readFileToStream(const char* filename, Print& out, const size_t ch
|
||||
ctx.readBuf = fileReadBuffer;
|
||||
ctx.readBufSize = chunkSize;
|
||||
|
||||
if (!ctx.reader.init(true)) {
|
||||
LOG_ERR("ZIP", "Failed to init inflate reader");
|
||||
InflateStream inflate;
|
||||
if (!inflate.init(true)) {
|
||||
LOG_ERR("ZIP", "Failed to init inflate stream");
|
||||
free(outputBuffer);
|
||||
free(fileReadBuffer);
|
||||
return false;
|
||||
}
|
||||
ctx.reader.setReadCallback(zipReadCallback);
|
||||
inflate.setFill(zipFillCallback, &ctx);
|
||||
|
||||
bool success = false;
|
||||
size_t totalProduced = 0;
|
||||
|
||||
while (true) {
|
||||
size_t produced;
|
||||
const InflateStatus status = ctx.reader.readAtMost(outputBuffer, chunkSize, &produced);
|
||||
const InflateStream::Status status = inflate.readAtMost(outputBuffer, chunkSize, &produced);
|
||||
|
||||
totalProduced += produced;
|
||||
if (totalProduced > static_cast<size_t>(inflatedDataSize)) {
|
||||
@@ -530,7 +530,7 @@ bool ZipFile::readFileToStream(const char* filename, Print& out, const size_t ch
|
||||
}
|
||||
}
|
||||
|
||||
if (status == InflateStatus::Done) {
|
||||
if (status == InflateStream::Status::Done) {
|
||||
if (totalProduced != static_cast<size_t>(inflatedDataSize)) {
|
||||
LOG_ERR("ZIP", "Decompressed size mismatch (expected %zu, got %zu)", static_cast<size_t>(inflatedDataSize),
|
||||
totalProduced);
|
||||
@@ -541,16 +541,16 @@ bool ZipFile::readFileToStream(const char* filename, Print& out, const size_t ch
|
||||
break;
|
||||
}
|
||||
|
||||
if (status == InflateStatus::Error) {
|
||||
if (status == InflateStream::Status::Error) {
|
||||
LOG_ERR("ZIP", "Decompression failed");
|
||||
break;
|
||||
}
|
||||
// InflateStatus::Ok: output buffer full, continue
|
||||
// InflateStream::Status::Ok: output buffer full, continue
|
||||
}
|
||||
|
||||
free(outputBuffer);
|
||||
free(fileReadBuffer);
|
||||
return success; // ctx.reader destructor frees the ring buffer
|
||||
return success; // inflate destructor frees the decompressor state + window
|
||||
}
|
||||
|
||||
LOG_ERR("ZIP", "Unsupported compression method");
|
||||
|
||||
+14
-2
@@ -65,6 +65,18 @@ void HalDisplay::displayBuffer(HalDisplay::RefreshMode mode, bool turnOffScreen)
|
||||
einkDisplay.displayBuffer(convertRefreshMode(mode), turnOffScreen);
|
||||
}
|
||||
|
||||
void HalDisplay::displayBufferAsync(HalDisplay::RefreshMode mode) {
|
||||
if (gpio.deviceIsX3() && mode == RefreshMode::HALF_REFRESH) {
|
||||
einkDisplay.requestResync(1);
|
||||
}
|
||||
|
||||
einkDisplay.displayBufferAsyncNoShadow(convertRefreshMode(mode));
|
||||
}
|
||||
|
||||
void HalDisplay::waitRefreshComplete() { einkDisplay.waitRefreshComplete(); }
|
||||
|
||||
bool HalDisplay::supportsAsyncRefresh() const { return einkDisplay.supportsAsyncRefresh(); }
|
||||
|
||||
void HalDisplay::refreshDisplay(HalDisplay::RefreshMode mode, bool turnOffScreen) {
|
||||
if (gpio.deviceIsX3() && mode == RefreshMode::HALF_REFRESH) {
|
||||
einkDisplay.requestResync(1);
|
||||
@@ -77,9 +89,9 @@ void HalDisplay::deepSleep() { einkDisplay.deepSleep(); }
|
||||
|
||||
uint8_t* HalDisplay::getFrameBuffer() const { return einkDisplay.getFrameBuffer(); }
|
||||
|
||||
void HalDisplay::releaseFrameBuffers() { einkDisplay.releaseBuffers(); }
|
||||
uint8_t* HalDisplay::lendFrameBufferStorage(uint32_t* sizeOut) { return einkDisplay.lendBuildStorage(sizeOut); }
|
||||
|
||||
bool HalDisplay::reallocFrameBuffers() { return einkDisplay.reallocBuffers(); }
|
||||
void HalDisplay::returnFrameBufferStorage() { einkDisplay.returnBuildStorage(); }
|
||||
|
||||
void HalDisplay::copyGrayscaleBuffers(const uint8_t* lsbBuffer, const uint8_t* msbBuffer) {
|
||||
einkDisplay.copyGrayscaleBuffers(lsbBuffer, msbBuffer);
|
||||
|
||||
+18
-5
@@ -39,6 +39,17 @@ class HalDisplay {
|
||||
bool fromProgmem = false) const;
|
||||
|
||||
void displayBuffer(RefreshMode mode = RefreshMode::FAST_REFRESH, bool turnOffScreen = false);
|
||||
// Non-blocking refresh (shadow-free): starts the panel waveform and returns
|
||||
// while the panel refreshes on its own. The framebuffer must stay untouched
|
||||
// until waitRefreshComplete(), and the caller must rebuild the differential
|
||||
// baseline before the next differential update (the tiled grayscale cleanup
|
||||
// does). Panels without deferral fall back to a blocking refresh.
|
||||
void displayBufferAsync(RefreshMode mode = RefreshMode::FAST_REFRESH);
|
||||
// Block until a pending deferred refresh completes (no-op when none is).
|
||||
void waitRefreshComplete();
|
||||
// True when displayBufferAsync() genuinely overlaps (panel driver defers);
|
||||
// false where it falls back to a blocking refresh.
|
||||
bool supportsAsyncRefresh() const;
|
||||
void refreshDisplay(RefreshMode mode = RefreshMode::FAST_REFRESH, bool turnOffScreen = false);
|
||||
|
||||
// Power management
|
||||
@@ -47,11 +58,13 @@ class HalDisplay {
|
||||
// Access to frame buffer
|
||||
uint8_t* getFrameBuffer() const;
|
||||
|
||||
// Lend the framebuffer's RAM to a memory-hungry phase. No display calls may
|
||||
// run between release and a successful realloc; buffers come back white, so
|
||||
// callers must redraw the full screen.
|
||||
void releaseFrameBuffers();
|
||||
bool reallocFrameBuffers();
|
||||
// Lend the framebuffer's ~48 KB STORAGE to a memory-hungry phase (chapter
|
||||
// builds) without freeing it: the allocation never moves, so repeated loans
|
||||
// cannot fragment the heap (free+realloc measurably did). No display calls
|
||||
// between lend and return; the panel keeps its last refreshed image. The
|
||||
// buffer comes back white — redraw fully. Returns nullptr if already lent.
|
||||
uint8_t* lendFrameBufferStorage(uint32_t* sizeOut);
|
||||
void returnFrameBufferStorage();
|
||||
|
||||
// X3 grayscale preconditioning (OEM "AA-pre-BW(mid)" settle pass), windowed
|
||||
// to the gray region in physical panel coordinates (no-arg = full frame).
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"name": "miniz",
|
||||
"version": "11.3.2",
|
||||
"description": "Vendored miniz (tinfl inflate only) + InflateStream wrapper",
|
||||
"build": {
|
||||
"srcDir": "src",
|
||||
"includeDir": "src"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,165 @@
|
||||
#include "InflateStream.h"
|
||||
|
||||
#include <BuildScratch.h>
|
||||
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
|
||||
#include "MinizConfig.h"
|
||||
|
||||
namespace {
|
||||
// tinfl's window must be a power of two; TINFL_LZ_DICT_SIZE is 32768.
|
||||
constexpr size_t WINDOW_SIZE = TINFL_LZ_DICT_SIZE;
|
||||
// tinfl_decompressor holds mz_uint32 arrays; 8 keeps the window aligned too.
|
||||
constexpr size_t STATE_ALIGNED = (sizeof(tinfl_decompressor) + 7) & ~size_t{7};
|
||||
} // namespace
|
||||
|
||||
InflateStream::~InflateStream() { deinit(); }
|
||||
|
||||
bool InflateStream::init(const bool streaming) {
|
||||
// Every consumer constructs a fresh stream per operation, so acquire storage
|
||||
// from scratch each init (releasing any prior backing first).
|
||||
deinit();
|
||||
|
||||
// During a framebuffer loan the lent 48KB is up for grabs: state (~11KB) +
|
||||
// window (32KB) fit inside it, so a chapter-build inflate costs the heap
|
||||
// nothing. Absent (or already claimed): plain heap, freed in deinit().
|
||||
const size_t needed = STATE_ALIGNED + (streaming ? WINDOW_SIZE : 0);
|
||||
arenaBase = buildscratch::claim(needed);
|
||||
if (arenaBase) {
|
||||
state = reinterpret_cast<tinfl_decompressor*>(arenaBase);
|
||||
window = streaming ? arenaBase + STATE_ALIGNED : nullptr;
|
||||
} else {
|
||||
// Raw malloc (not makeUniqueNoThrow): the header keeps tinfl_decompressor
|
||||
// an incomplete type so consumers never include miniz; both blocks are
|
||||
// freed in deinit()/the destructor.
|
||||
state = static_cast<tinfl_decompressor*>(malloc(sizeof(tinfl_decompressor)));
|
||||
if (!state) return false;
|
||||
if (streaming) {
|
||||
window = static_cast<uint8_t*>(malloc(WINDOW_SIZE));
|
||||
if (!window) return false; // state kept; deinit()/next init reclaims it
|
||||
}
|
||||
}
|
||||
|
||||
tinfl_init(state);
|
||||
windowPos = 0;
|
||||
pendingStart = 0;
|
||||
pendingLen = 0;
|
||||
inPtr = nullptr;
|
||||
inAvail = 0;
|
||||
fill = nullptr;
|
||||
fillCtx = nullptr;
|
||||
inputExhausted = false;
|
||||
zlibWrapped = false;
|
||||
finished = false;
|
||||
oneShotStart = nullptr;
|
||||
return true;
|
||||
}
|
||||
|
||||
void InflateStream::deinit() {
|
||||
if (arenaBase) {
|
||||
buildscratch::release(arenaBase);
|
||||
arenaBase = nullptr;
|
||||
} else {
|
||||
free(state);
|
||||
free(window);
|
||||
}
|
||||
state = nullptr;
|
||||
window = nullptr;
|
||||
}
|
||||
|
||||
void InflateStream::setSource(const uint8_t* src, const size_t len) {
|
||||
inPtr = src;
|
||||
inAvail = len;
|
||||
inputExhausted = true; // the whole input is present; nothing more will come
|
||||
}
|
||||
|
||||
void InflateStream::setFill(const FillFn fn, void* ctx) {
|
||||
fill = fn;
|
||||
fillCtx = ctx;
|
||||
}
|
||||
|
||||
InflateStream::Status InflateStream::readAtMost(uint8_t* dest, const size_t maxLen, size_t* produced) {
|
||||
*produced = 0;
|
||||
if (!state) return Status::Error;
|
||||
|
||||
const bool streaming = window != nullptr;
|
||||
if (!streaming && !oneShotStart) oneShotStart = dest;
|
||||
|
||||
for (;;) {
|
||||
// Drain window bytes left over from a previous tinfl call. In ring mode
|
||||
// tinfl may produce more than the caller asked for in one shot -- the
|
||||
// overshoot stays pending in the window until a later readAtMost.
|
||||
if (pendingLen > 0) {
|
||||
size_t n = maxLen - *produced;
|
||||
if (n > pendingLen) n = pendingLen;
|
||||
memcpy(dest + *produced, window + pendingStart, n);
|
||||
pendingStart += n;
|
||||
pendingLen -= n;
|
||||
*produced += n;
|
||||
}
|
||||
if (*produced == maxLen) {
|
||||
return (finished && pendingLen == 0) ? Status::Done : Status::Ok;
|
||||
}
|
||||
if (finished) return Status::Done;
|
||||
|
||||
if (inAvail == 0 && !inputExhausted && fill) {
|
||||
inAvail = fill(fillCtx, &inPtr);
|
||||
if (inAvail == 0) inputExhausted = true;
|
||||
}
|
||||
|
||||
const mz_uint32 flags = (zlibWrapped ? TINFL_FLAG_PARSE_ZLIB_HEADER : 0) |
|
||||
(inputExhausted ? 0 : TINFL_FLAG_HAS_MORE_INPUT) |
|
||||
(streaming ? 0 : TINFL_FLAG_USING_NON_WRAPPING_OUTPUT_BUF);
|
||||
|
||||
size_t inBytes = inAvail;
|
||||
tinfl_status status;
|
||||
size_t outBytes;
|
||||
if (streaming) {
|
||||
// Ring mode invariant: tinfl derives its wrap mask from
|
||||
// (cursor offset + avail_out), so avail_out MUST always reach the end of
|
||||
// the 32KB window -- never cap it to the caller's remaining space.
|
||||
outBytes = WINDOW_SIZE - windowPos;
|
||||
status = tinfl_decompress(state, inPtr, &inBytes, window, window + windowPos, &outBytes, flags);
|
||||
pendingStart = windowPos;
|
||||
pendingLen = outBytes;
|
||||
windowPos += outBytes;
|
||||
if (windowPos == WINDOW_SIZE) windowPos = 0;
|
||||
} else {
|
||||
// One-shot: back-references resolve directly inside the destination buffer.
|
||||
outBytes = maxLen - *produced;
|
||||
status = tinfl_decompress(state, inPtr, &inBytes, oneShotStart, dest + *produced, &outBytes, flags);
|
||||
*produced += outBytes;
|
||||
}
|
||||
inPtr += inBytes;
|
||||
inAvail -= inBytes;
|
||||
|
||||
if (status == TINFL_STATUS_DONE) {
|
||||
finished = true; // drain any pending window bytes on the next pass
|
||||
continue;
|
||||
}
|
||||
if (status < TINFL_STATUS_DONE) return Status::Error; // corrupt stream / adler mismatch
|
||||
// TINFL_STATUS_NEEDS_MORE_INPUT loops back to the fill above; once the fill
|
||||
// runs dry the HAS_MORE_INPUT flag drops and tinfl either finishes or fails
|
||||
// (truncated stream) instead of spinning.
|
||||
if (status == TINFL_STATUS_NEEDS_MORE_INPUT && inputExhausted && inAvail == 0) {
|
||||
return Status::Error;
|
||||
}
|
||||
if (*produced == maxLen) {
|
||||
return (finished && pendingLen == 0) ? Status::Done : Status::Ok;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool InflateStream::read(uint8_t* dest, const size_t len) {
|
||||
size_t total = 0;
|
||||
while (total < len) {
|
||||
size_t produced = 0;
|
||||
const Status status = readAtMost(dest + total, len - total, &produced);
|
||||
total += produced;
|
||||
if (status == Status::Error) return false;
|
||||
if (status == Status::Done) return total == len;
|
||||
if (produced == 0) return false; // no progress safeguard
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
|
||||
// Forward declaration keeps miniz out of consumer translation units; the
|
||||
// decompressor state is heap-allocated in the .cpp where the type is complete.
|
||||
struct tinfl_decompressor_tag;
|
||||
|
||||
// Streaming deflate decompressor wrapping miniz's tinfl.
|
||||
//
|
||||
// Replaces the uzlib-backed InflateReader on the throughput paths (EPUB zip
|
||||
// entries, PNG IDAT). tinfl decodes via lookup tables where uzlib walks the
|
||||
// Huffman tree bit-by-bit -- several times faster on this CPU -- at the cost
|
||||
// of a larger decompressor state (~11KB, transient for the scope of the
|
||||
// stream; taken from the lent framebuffer bytes via buildscratch::claim()
|
||||
// when a FrameBufferLoan is active, heap otherwise). FontDecompressor
|
||||
// intentionally stays on InflateReader:
|
||||
// its one-shot flash-resident group decompressions are tiny, and the render
|
||||
// path should not carry the extra state allocation.
|
||||
//
|
||||
// Two modes:
|
||||
// init(false) -- one-shot: the destination buffer holds the ENTIRE output,
|
||||
// so back-references resolve inside it and no 32KB window is
|
||||
// allocated. read()/readAtMost() must be driven with
|
||||
// contiguous, forward-only slices of that one buffer
|
||||
// (a single read(dest, totalSize) is the common case).
|
||||
// init(true) -- streaming: allocates a 32KB window; output can go to any
|
||||
// buffer in any-sized chunks across calls.
|
||||
//
|
||||
// Input is either a single contiguous buffer (setSource) or pulled on demand
|
||||
// through a fill callback (setFill): return the number of bytes available and
|
||||
// point *data at them (valid until the next fill call); return 0 at end of
|
||||
// input. Call setZlibWrapped() before the first read when the stream has a
|
||||
// zlib header (e.g. PNG IDAT).
|
||||
class InflateStream {
|
||||
public:
|
||||
enum class Status {
|
||||
Ok, // Output buffer full; more decompressed data remains.
|
||||
Done, // Stream ended cleanly. produced may be < maxLen.
|
||||
Error, // Corrupt/truncated stream, or decompression failed.
|
||||
};
|
||||
|
||||
using FillFn = size_t (*)(void* ctx, const uint8_t** data);
|
||||
|
||||
InflateStream() = default;
|
||||
~InflateStream();
|
||||
InflateStream(const InflateStream&) = delete;
|
||||
InflateStream& operator=(const InflateStream&) = delete;
|
||||
|
||||
// Allocate decompressor state (and the 32KB window when streaming) and reset
|
||||
// stream state. Reuses existing allocations on repeated calls. Returns false
|
||||
// on OOM.
|
||||
bool init(bool streaming);
|
||||
|
||||
// Free the decompressor state and window.
|
||||
void deinit();
|
||||
|
||||
// Provide the entire compressed input as one contiguous buffer.
|
||||
void setSource(const uint8_t* src, size_t len);
|
||||
|
||||
// Provide compressed input on demand. ctx is passed back to fn verbatim.
|
||||
void setFill(FillFn fn, void* ctx);
|
||||
|
||||
// Declare the input zlib-wrapped (2-byte header + trailing adler32).
|
||||
void setZlibWrapped() { zlibWrapped = true; }
|
||||
|
||||
// Decompress exactly len bytes into dest. Returns false if the stream ends
|
||||
// or errors before producing len bytes.
|
||||
bool read(uint8_t* dest, size_t len);
|
||||
|
||||
// Decompress up to maxLen bytes into dest; *produced gets the byte count.
|
||||
Status readAtMost(uint8_t* dest, size_t maxLen, size_t* produced);
|
||||
|
||||
private:
|
||||
tinfl_decompressor_tag* state = nullptr; // ~11KB: heap, or inside the claimed build scratch
|
||||
uint8_t* window = nullptr; // 32KB ring, streaming mode only
|
||||
uint8_t* arenaBase = nullptr; // non-null when state/window live in lent framebuffer bytes
|
||||
size_t windowPos = 0; // ring write cursor
|
||||
// Decompressed-but-undelivered region of the window (tinfl can overshoot the
|
||||
// caller's requested length; the overshoot waits here for the next read).
|
||||
size_t pendingStart = 0;
|
||||
size_t pendingLen = 0;
|
||||
|
||||
const uint8_t* inPtr = nullptr;
|
||||
size_t inAvail = 0;
|
||||
FillFn fill = nullptr;
|
||||
void* fillCtx = nullptr;
|
||||
bool inputExhausted = false;
|
||||
bool zlibWrapped = false;
|
||||
bool finished = false;
|
||||
|
||||
// One-shot mode: tinfl needs the output buffer start for back-references.
|
||||
uint8_t* oneShotStart = nullptr;
|
||||
};
|
||||
@@ -0,0 +1,35 @@
|
||||
/* CrossPoint only needs miniz's low-level streaming inflate (tinfl). The
|
||||
* archive, deflate, stdio, and zlib-compatibility layers are compiled out so
|
||||
* the vendored library stays small and never touches the filesystem or clock.
|
||||
* Include this header instead of <miniz.h> so every translation unit sees the
|
||||
* same configuration. */
|
||||
#pragma once
|
||||
|
||||
#define MINIZ_NO_STDIO
|
||||
#define MINIZ_NO_TIME
|
||||
#define MINIZ_NO_ARCHIVE_APIS
|
||||
#define MINIZ_NO_ARCHIVE_WRITING_APIS
|
||||
#define MINIZ_NO_DEFLATE_APIS
|
||||
#define MINIZ_NO_ZLIB_COMPATIBLE_NAMES
|
||||
|
||||
// The ESP32 mask ROM exports tinfl_* at fixed addresses via DIRECT linker
|
||||
// script assignments (e.g. "tinfl_decompress = 0x...;" in the ROM .ld),
|
||||
// which override object-file definitions -- without these renames the
|
||||
// firmware silently binds to the ROM's 2021 build (TINFL_LESS_MEMORY, a
|
||||
// different tinfl_decompressor layout) and corrupts inflate state on real
|
||||
// data. Rename so the linker can never capture them. The prefix is
|
||||
// crosspoint_ (NOT freeink_) so a future branch that links FreeInkBook's
|
||||
// identically-renamed copy does not collide.
|
||||
#define tinfl_decompress crosspoint_tinfl_decompress
|
||||
#define tinfl_decompress_mem_to_heap crosspoint_tinfl_decompress_mem_to_heap
|
||||
#define tinfl_decompress_mem_to_mem crosspoint_tinfl_decompress_mem_to_mem
|
||||
#define tinfl_decompress_mem_to_callback crosspoint_tinfl_decompress_mem_to_callback
|
||||
#define mz_crc32 crosspoint_mz_crc32
|
||||
#define mz_adler32 crosspoint_mz_adler32
|
||||
#define mz_free crosspoint_mz_free
|
||||
|
||||
// Include the vendored miniz by relative path: ESP-IDF ships a ROM miniz.h
|
||||
// with the SAME include guard but a different (TINFL_LESS_MEMORY) struct
|
||||
// layout -- resolving <miniz.h> through the platform include path would
|
||||
// silently compile against the wrong structures.
|
||||
#include "../third_party/miniz.h"
|
||||
@@ -0,0 +1,7 @@
|
||||
/* Compiles the vendored miniz with CrossPoint's configuration. The include
|
||||
* order is load-bearing (the config defines/renames must be seen first). */
|
||||
// clang-format off
|
||||
#include "MinizConfig.h"
|
||||
|
||||
#include "../third_party/miniz.c"
|
||||
// clang-format on
|
||||
Vendored
+7922
File diff suppressed because it is too large
Load Diff
Vendored
+1510
File diff suppressed because it is too large
Load Diff
@@ -40,6 +40,26 @@ build_flags =
|
||||
-DPNG_MAX_BUFFERED_PIXELS=16416
|
||||
-DFREEINK_DEVICE_X4=1
|
||||
-DFREEINK_DEVICE_X3=1
|
||||
-DFREEINK_NET_WOLFSSL=1
|
||||
-DWOLFSSL_USER_SETTINGS
|
||||
-DWOLFSSL_OPTIONS_H
|
||||
-DWOLFSSL_CLIENT_EXAMPLE
|
||||
-DWOLFSSL_TLS13
|
||||
# MEMFIX-PORT: single-precision ECC (sp_c32.c). Without it every P-256 operation
|
||||
# (TLS 1.3 key_share keygen, ECDHE, ECDSA cert verify) runs on fast-math bignums
|
||||
# that WOLFSSL_SMALL_STACK heap-allocates at FP_MAX_BITS size -- tens of KB of
|
||||
# temporaries, which OOMs (MP_MEM) at the ~50KB free heap a reading session
|
||||
# leaves. SP uses fixed 256-bit arrays: a few KB, and several times faster.
|
||||
# SP_SMALL trades the large precomputed point tables for smaller flash.
|
||||
-DWOLFSSL_HAVE_SP_ECC
|
||||
-DWOLFSSL_SP_SMALL
|
||||
-DHAVE_TLS_EXTENSIONS
|
||||
-DHAVE_SUPPORTED_CURVES
|
||||
-DHAVE_HKDF
|
||||
-DHAVE_FFDHE_2048
|
||||
-DHAVE_CURVE25519
|
||||
-DWC_RSA_PSS
|
||||
-DHAVE_SNI
|
||||
-Wno-bidi-chars
|
||||
-Wl,--wrap=panic_print_backtrace,--wrap=panic_abort,--wrap=bootloader_common_check_efuse_blk_validity
|
||||
-fno-exceptions
|
||||
@@ -86,6 +106,21 @@ custom_sdkconfig =
|
||||
CONFIG_BT_CTRL_BLE_MAX_ACT=3 ; was 6; need conn + scan + initiate only
|
||||
CONFIG_BT_CTRL_BLE_ADV_REPORT_FLOW_CTRL_NUM=50 ; was 100; pairing-time scan only
|
||||
CONFIG_BT_CTRL_ADV_DUP_FILT_MAX=10 ; was 30
|
||||
; MEMFIX-PORT: task stack right-sizing (~7 KB); NOTE develop has no
|
||||
; custom_sdkconfig block — port via sdkconfig.defaults or equivalent.
|
||||
; Task stack right-sizing from measured high-water marks (heap block map +
|
||||
; per-task stack audit, July 2026): esp_timer used ~0.8 KB of 8 KB across
|
||||
; every capture incl. BLE sessions; the FreeRTOS timer service used ~0.5 KB
|
||||
; of 4 KB. Neither runs TLS or app code. ~7 KB back to the heap.
|
||||
CONFIG_ESP_TIMER_TASK_STACK_SIZE=4096
|
||||
CONFIG_FREERTOS_TIMER_TASK_STACK_DEPTH=2560
|
||||
; Move the WiFi stack's non-critical hot paths out of IRAM into flash.
|
||||
; On the C3, IRAM and DRAM share one SRAM pool, so the ~25-30 KB this
|
||||
; frees lands directly in the heap — paid for with lower WiFi throughput
|
||||
; during transfers (occasional sync/OTA use, not streaming: acceptable).
|
||||
; IRAM cost is static, so the heap gain applies even with WiFi off.
|
||||
CONFIG_ESP_WIFI_IRAM_OPT=n
|
||||
CONFIG_ESP_WIFI_RX_IRAM_OPT=n
|
||||
; Keep the Arduino wrappers for the removed cloud components (below) out of
|
||||
; the core source list; all other bundled libraries default to enabled.
|
||||
CONFIG_ARDUINO_SELECTIVE_COMPILATION=y
|
||||
@@ -106,6 +141,7 @@ custom_component_remove =
|
||||
espressif/cbor
|
||||
|
||||
extra_scripts =
|
||||
pre:scripts/patch_wolfssl.py
|
||||
pre:scripts/build_html.py
|
||||
pre:scripts/gen_i18n.py
|
||||
pre:scripts/git_branch.py
|
||||
@@ -123,6 +159,7 @@ lib_deps =
|
||||
PowerManager=symlink://freeink-sdk/libs/hardware/PowerManager
|
||||
Rtc=symlink://freeink-sdk/libs/hardware/Rtc
|
||||
Imu=symlink://freeink-sdk/libs/hardware/Imu
|
||||
SecureNet=symlink://freeink-sdk/libs/network/SecureNet
|
||||
FreeInkUI=symlink://freeink-sdk/libs/ui/FreeInkUI
|
||||
Icons=symlink://freeink-sdk/libs/assets/Icons
|
||||
BleKeyboardHost=symlink://freeink-sdk/libs/network/BleKeyboardHost
|
||||
@@ -132,6 +169,10 @@ lib_deps =
|
||||
bitbank2/PNGdec @ 1.1.6
|
||||
https://github.com/bitbank2/JPEGDEC.git#86282979224c8a32fd51e091ed5a35b0c699a52b
|
||||
links2004/WebSockets @ 2.7.3
|
||||
wolfssl/Arduino-wolfSSL @ 5.7.2
|
||||
|
||||
lib_ignore =
|
||||
BLE
|
||||
|
||||
[env:default]
|
||||
extends = base
|
||||
|
||||
@@ -6,6 +6,9 @@ Tests that a bare <br> element between paragraphs produces a visible blank-line
|
||||
gap (section separator), while a <br> inside a paragraph only produces a line
|
||||
break with no extra spacing.
|
||||
|
||||
Also tests that <li> containing <p> renders the bullet inline with the paragraph
|
||||
text, not on a separate line (GitHub issue #956).
|
||||
|
||||
Cases covered:
|
||||
1. Standalone <br> between paragraphs (section break — must show gap).
|
||||
2. <br class="..."> with a CSS class (calibre-style section break).
|
||||
@@ -13,6 +16,9 @@ Cases covered:
|
||||
4. Inline <br> inside a <p> (line break only — no extra gap).
|
||||
5. <br> at start of chapter (no gap before first paragraph).
|
||||
6. <br> following a heading.
|
||||
7. <li><p> with bold+italic text (bullet must be inline with text).
|
||||
8. <li><p> with nested <ul> (bullet inline, nested list indented).
|
||||
9. <li> with direct text (no <p> wrapper — baseline, already works).
|
||||
|
||||
Visual verification instructions are embedded as the first paragraph of each
|
||||
chapter so a human tester can confirm the expected result on device.
|
||||
@@ -36,6 +42,12 @@ p { margin-top: 1pt; margin-bottom: 0; text-indent: 1em; text-align: justify;
|
||||
h1 { text-align: center; margin-top: 0.5em; margin-bottom: 0.5em; }
|
||||
h2 { text-align: center; margin-top: 0.5em; margin-bottom: 0.5em; }
|
||||
.section-br { display: block; }
|
||||
.list-simple1 { margin-left: 1em; }
|
||||
.list-item1 { text-indent: 0; }
|
||||
.list-bullet { margin-left: 1em; }
|
||||
.list-bullet2 { margin-left: 2em; }
|
||||
.list { text-indent: 0; }
|
||||
.list-item { text-indent: 0; }
|
||||
"""
|
||||
|
||||
def xhtml(title, body):
|
||||
@@ -130,6 +142,55 @@ area above it despite the <br> being the very first element.</p>
|
||||
<p>{FILLER}</p>
|
||||
""")
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Chapter 7 — <li><p> with bold+italic (issue #956 example 1)
|
||||
# ---------------------------------------------------------------------------
|
||||
ch7 = xhtml("Ch7: li>p bold italic", """
|
||||
<h1>Ch 7: <li><p> with Bold+Italic</h1>
|
||||
<p>PASS: Each bullet below must be on the SAME line as its text, not on a
|
||||
separate line above it. The bullet and text are inline.</p>
|
||||
<ul class="list-simple1">
|
||||
<li><p class="list-item1"><b><i>Sketching User Experiences</i> by Bill Buxton</b>. This book examines the notion of sketching (Hint: prototypes are a kind of sketch) across a wide variety of disciplines, with eye-opening results.</p></li>
|
||||
<li><p class="list-item1"><b><i>Have Paper, Will Prototype</i> by Bill Lucas</b>. This lecture is a series of case studies about how to successfully create paper prototypes of computer interfaces.</p></li>
|
||||
<li><p class="list-item1"><b><i>The Kobold Guide to Board Game Design</i> by Mike Selinker</b>. The very best book there is on how to design great board games.</p></li>
|
||||
</ul>
|
||||
<p>PASS: All three bullets above should be inline with their respective text.</p>
|
||||
""")
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Chapter 8 — <li><p> with nested <ul> (issue #956 example 2)
|
||||
# ---------------------------------------------------------------------------
|
||||
ch8 = xhtml("Ch8: li>p nested ul", """
|
||||
<h1>Ch 8: <li><p> with Nested List</h1>
|
||||
<p>PASS: Each bullet must be inline with its text. The nested list should be
|
||||
indented further with its own bullets also inline.</p>
|
||||
<ul class="list-bullet">
|
||||
<li><p class="list">Problem statement: Design a flying dino game where dinosaurs race in and above the water.</p></li>
|
||||
<li><p class="list">Detailed problem statements</p>
|
||||
<ul class="list-bullet2">
|
||||
<li><p class="list-item">We need to figure out if we can schedule all the animation time needed for the dinosaurs.</p></li>
|
||||
<li><p class="list-item">We need to develop the right number of levels for this game.</p></li>
|
||||
<li><p class="list-item">We need to figure out all the power-ups that will go into this game.</p></li>
|
||||
</ul></li>
|
||||
</ul>
|
||||
<p>PASS: All bullets above should be inline with their text, including the nested ones.</p>
|
||||
""")
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Chapter 9 — <li> with direct text (no <p> wrapper, baseline)
|
||||
# ---------------------------------------------------------------------------
|
||||
ch9 = xhtml("Ch9: li direct text", """
|
||||
<h1>Ch 9: <li> Direct Text (Baseline)</h1>
|
||||
<p>PASS: Each bullet below must be inline with its text. This is the baseline
|
||||
case that already works — no <p> wrapper inside <li>.</p>
|
||||
<ul>
|
||||
<li>The fundamental difference between changes to the behavior of a system and changes to its structure.</li>
|
||||
<li>The enabling magic of alternating investment in structure and investment in behavior.</li>
|
||||
<li>The basics of the theory of how software design works and the forces that act on it.</li>
|
||||
</ul>
|
||||
<p>PASS: All three bullets above should be inline with their text.</p>
|
||||
""")
|
||||
|
||||
CHAPTERS = [
|
||||
("ch1", "chapter1.xhtml", "Chapter 1: Standalone br", ch1),
|
||||
("ch2", "chapter2.xhtml", "Chapter 2: Classed br", ch2),
|
||||
@@ -137,6 +198,9 @@ CHAPTERS = [
|
||||
("ch4", "chapter4.xhtml", "Chapter 4: Inline br", ch4),
|
||||
("ch5", "chapter5.xhtml", "Chapter 5: br after heading", ch5),
|
||||
("ch6", "chapter6.xhtml", "Chapter 6: br at start", ch6),
|
||||
("ch7", "chapter7.xhtml", "Chapter 7: li>p bold italic", ch7),
|
||||
("ch8", "chapter8.xhtml", "Chapter 8: li>p nested ul", ch8),
|
||||
("ch9", "chapter9.xhtml", "Chapter 9: li direct text", ch9),
|
||||
]
|
||||
|
||||
def build_epub(path):
|
||||
@@ -177,7 +241,7 @@ def build_epub(path):
|
||||
<package xmlns="http://www.idpf.org/2007/opf" version="3.0" unique-identifier="uid">
|
||||
<metadata xmlns:dc="http://purl.org/dc/elements/1.1/">
|
||||
<dc:identifier id="uid">test-epub-br-section-break</dc:identifier>
|
||||
<dc:title>Test: br Section Break</dc:title>
|
||||
<dc:title>Test: br Section Break & li/p</dc:title>
|
||||
<dc:language>en</dc:language>
|
||||
</metadata>
|
||||
<manifest>
|
||||
|
||||
@@ -0,0 +1,329 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Generate an EPUB from USER_GUIDE.md."""
|
||||
|
||||
import html as _html
|
||||
import io
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
import markdown
|
||||
from ebooklib import epub
|
||||
from PIL import Image, ImageDraw, ImageFont
|
||||
|
||||
ROOT = Path(__file__).parent.parent
|
||||
SOURCE_MD = ROOT / "USER_GUIDE.md"
|
||||
OUTPUT_EPUB = ROOT / "CrossPoint_User_Guide.epub"
|
||||
LOGO_PNG = ROOT / "src/images/Logo120.png"
|
||||
|
||||
# Portrait cover dimensions matching the X4 display (480×800)
|
||||
COVER_W, COVER_H = 480, 800
|
||||
|
||||
CSS = """
|
||||
body {
|
||||
font-family: Georgia, 'Times New Roman', serif;
|
||||
font-size: 1em;
|
||||
line-height: 1.6;
|
||||
margin: 1em 1.5em;
|
||||
color: #111;
|
||||
}
|
||||
|
||||
h1 { font-size: 1.8em; margin-top: 1.2em; margin-bottom: 0.4em; }
|
||||
h2 { font-size: 1.4em; margin-top: 1.5em; margin-bottom: 0.3em; border-bottom: 1px solid #ccc; padding-bottom: 0.2em; }
|
||||
h3 { font-size: 1.2em; margin-top: 1.2em; margin-bottom: 0.2em; }
|
||||
h4 { font-size: 1.05em; margin-top: 1em; margin-bottom: 0.2em; }
|
||||
h5 { font-size: 1em; margin-top: 0.8em; margin-bottom: 0.2em; }
|
||||
|
||||
p { margin: 0.6em 0; }
|
||||
|
||||
code {
|
||||
font-family: 'Courier New', Courier, monospace;
|
||||
font-size: 0.85em;
|
||||
background: #f4f4f4;
|
||||
padding: 0.1em 0.3em;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
pre {
|
||||
font-family: 'Courier New', Courier, monospace;
|
||||
font-size: 0.8em;
|
||||
background: #f4f4f4;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
padding: 0.8em 1em;
|
||||
overflow-x: auto;
|
||||
white-space: pre-wrap;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
pre code {
|
||||
background: none;
|
||||
padding: 0;
|
||||
border-radius: 0;
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
width: 100%;
|
||||
margin: 0.8em 0;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
th, td {
|
||||
border: 1px solid #ccc;
|
||||
padding: 0.4em 0.6em;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
th { background: #eee; font-weight: bold; }
|
||||
|
||||
ul, ol { margin: 0.5em 0; padding-left: 1.8em; }
|
||||
li { margin: 0.25em 0; }
|
||||
|
||||
a { color: #1a6699; text-decoration: none; }
|
||||
|
||||
hr { border: none; border-top: 1px solid #ccc; margin: 1.5em 0; }
|
||||
|
||||
.callout {
|
||||
border-left: 4px solid #888;
|
||||
background: #f8f8f8;
|
||||
padding: 0.5em 0.8em;
|
||||
margin: 0.8em 0;
|
||||
border-radius: 0 4px 4px 0;
|
||||
}
|
||||
|
||||
.callout-note { border-color: #2196F3; background: #e3f2fd; }
|
||||
.callout-tip { border-color: #4CAF50; background: #e8f5e9; }
|
||||
.callout-warning { border-color: #FF9800; background: #fff3e0; }
|
||||
|
||||
.callout-title {
|
||||
font-weight: bold;
|
||||
margin-bottom: 0.3em;
|
||||
}
|
||||
|
||||
/* Cover page */
|
||||
.cover-page {
|
||||
text-align: center;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.cover-page img {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
display: block;
|
||||
margin: 0;
|
||||
}
|
||||
"""
|
||||
|
||||
|
||||
def make_cover_png() -> bytes:
|
||||
"""Generate a full-size (480×800) cover image with logo, title, and subtitle."""
|
||||
cover = Image.new('RGB', (COVER_W, COVER_H), color=(255, 255, 255))
|
||||
draw = ImageDraw.Draw(cover)
|
||||
|
||||
# Paste logo centred at ~35% down the canvas
|
||||
with Image.open(LOGO_PNG) as logo_raw:
|
||||
if logo_raw.mode in ('RGBA', 'LA', 'P'):
|
||||
bg = Image.new('RGB', logo_raw.size, (255, 255, 255))
|
||||
bg.paste(logo_raw, mask=logo_raw.convert('RGBA').split()[3])
|
||||
logo = bg
|
||||
else:
|
||||
logo = logo_raw.convert('RGB')
|
||||
|
||||
# Scale logo to 240×240 (half the cover width)
|
||||
logo = logo.resize((240, 240), Image.LANCZOS)
|
||||
logo_x = (COVER_W - 240) // 2
|
||||
logo_y = int(COVER_H * 0.28)
|
||||
cover.paste(logo, (logo_x, logo_y))
|
||||
|
||||
# Try to use a system font; fall back to default if unavailable
|
||||
try:
|
||||
font_title = ImageFont.truetype('/usr/share/fonts/truetype/liberation/LiberationSerif-Bold.ttf', 52)
|
||||
font_sub = ImageFont.truetype('/usr/share/fonts/truetype/liberation/LiberationSerif-Regular.ttf', 32)
|
||||
except OSError:
|
||||
font_title = ImageFont.load_default()
|
||||
font_sub = font_title
|
||||
|
||||
title = 'CrossPoint'
|
||||
subtitle = 'User Guide'
|
||||
|
||||
# Draw title text
|
||||
bbox = draw.textbbox((0, 0), title, font=font_title)
|
||||
tw = bbox[2] - bbox[0]
|
||||
ty = logo_y + 240 + 48
|
||||
draw.text(((COVER_W - tw) // 2, ty), title, fill=(0, 0, 0), font=font_title)
|
||||
|
||||
# Draw subtitle text
|
||||
bbox2 = draw.textbbox((0, 0), subtitle, font=font_sub)
|
||||
sw = bbox2[2] - bbox2[0]
|
||||
sy = ty + (bbox[3] - bbox[1]) + 18
|
||||
draw.text(((COVER_W - sw) // 2, sy), subtitle, fill=(80, 80, 80), font=font_sub)
|
||||
|
||||
# Thin horizontal rules above and below the text block
|
||||
rule_y1 = ty - 24
|
||||
rule_y2 = sy + (bbox2[3] - bbox2[1]) + 24
|
||||
draw.line([(60, rule_y1), (COVER_W - 60, rule_y1)], fill=(180, 180, 180), width=1)
|
||||
draw.line([(60, rule_y2), (COVER_W - 60, rule_y2)], fill=(180, 180, 180), width=1)
|
||||
|
||||
buf = io.BytesIO()
|
||||
cover.save(buf, format='PNG')
|
||||
return buf.getvalue()
|
||||
|
||||
|
||||
def preprocess_callouts(text: str) -> str:
|
||||
"""Convert GitHub-style > [!TYPE] callouts to HTML divs."""
|
||||
lines = text.splitlines()
|
||||
out = []
|
||||
i = 0
|
||||
while i < len(lines):
|
||||
line = lines[i]
|
||||
m = re.match(r'^>\s*\[!(NOTE|TIP|WARNING)\]\s*$', line.strip())
|
||||
if m:
|
||||
kind = m.group(1).lower()
|
||||
title = kind.capitalize()
|
||||
body_lines = []
|
||||
i += 1
|
||||
while i < len(lines) and lines[i].startswith('>') \
|
||||
and not re.match(r'^>\s*\[!', lines[i]):
|
||||
body_lines.append(lines[i][1:].lstrip())
|
||||
i += 1
|
||||
body = _html.escape('\n'.join(body_lines).strip())
|
||||
out.append(
|
||||
f'\n<div class="callout callout-{kind}">'
|
||||
f'<div class="callout-title">{title}</div>'
|
||||
f'<p>{body}</p>'
|
||||
f'</div>\n'
|
||||
)
|
||||
else:
|
||||
out.append(line)
|
||||
i += 1
|
||||
return '\n'.join(out)
|
||||
|
||||
|
||||
def strip_toc(text: str) -> str:
|
||||
"""Remove the inline TOC list that follows the H1 heading."""
|
||||
return re.sub(
|
||||
r'(\n# CrossPoint User Guide\n)(.*?)(\n## 1\.)',
|
||||
lambda m: m.group(1) + m.group(3),
|
||||
text,
|
||||
flags=re.DOTALL,
|
||||
)
|
||||
|
||||
|
||||
def split_chapters(html: str):
|
||||
"""Split rendered HTML into (anchor_id, title, html_fragment) tuples by H2."""
|
||||
pattern = re.compile(r'(<h2[^>]*>)(.*?)(</h2>)', re.DOTALL)
|
||||
chapters = []
|
||||
positions = [(m.start(), m.group(0), m.group(2)) for m in pattern.finditer(html)]
|
||||
|
||||
if not positions:
|
||||
return [('intro', 'CrossPoint User Guide', html)]
|
||||
|
||||
intro_html = html[:positions[0][0]].strip()
|
||||
if intro_html:
|
||||
chapters.append(('intro', 'Introduction', intro_html))
|
||||
|
||||
seen_anchors = {c[0] for c in chapters} # seed with 'intro' if present
|
||||
for idx, (start, _tag, raw_title) in enumerate(positions):
|
||||
end = positions[idx + 1][0] if idx + 1 < len(positions) else len(html)
|
||||
fragment = html[start:end].strip()
|
||||
title = _html.unescape(re.sub(r'<[^>]+>', '', raw_title).strip())
|
||||
base = re.sub(r'[^a-z0-9]+', '-', title.lower()).strip('-') or 'section'
|
||||
anchor = base
|
||||
n = 1
|
||||
while anchor in seen_anchors:
|
||||
anchor = f'{base}-{n}'
|
||||
n += 1
|
||||
seen_anchors.add(anchor)
|
||||
chapters.append((anchor, title, fragment))
|
||||
|
||||
return chapters
|
||||
|
||||
|
||||
def make_xhtml(title: str, body: str) -> bytes:
|
||||
return (
|
||||
'<?xml version="1.0" encoding="utf-8"?>\n'
|
||||
'<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" '
|
||||
'"http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">\n'
|
||||
'<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en">\n'
|
||||
f'<head><title>{title}</title>\n'
|
||||
'<link rel="stylesheet" type="text/css" href="../style/main.css"/>\n'
|
||||
f'</head>\n<body>\n{body}\n</body>\n</html>'
|
||||
).encode('utf-8')
|
||||
|
||||
|
||||
def build_epub():
|
||||
source = SOURCE_MD.read_text(encoding='utf-8')
|
||||
source = strip_toc(source)
|
||||
source = preprocess_callouts(source)
|
||||
|
||||
md = markdown.Markdown(extensions=['tables', 'fenced_code', 'attr_list'])
|
||||
body_html = md.convert(source)
|
||||
|
||||
book = epub.EpubBook()
|
||||
book.set_identifier('crosspoint-user-guide-v1')
|
||||
book.set_title('CrossPoint User Guide')
|
||||
book.set_language('en')
|
||||
book.add_author('CrossPoint Reader Project')
|
||||
|
||||
style = epub.EpubItem(
|
||||
uid='style',
|
||||
file_name='style/main.css',
|
||||
media_type='text/css',
|
||||
content=CSS,
|
||||
)
|
||||
book.add_item(style)
|
||||
|
||||
# Full-size cover image (480×800) — used by CrossPoint for home-screen thumbnail.
|
||||
# uid='cover-image' matches the EPUB 2 <meta name="cover" content="cover-image"/> value.
|
||||
cover_png_bytes = make_cover_png()
|
||||
cover_img_item = epub.EpubItem(
|
||||
uid='cover-image',
|
||||
file_name='images/cover.png',
|
||||
media_type='image/png',
|
||||
content=cover_png_bytes,
|
||||
)
|
||||
cover_img_item.properties = ['cover-image'] # EPUB 3 manifest property
|
||||
book.add_item(cover_img_item)
|
||||
# EPUB 2 cover declaration — CrossPoint's Tier 1 OPF lookup
|
||||
book.add_metadata('OPF', 'meta', '', {'name': 'cover', 'content': 'cover-image'})
|
||||
|
||||
# Cover page XHTML — first readable page in the spine
|
||||
cover_page = epub.EpubHtml(title='CrossPoint User Guide', file_name='cover.xhtml', lang='en')
|
||||
cover_page.content = make_xhtml(
|
||||
'CrossPoint User Guide',
|
||||
'<div class="cover-page">'
|
||||
'<img src="images/cover.png" alt="CrossPoint User Guide cover"/>'
|
||||
'</div>',
|
||||
)
|
||||
cover_page.add_item(style)
|
||||
cover_page.add_item(cover_img_item)
|
||||
book.add_item(cover_page)
|
||||
|
||||
chapters_data = split_chapters(body_html)
|
||||
epub_chapters = []
|
||||
|
||||
for anchor, title, fragment in chapters_data:
|
||||
filename = f'chap_{anchor}.xhtml'
|
||||
chapter = epub.EpubHtml(title=title, file_name=filename, lang='en')
|
||||
chapter.content = make_xhtml(title, fragment)
|
||||
chapter.add_item(style)
|
||||
book.add_item(chapter)
|
||||
epub_chapters.append(chapter)
|
||||
|
||||
book.toc = (epub.Link('cover.xhtml', 'Cover', 'cover'),) + tuple(epub_chapters)
|
||||
book.add_item(epub.EpubNcx())
|
||||
book.add_item(epub.EpubNav())
|
||||
|
||||
# nav is excluded from the spine — CrossPoint locates it via properties="nav" in
|
||||
# the manifest and does not respect linear="no", so omitting it prevents it from
|
||||
# appearing as a readable page. Cover is the first spine item.
|
||||
book.spine = [cover_page] + epub_chapters
|
||||
|
||||
epub.write_epub(str(OUTPUT_EPUB), book)
|
||||
print(f'Generated: {OUTPUT_EPUB}')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
build_epub()
|
||||
@@ -0,0 +1,33 @@
|
||||
from pathlib import Path
|
||||
|
||||
Import("env")
|
||||
|
||||
|
||||
PROJECT_DIR = Path(env.subst("$PROJECT_DIR"))
|
||||
MARKER = "/* CrossPoint wolfSSL compatibility overrides */"
|
||||
OVERRIDES = f"""
|
||||
|
||||
{MARKER}
|
||||
#undef NO_DH
|
||||
#ifndef HAVE_FFDHE_2048
|
||||
#define HAVE_FFDHE_2048
|
||||
#endif
|
||||
/* MEMFIX-PORT: 8192 handles up to RSA-4096 keys (the public-CA maximum,
|
||||
ISRG Root X1 included) with half the per-bignum heap of 16384: with
|
||||
WOLFSSL_SMALL_STACK each fast-math temp is FP_MAX_BITS/8 * 2 bytes on the
|
||||
heap, and TLS cert verification allocates dozens at once. */
|
||||
#undef FP_MAX_BITS
|
||||
#define FP_MAX_BITS 8192
|
||||
"""
|
||||
|
||||
|
||||
def patch_user_settings(path: Path) -> None:
|
||||
text = path.read_text()
|
||||
if MARKER in text:
|
||||
text = text.split(MARKER, 1)[0].rstrip()
|
||||
path.write_text(text + OVERRIDES + "\n")
|
||||
print(f"Patched wolfSSL settings: {path.relative_to(PROJECT_DIR)}")
|
||||
|
||||
|
||||
for settings in PROJECT_DIR.glob(".pio/libdeps/*/Arduino-wolfSSL/src/user_settings.h"):
|
||||
patch_user_settings(settings)
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user