Merge remote-tracking branch 'origin/master' into feat-touch

This commit is contained in:
Justin Mitchell
2026-06-21 01:18:18 -04:00
76 changed files with 1677 additions and 381 deletions
+3
View File
@@ -101,6 +101,9 @@ static uint8_t lookupKernClass(const EpdKernClassEntry* entries, const uint16_t
}
int8_t EpdFont::getKerning(const uint32_t leftCp, const uint32_t rightCp) const {
if (utf8IsCjkBreakable(leftCp) || utf8IsCjkBreakable(rightCp)) {
return 0;
}
if (!data->kernMatrix) {
return 0;
}
+81 -41
View File
@@ -115,6 +115,9 @@ void SdCardFont::freeStyleAll(PerStyle& s) {
freeStyleMiniData(s);
delete[] s.fullIntervals;
s.fullIntervals = nullptr;
delete[] s.bmpIntervals;
s.bmpIntervals = nullptr;
s.intervalsAreBmp16 = false;
freeStyleKernLigatureData(s);
s.present = false;
}
@@ -516,59 +519,94 @@ bool SdCardFont::load(const char* path) {
styleCount_ = styleCount;
contentHash_ = hash;
// Load full intervals into RAM for each present style
// Load full intervals into RAM for each present style. BMP-only fonts with
// fewer than 65536 glyphs use a compact 6-byte interval table instead of the
// on-disk 12-byte table; large sparse CJK subsets otherwise keep tens of KB
// of always-resident heap just for lookup metadata.
for (uint8_t i = 0; i < MAX_STYLES; i++) {
auto& s = styles_[i];
if (!s.present) continue;
s.fullIntervals = new (std::nothrow) EpdUnicodeInterval[s.header.intervalCount];
if (!s.fullIntervals) {
LOG_ERR("SDCF", "Failed to allocate %u intervals for style %u", s.header.intervalCount, i);
freeAll();
return false;
}
if (!file.seekSet(s.intervalsFileOffset)) {
LOG_ERR("SDCF", "Failed to seek to intervals for style %u", i);
freeAll();
return false;
}
size_t intervalsBytes = s.header.intervalCount * sizeof(EpdUnicodeInterval);
if (file.read(reinterpret_cast<uint8_t*>(s.fullIntervals), intervalsBytes) != static_cast<int>(intervalsBytes)) {
LOG_ERR("SDCF", "Failed to read intervals for style %u", i);
freeAll();
return false;
}
// Validate interval contents before any later code (findGlobalGlyphIndex,
// glyph reads) trusts them. A malformed file could otherwise drive
// out-of-range glyph indices into bogus on-disk reads.
{
uint32_t expectedOffset = 0;
uint32_t prevLast = 0;
bool canUseBmp16 = s.header.glyphCount <= UINT16_MAX;
uint32_t expectedOffset = 0;
uint32_t prevLast = 0;
EpdUnicodeInterval iv{};
for (uint32_t j = 0; j < s.header.intervalCount; ++j) {
if (file.read(reinterpret_cast<uint8_t*>(&iv), sizeof(iv)) != sizeof(iv)) {
LOG_ERR("SDCF", "Failed to read interval %u for style %u", j, i);
freeAll();
return false;
}
if (iv.first > iv.last) {
LOG_ERR("SDCF", "Style %u: invalid interval %u (first 0x%lX > last 0x%lX)", i, j,
static_cast<unsigned long>(iv.first), static_cast<unsigned long>(iv.last));
file.close();
freeAll();
return false;
}
const uint32_t span = iv.last - iv.first + 1;
const bool overlapsPrev = (j > 0 && iv.first <= prevLast);
const bool spanTooBig = (span > s.header.glyphCount);
const bool offsetMismatch = (iv.offset != expectedOffset);
const bool offsetOverruns = (iv.offset > s.header.glyphCount - span);
if (overlapsPrev || spanTooBig || offsetMismatch || offsetOverruns) {
LOG_ERR("SDCF", "Style %u: invalid interval layout at %u (overlap=%d span=%u offMis=%d offOver=%d)", i, j,
overlapsPrev, span, offsetMismatch, offsetOverruns);
file.close();
freeAll();
return false;
}
if (iv.first > UINT16_MAX || iv.last > UINT16_MAX || iv.offset > UINT16_MAX) {
canUseBmp16 = false;
}
expectedOffset += span;
prevLast = iv.last;
}
if (!file.seekSet(s.intervalsFileOffset)) {
LOG_ERR("SDCF", "Failed to seek back to intervals for style %u", i);
freeAll();
return false;
}
if (canUseBmp16) {
s.bmpIntervals = new (std::nothrow) PerStyle::BmpInterval16[s.header.intervalCount];
if (!s.bmpIntervals) {
LOG_ERR("SDCF", "Failed to allocate compact intervals for style %u", i);
freeAll();
return false;
}
for (uint32_t j = 0; j < s.header.intervalCount; ++j) {
const auto& iv = s.fullIntervals[j];
if (iv.first > iv.last) {
LOG_ERR("SDCF", "Style %u: invalid interval %u (first 0x%lX > last 0x%lX)", i, j,
static_cast<unsigned long>(iv.first), static_cast<unsigned long>(iv.last));
file.close();
if (file.read(reinterpret_cast<uint8_t*>(&iv), sizeof(iv)) != sizeof(iv)) {
LOG_ERR("SDCF", "Failed to read compact interval %u for style %u", j, i);
freeAll();
return false;
}
const uint32_t span = iv.last - iv.first + 1;
const bool overlapsPrev = (j > 0 && iv.first <= prevLast);
const bool spanTooBig = (span > s.header.glyphCount);
const bool offsetMismatch = (iv.offset != expectedOffset);
const bool offsetOverruns = (iv.offset > s.header.glyphCount - span);
if (overlapsPrev || spanTooBig || offsetMismatch || offsetOverruns) {
LOG_ERR("SDCF", "Style %u: invalid interval layout at %u (overlap=%d span=%u offMis=%d offOver=%d)", i, j,
overlapsPrev, span, offsetMismatch, offsetOverruns);
file.close();
freeAll();
return false;
}
expectedOffset += span;
prevLast = iv.last;
s.bmpIntervals[j] = {static_cast<uint16_t>(iv.first), static_cast<uint16_t>(iv.last),
static_cast<uint16_t>(iv.offset)};
}
s.intervalsAreBmp16 = true;
} else {
s.fullIntervals = new (std::nothrow) EpdUnicodeInterval[s.header.intervalCount];
if (!s.fullIntervals) {
LOG_ERR("SDCF", "Failed to allocate %u intervals for style %u", s.header.intervalCount, i);
freeAll();
return false;
}
size_t intervalsBytes = s.header.intervalCount * sizeof(EpdUnicodeInterval);
if (file.read(reinterpret_cast<uint8_t*>(s.fullIntervals), intervalsBytes) != static_cast<int>(intervalsBytes)) {
LOG_ERR("SDCF", "Failed to read intervals for style %u", i);
freeAll();
return false;
}
}
@@ -603,13 +641,15 @@ int32_t SdCardFont::findGlobalGlyphIndex(const PerStyle& s, uint32_t codepoint)
int right = static_cast<int>(s.header.intervalCount) - 1;
while (left <= right) {
int mid = left + (right - left) / 2;
const auto& interval = s.fullIntervals[mid];
if (codepoint < interval.first) {
const uint32_t first = s.intervalsAreBmp16 ? s.bmpIntervals[mid].first : s.fullIntervals[mid].first;
const uint32_t last = s.intervalsAreBmp16 ? s.bmpIntervals[mid].last : s.fullIntervals[mid].last;
if (codepoint < first) {
right = mid - 1;
} else if (codepoint > interval.last) {
} else if (codepoint > last) {
left = mid + 1;
} else {
return static_cast<int32_t>(interval.offset + (codepoint - interval.first));
const uint32_t offset = s.intervalsAreBmp16 ? s.bmpIntervals[mid].offset : s.fullIntervals[mid].offset;
return static_cast<int32_t>(offset + (codepoint - first));
}
}
return -1;
@@ -1257,7 +1297,7 @@ const EpdGlyph* SdCardFont::onGlyphMiss(void* ctx, uint32_t codepoint) {
if (!self->loaded_ || styleIdx >= MAX_STYLES || !self->styles_[styleIdx].present) return nullptr;
const auto& s = self->styles_[styleIdx];
if (!s.fullIntervals) return nullptr;
if (!s.fullIntervals && !s.bmpIntervals) return nullptr;
// Check overflow cache first (matching both codepoint and style)
for (uint32_t i = 0; i < self->overflowCount_; i++) {
+11 -3
View File
@@ -58,9 +58,9 @@ class SdCardFont {
// Returns true if advance table is populated for at least one style.
bool hasAdvanceTable() const;
// Free mini data for all styles, restore stub EpdFontData.
// Also clears the temporary advance table (built per layout pass) but
// preserves the persistent advance cache (reused across passes).
// Free mini data for all styles and restore stub EpdFontData.
// Preserves the persistent advance cache so repeated layout passes can reuse
// previously fetched metrics.
void clearCache();
// Drop the persistent advance cache. Call when unloading the SD font or
@@ -140,6 +140,14 @@ class SdCardFont {
// Full intervals loaded from file (kept in RAM for codepoint lookup)
EpdUnicodeInterval* fullIntervals = nullptr;
struct BmpInterval16 {
uint16_t first;
uint16_t last;
uint16_t offset;
} __attribute__((packed));
static_assert(sizeof(BmpInterval16) == 6, "BmpInterval16 must remain compact");
BmpInterval16* bmpIntervals = nullptr;
bool intervalsAreBmp16 = false;
// Persistent kern-class + ligature tables (lazy-loaded on first prewarm).
// The full kern MATRIX is NOT resident — on Literata-class fonts a single
+5 -9
View File
@@ -34,19 +34,15 @@ bool SdCardFontManager::loadFamily(const SdCardFontFamilyInfo& family, GfxRender
unloadAll(renderer);
}
// Select by ordinal position: sort available sizes, then map the font size
// enum (SMALL=0 .. EXTRA_LARGE=3) to the corresponding slot. When the
// family has fewer sizes than 4, clamp to the last available size.
auto sizes = family.availableSizes();
if (sizes.empty()) {
// Select the physical point size closest to the built-in reader sizes. Some
// CJK font packs only ship larger sizes, so ordinal selection can make
// MEDIUM load 18pt+ and produce oversized pages on small devices.
const SdCardFontFileInfo* selected = family.findClosestReaderSize(fontSizeEnum);
if (!selected) {
LOG_ERR("SDMGR", "Family %s has no files to load", family.name.c_str());
return false;
}
uint8_t idx = fontSizeEnum;
if (idx >= sizes.size()) idx = sizes.size() - 1;
const SdCardFontFileInfo* selected = family.findFile(sizes[idx]);
auto* font = new (std::nothrow) SdCardFont();
if (!font) {
LOG_ERR("SDMGR", "Failed to allocate SdCardFont for %s", selected->path.c_str());
+5 -5
View File
@@ -15,10 +15,10 @@ class SdCardFontManager {
SdCardFontManager(const SdCardFontManager&) = delete;
SdCardFontManager& operator=(const SdCardFontManager&) = delete;
// Load the font file matching fontSizeEnum (SMALL=0 .. EXTRA_LARGE=3) by
// ordinal position in the family's sorted size list. Only one .cpfont file
// is loaded; other sizes remain on disk. This keeps resident interval +
// kern/ligature tables to one size's worth of memory.
// Load the font file whose physical point size is closest to the reader
// fontSizeEnum (SMALL=12, MEDIUM=14, LARGE=16, EXTRA_LARGE=18). Only one
// .cpfont file is loaded; other sizes remain on disk. This keeps resident
// interval + kern/ligature tables to one size's worth of memory.
// Returns true on success.
bool loadFamily(const SdCardFontFamilyInfo& family, GfxRenderer& renderer, uint8_t fontSizeEnum);
@@ -32,7 +32,7 @@ class SdCardFontManager {
// Get name of currently loaded family (empty if none).
const std::string& currentFamilyName() const { return loadedFamilyName_; };
// Point size that was actually loaded (closest match to targetPtSize).
// Point size that was actually loaded.
// 0 if nothing loaded.
uint8_t currentPointSize() const { return loadedPointSize_; };
+34
View File
@@ -15,6 +15,40 @@ const SdCardFontFileInfo* SdCardFontFamilyInfo::findFile(uint8_t size, uint8_t s
return nullptr;
}
const SdCardFontFileInfo* SdCardFontFamilyInfo::findClosestReaderSize(const uint8_t fontSizeEnum,
const uint8_t style) const {
if (files.empty()) return nullptr;
uint8_t target = 14;
switch (fontSizeEnum) {
case 0:
target = 12;
break;
case 2:
target = 16;
break;
case 3:
target = 18;
break;
case 1:
default:
target = 14;
break;
}
const SdCardFontFileInfo* best = nullptr;
uint8_t bestDelta = 255;
for (const auto& f : files) {
if (f.style != style) continue;
const uint8_t delta = f.pointSize > target ? f.pointSize - target : target - f.pointSize;
if (!best || delta < bestDelta || (delta == bestDelta && f.pointSize < best->pointSize)) {
best = &f;
bestDelta = delta;
}
}
return best;
}
bool SdCardFontFamilyInfo::hasSize(uint8_t size) const {
for (const auto& f : files) {
if (f.pointSize == size) return true;
+1
View File
@@ -18,6 +18,7 @@ struct SdCardFontFamilyInfo {
std::vector<SdCardFontFileInfo> files;
const SdCardFontFileInfo* findFile(uint8_t size, uint8_t style = 0) const;
const SdCardFontFileInfo* findClosestReaderSize(uint8_t fontSizeEnum, uint8_t style = 0) const;
bool hasSize(uint8_t size) const;
std::vector<uint8_t> availableSizes() const;
};
+1 -1
View File
@@ -248,7 +248,7 @@ unmerged_intervals = sorted(intervals + add_ints)
intervals = []
unvalidated_intervals = []
for i_start, i_end in unmerged_intervals:
if len(unvalidated_intervals) > 0 and i_start + 1 <= unvalidated_intervals[-1][1]:
if len(unvalidated_intervals) > 0 and i_start <= unvalidated_intervals[-1][1] + 1:
unvalidated_intervals[-1] = (unvalidated_intervals[-1][0], max(unvalidated_intervals[-1][1], i_end))
continue
unvalidated_intervals.append((i_start, i_end))
+4 -2
View File
@@ -5,6 +5,7 @@
#include <JpegToBmpConverter.h>
#include <Logging.h>
#include <PngToBmpConverter.h>
#include <Utf8.h>
#include <ZipFile.h>
#include "Epub/parsers/ContainerParser.h"
@@ -73,8 +74,9 @@ bool Epub::parseContentOpf(BookMetadataCache::BookMetadata& bookMetadata, const
return false;
}
// Grab data from opfParser into epub
bookMetadata.title = opfParser.title;
// Grab data from opfParser into epub. Normalize titles to NFC so NFD (combining
// mark) text renders correctly — the device fonts have no mark positioning.
bookMetadata.title = utf8ComposeNfc(opfParser.title);
bookMetadata.author = opfParser.author;
bookMetadata.language = opfParser.language;
bookMetadata.coverItemHref = opfParser.coverItemHref;
+5 -2
View File
@@ -2,6 +2,7 @@
#include <Logging.h>
#include <Serialization.h>
#include <Utf8.h>
#include <ZipFile.h>
#include <deque>
@@ -9,7 +10,7 @@
#include "FsHelpers.h"
namespace {
constexpr uint8_t BOOK_CACHE_VERSION = 7;
constexpr uint8_t BOOK_CACHE_VERSION = 8; // v8: TOC/book titles stored NFC-composed
constexpr char bookBinFile[] = "/book.bin";
constexpr char tmpSpineBinFile[] = "/spine.bin.tmp";
constexpr char tmpTocBinFile[] = "/toc.bin.tmp";
@@ -403,7 +404,9 @@ void BookMetadataCache::createTocEntry(const std::string& title, const std::stri
}
}
const TocEntry entry(title, href, anchor, level, spineIndex);
// 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);
tocCount++;
}
+249 -32
View File
@@ -24,6 +24,7 @@ constexpr size_t RTL_PARAGRAPH_PROBE_WORDS = 3;
// Per-word: scan enough chars to see through leading neutrals (quotes, numbers)
// before giving up. 64 is a hedge for pathological cases like long numeric tokens.
constexpr int RTL_PER_WORD_PROBE_DEPTH = 64;
constexpr size_t MIN_JUSTIFY_GAPS = 1;
// Byte-level pre-check: Hebrew UTF-8 lead bytes 0xD6-0xD7, Arabic/Syriac 0xD8-0xDB.
bool mayContainRtlBytes(const char* str) {
@@ -57,6 +58,134 @@ uint32_t lastCodepoint(const std::string& word) {
bool containsSoftHyphen(const std::string& word) { return word.find(SOFT_HYPHEN_UTF8) != std::string::npos; }
bool isNoBreakBeforeCjkPunctuation(const uint32_t cp) {
switch (cp) {
case '.':
case ',':
case ':':
case ';':
case '!':
case '?':
case ')':
case ']':
case '}':
case 0x00BB: // »
case 0x2019: //
case 0x201D: // ”
case 0x3001: // 、
case 0x3002: // 。
case 0x3009: // 〉
case 0x300B: // 》
case 0x300D: // 」
case 0x300F: // 』
case 0x3011: // 】
case 0x3015: //
case 0x3017: // 〗
case 0x3019: // 〙
case 0x301B: // 〛
case 0xFF01: //
case 0xFF09: //
case 0xFF0C: //
case 0xFF0E: //
case 0xFF1A: //
case 0xFF1B: //
case 0xFF1F: //
case 0xFF3D: //
case 0xFF5D: //
return true;
default:
return false;
}
}
bool isNoBreakAfterCjkPunctuation(const uint32_t cp) {
switch (cp) {
case '(':
case '[':
case '{':
case 0x00AB: // «
case 0x2018: //
case 0x201C: // “
case 0x3008: // 〈
case 0x300A: // 《
case 0x300C: // 「
case 0x300E: // 『
case 0x3010: // 【
case 0x3014: //
case 0x3016: // 〖
case 0x3018: // 〘
case 0x301A: // 〚
case 0xFF08: //
case 0xFF3B: //
case 0xFF5B: //
return true;
default:
return false;
}
}
bool containsCjkBreakableCodepoint(const std::string& text) {
const auto* ptr = reinterpret_cast<const unsigned char*>(text.c_str());
while (*ptr) {
const uint32_t cp = utf8NextCodepoint(&ptr);
if (utf8IsCjkBreakable(cp)) {
return true;
}
}
return false;
}
bool hasCjkBreakOpportunityBetween(const uint32_t leftCp, const uint32_t rightCp) {
if (!utf8IsCjkBreakable(leftCp) && !utf8IsCjkBreakable(rightCp)) return false;
if (isNoBreakAfterCjkPunctuation(leftCp) || isNoBreakBeforeCjkPunctuation(rightCp)) return false;
if (utf8IsCombiningMark(rightCp)) return false;
return true;
}
std::vector<size_t> cjkCharacterBreakByteOffsets(const std::string& text) {
struct CodepointBoundary {
uint32_t cp;
size_t endOffset;
};
std::vector<CodepointBoundary> codepoints;
codepoints.reserve(text.size());
bool hasCjkBreakable = false;
const auto* ptr = reinterpret_cast<const unsigned char*>(text.c_str());
const auto* const start = ptr;
while (*ptr) {
const uint32_t cp = utf8NextCodepoint(&ptr);
if (cp == 0) break;
if (utf8IsCjkBreakable(cp)) {
hasCjkBreakable = true;
}
codepoints.push_back({cp, static_cast<size_t>(ptr - start)});
}
if (!hasCjkBreakable || codepoints.size() < 2) return {};
std::vector<size_t> allowedOffsets;
allowedOffsets.reserve(codepoints.size() - 1);
for (size_t i = 0; i + 1 < codepoints.size(); ++i) {
const uint32_t current = codepoints[i].cp;
const uint32_t next = codepoints[i + 1].cp;
if (!hasCjkBreakOpportunityBetween(current, next)) continue;
allowedOffsets.push_back(codepoints[i].endOffset);
}
return allowedOffsets;
}
int computeJustifyExtra(const int spareSpace, const size_t gapCount) {
if (gapCount < MIN_JUSTIFY_GAPS || spareSpace <= 0) return 0;
// Distribute the spare space evenly across gaps. Do NOT bail out to 0 when the
// per-gap stretch is large: a sparse line (few words on a wide page) legitimately
// needs big gaps to reach the margin. Returning 0 there disables justification for
// that line, leaving it right-aligned (RTL) / left-aligned (LTR) — the mismatched
// alignment bug. Match the un-capped behavior of the old code.
return spareSpace / static_cast<int>(gapCount);
}
// Removes every soft hyphen in-place so rendered glyphs match measured widths.
void stripSoftHyphensInPlace(std::string& word) {
size_t pos = 0;
@@ -125,6 +254,14 @@ void ParsedText::addWord(std::string word, const EpdFontFamily::Style fontStyle,
const bool attachToPrevious) {
if (word.empty()) return;
// The device fonts carry no combining-mark positioning, so EPUB text stored in NFD
// (a base letter followed by separate combining accents -- common for Vietnamese,
// and used for many EPUB <h1> chapter headings) renders with the marks detached or
// misplaced. Compose to NFC here, the single funnel every word passes through, so a
// precomposed glyph is used instead. This runs once per word at layout time (the
// result is cached in the section file) and is a cheap no-op for mark-free text.
word = utf8ComposeNfc(word);
EpdFontFamily::Style baseStyle = fontStyle;
if (underline) {
baseStyle = static_cast<EpdFontFamily::Style>(baseStyle | EpdFontFamily::UNDERLINE);
@@ -132,12 +269,54 @@ void ParsedText::addWord(std::string word, const EpdFontFamily::Style fontStyle,
const bool wordStartsRtl = !hasRtlWord && mayContainRtlBytes(word.c_str()) &&
BidiUtils::startsWithRtl(word.c_str(), RTL_PER_WORD_PROBE_DEPTH);
const auto pushToken = [&](std::string token, const bool continues, const bool noSpaceBefore,
const bool isFocusSuffix) {
words.push_back(std::move(token));
wordStyles.push_back(baseStyle);
wordContinues.push_back(continues);
wordNoSpaceBefore.push_back(noSpaceBefore);
wordIsFocusSuffix.push_back(isFocusSuffix);
};
bool effectiveAttachToPrevious = attachToPrevious;
bool effectiveNoSpaceBefore = false;
if (attachToPrevious && !words.empty() &&
hasCjkBreakOpportunityBetween(lastCodepoint(words.back()), firstCodepoint(word))) {
effectiveAttachToPrevious = false;
effectiveNoSpaceBefore = true;
}
if (auto breakOffsets = cjkCharacterBreakByteOffsets(word); !breakOffsets.empty()) {
bool firstToken = true;
size_t tokenStart = 0;
for (const size_t breakOffset : breakOffsets) {
if (breakOffset <= tokenStart || breakOffset > word.size()) continue;
pushToken(word.substr(tokenStart, breakOffset - tokenStart), firstToken ? effectiveAttachToPrevious : false,
firstToken ? effectiveNoSpaceBefore : true, false);
firstToken = false;
tokenStart = breakOffset;
}
if (tokenStart < word.size()) {
pushToken(word.substr(tokenStart), firstToken ? effectiveAttachToPrevious : false,
firstToken ? effectiveNoSpaceBefore : true, false);
}
if (wordStartsRtl) {
hasRtlWord = true;
}
return;
}
if (containsCjkBreakableCodepoint(word)) {
pushToken(std::move(word), effectiveAttachToPrevious, effectiveNoSpaceBefore, false);
if (wordStartsRtl) {
hasRtlWord = true;
}
return;
}
// Already-bold text should stay fully bold; focus splitting would make its suffix regular later.
if (!this->focusReadingEnabled || (baseStyle & EpdFontFamily::BOLD) != 0) {
words.push_back(std::move(word));
wordStyles.push_back(baseStyle);
wordContinues.push_back(attachToPrevious);
wordIsFocusSuffix.push_back(false);
pushToken(std::move(word), effectiveAttachToPrevious, effectiveNoSpaceBefore, false);
if (wordStartsRtl) {
hasRtlWord = true;
}
@@ -166,17 +345,19 @@ void ParsedText::addWord(std::string word, const EpdFontFamily::Style fontStyle,
words.reserve(newCapacity);
wordStyles.reserve(newCapacity);
wordContinues.reserve(newCapacity);
wordNoSpaceBefore.reserve(newCapacity);
wordIsFocusSuffix.reserve(newCapacity);
}
// Lambda helper to process and push individual sub-segments of the string
// Use std::string_view to avoid heap allocations when slicing
auto processSegment = [&](std::string_view segment, bool isWord, bool attach) {
auto processSegment = [&](std::string_view segment, bool isWord, bool attach, bool noSpaceBefore) {
if (!isWord) {
// Punctuation and Numbers stay regular
words.emplace_back(segment);
wordStyles.push_back(baseStyle);
wordContinues.push_back(attach);
wordNoSpaceBefore.push_back(noSpaceBefore);
wordIsFocusSuffix.push_back(false);
} else {
size_t charCount = 0;
@@ -198,6 +379,7 @@ void ParsedText::addWord(std::string word, const EpdFontFamily::Style fontStyle,
words.emplace_back(segment);
wordStyles.push_back(static_cast<EpdFontFamily::Style>(baseStyle | EpdFontFamily::BOLD));
wordContinues.push_back(attach);
wordNoSpaceBefore.push_back(noSpaceBefore);
wordIsFocusSuffix.push_back(false);
} else {
countPtr = reinterpret_cast<const unsigned char*>(segment.data());
@@ -210,12 +392,14 @@ void ParsedText::addWord(std::string word, const EpdFontFamily::Style fontStyle,
words.emplace_back(segment.substr(0, splitByteOffset));
wordStyles.push_back(static_cast<EpdFontFamily::Style>(baseStyle | EpdFontFamily::BOLD));
wordContinues.push_back(attach);
wordNoSpaceBefore.push_back(noSpaceBefore);
wordIsFocusSuffix.push_back(false);
// Regular suffix - marked so extractLine can merge it back into single TextBlock entry
words.emplace_back(segment.substr(splitByteOffset));
wordStyles.push_back(baseStyle);
wordContinues.push_back(true);
wordNoSpaceBefore.push_back(false);
wordIsFocusSuffix.push_back(true);
}
}
@@ -243,7 +427,8 @@ void ParsedText::addWord(std::string word, const EpdFontFamily::Style fontStyle,
// Only the very first segment inherits the original attachToPrevious flag.
// Every subsequent segment MUST attach=true so it glues seamlessly to the prefix.
processSegment(segment, inWordSegment, isFirstSegment ? attachToPrevious : true);
processSegment(segment, inWordSegment, isFirstSegment ? effectiveAttachToPrevious : true,
isFirstSegment ? effectiveNoSpaceBefore : false);
// Setup for the next segment
segmentStart = currentCpStart;
@@ -255,7 +440,8 @@ void ParsedText::addWord(std::string word, const EpdFontFamily::Style fontStyle,
// Process the final remaining segment
size_t segmentLen = end - segmentStart;
std::string_view segment(reinterpret_cast<const char*>(segmentStart), segmentLen);
processSegment(segment, inWordSegment, isFirstSegment ? attachToPrevious : true);
processSegment(segment, inWordSegment, isFirstSegment ? effectiveAttachToPrevious : true,
isFirstSegment ? effectiveNoSpaceBefore : false);
if (wordStartsRtl) {
hasRtlWord = true;
}
@@ -324,14 +510,16 @@ void ParsedText::layoutAndExtractLines(const GfxRenderer& renderer, const int fo
std::vector<size_t> lineBreakIndices;
if (hyphenationEnabled) {
// Use greedy layout that can split words mid-loop when a hyphenated prefix fits.
lineBreakIndices = computeHyphenatedLineBreaks(renderer, fontId, pageWidth, wordWidths, wordContinues);
lineBreakIndices =
computeHyphenatedLineBreaks(renderer, fontId, pageWidth, wordWidths, wordContinues, wordNoSpaceBefore);
} else {
lineBreakIndices = computeLineBreaks(renderer, fontId, pageWidth, wordWidths, wordContinues);
lineBreakIndices = computeLineBreaks(renderer, fontId, pageWidth, wordWidths, wordContinues, wordNoSpaceBefore);
}
const size_t lineCount = includeLastLine ? lineBreakIndices.size() : lineBreakIndices.size() - 1;
for (size_t i = 0; i < lineCount; ++i) {
extractLine(i, pageWidth, wordWidths, wordContinues, lineBreakIndices, processLine, renderer, fontId);
extractLine(i, pageWidth, wordWidths, wordContinues, wordNoSpaceBefore, lineBreakIndices, processLine, renderer,
fontId);
}
// Remove consumed words so size() reflects only remaining words
@@ -340,6 +528,7 @@ void ParsedText::layoutAndExtractLines(const GfxRenderer& renderer, const int fo
words.erase(words.begin(), words.begin() + consumed);
wordStyles.erase(wordStyles.begin(), wordStyles.begin() + consumed);
wordContinues.erase(wordContinues.begin(), wordContinues.begin() + consumed);
wordNoSpaceBefore.erase(wordNoSpaceBefore.begin(), wordNoSpaceBefore.begin() + consumed);
wordIsFocusSuffix.erase(wordIsFocusSuffix.begin(), wordIsFocusSuffix.begin() + consumed);
}
}
@@ -356,7 +545,8 @@ std::vector<uint16_t> ParsedText::calculateWordWidths(const GfxRenderer& rendere
}
std::vector<size_t> ParsedText::computeLineBreaks(const GfxRenderer& renderer, const int fontId, const int pageWidth,
std::vector<uint16_t>& wordWidths, std::vector<bool>& continuesVec) {
std::vector<uint16_t>& wordWidths, std::vector<bool>& continuesVec,
std::vector<bool>& noSpaceBeforeVec) {
if (words.empty()) {
return {};
}
@@ -395,7 +585,9 @@ std::vector<size_t> ParsedText::computeLineBreaks(const GfxRenderer& renderer, c
for (size_t j = i; j < totalWordCount; ++j) {
// Add space before word j, unless it's the first word on the line or a continuation
int gap = 0;
if (j > static_cast<size_t>(i) && !continuesVec[j]) {
if (j > static_cast<size_t>(i) && noSpaceBeforeVec[j]) {
gap = 0;
} else if (j > static_cast<size_t>(i) && !continuesVec[j]) {
gap =
renderer.getSpaceAdvance(fontId, lastCodepoint(words[j - 1]), firstCodepoint(words[j]), wordStyles[j - 1]);
} else if (j > static_cast<size_t>(i) && continuesVec[j]) {
@@ -470,7 +662,8 @@ std::vector<size_t> ParsedText::computeLineBreaks(const GfxRenderer& renderer, c
// Builds break indices while opportunistically splitting the word that would overflow the current line.
std::vector<size_t> ParsedText::computeHyphenatedLineBreaks(const GfxRenderer& renderer, const int fontId,
const int pageWidth, std::vector<uint16_t>& wordWidths,
std::vector<bool>& continuesVec) {
std::vector<bool>& continuesVec,
std::vector<bool>& noSpaceBeforeVec) {
const int firstLineIndent = resolveFirstLineIndent(true, renderer, fontId);
std::vector<size_t> lineBreakIndices;
@@ -488,7 +681,9 @@ std::vector<size_t> ParsedText::computeHyphenatedLineBreaks(const GfxRenderer& r
while (currentIndex < wordWidths.size()) {
const bool isFirstWord = currentIndex == lineStart;
int spacing = 0;
if (!isFirstWord && !continuesVec[currentIndex]) {
if (!isFirstWord && noSpaceBeforeVec[currentIndex]) {
spacing = 0;
} else if (!isFirstWord && !continuesVec[currentIndex]) {
spacing = renderer.getSpaceAdvance(fontId, lastCodepoint(words[currentIndex - 1]),
firstCodepoint(words[currentIndex]), wordStyles[currentIndex - 1]);
} else if (!isFirstWord && continuesVec[currentIndex]) {
@@ -618,6 +813,7 @@ bool ParsedText::hyphenateWordAtIndex(const size_t wordIndex, const int availabl
// line, while "kilometer" moves to the next line.
// wordContinues[wordIndex] is intentionally left unchanged — the prefix keeps its original attachment.
wordContinues.insert(wordContinues.begin() + wordIndex + 1, false);
wordNoSpaceBefore.insert(wordNoSpaceBefore.begin() + wordIndex + 1, false);
// Update cached widths to reflect the new prefix/remainder pairing.
wordWidths[wordIndex] = static_cast<uint16_t>(chosenWidth);
@@ -627,7 +823,8 @@ bool ParsedText::hyphenateWordAtIndex(const size_t wordIndex, const int availabl
}
void ParsedText::extractLine(const size_t breakIndex, const int pageWidth, const std::vector<uint16_t>& wordWidths,
const std::vector<bool>& continuesVec, const std::vector<size_t>& lineBreakIndices,
const std::vector<bool>& continuesVec, const std::vector<bool>& noSpaceBeforeVec,
const std::vector<size_t>& lineBreakIndices,
const std::function<void(std::shared_ptr<TextBlock>)>& processLine,
const GfxRenderer& renderer, const int fontId) {
const size_t lineBreak = lineBreakIndices[breakIndex];
@@ -660,7 +857,11 @@ void ParsedText::extractLine(const size_t breakIndex, const int pageWidth, const
for (size_t wordIdx = 0; wordIdx < lineWordCount; wordIdx++) {
lineWordWidthSum += wordWidths[lastBreakAt + wordIdx];
// Count gaps: each word after the first creates a gap, unless it's a continuation
if (wordIdx > 0 && !continuesVec[lastBreakAt + wordIdx]) {
if (wordIdx > 0 && noSpaceBeforeVec[lastBreakAt + wordIdx]) {
// Unicode break opportunity with no inserted Latin-style space. It is still
// a stretchable gap for justified CJK/Korean text.
actualGapCount++;
} else if (wordIdx > 0 && !continuesVec[lastBreakAt + wordIdx]) {
actualGapCount++;
totalNaturalGaps += renderer.getSpaceAdvance(fontId, lastCodepoint(lineWords[wordIdx - 1]),
firstCodepoint(lineWords[wordIdx]), lineWordStyles[wordIdx - 1]);
@@ -689,8 +890,8 @@ void ParsedText::extractLine(const size_t breakIndex, const int pageWidth, const
// For justified text, compute per-gap extra to distribute remaining space evenly
const int spareSpace = effectivePageWidth - lineWordWidthSum - totalNaturalGaps;
const int justifyExtra = (effectiveAlignment == CssTextAlign::Justify && !isLastLine && actualGapCount >= 1)
? spareSpace / static_cast<int>(actualGapCount)
const int justifyExtra = (effectiveAlignment == CssTextAlign::Justify && !isLastLine)
? computeJustifyExtra(spareSpace, actualGapCount)
: 0;
// BiDi processing: reorder words with UAX#9 in full-line context.
@@ -709,11 +910,13 @@ void ParsedText::extractLine(const size_t breakIndex, const int pageWidth, const
reorderedStylesScratch.clear();
reorderedWidthsScratch.clear();
reorderedContinuesScratch.clear();
reorderedNoSpaceBeforeScratch.clear();
reorderedFocusSuffixScratch.clear();
reorderedWordsScratch.reserve(visualOrderScratch.size());
reorderedStylesScratch.reserve(visualOrderScratch.size());
reorderedWidthsScratch.reserve(visualOrderScratch.size());
reorderedContinuesScratch.reserve(visualOrderScratch.size());
reorderedNoSpaceBeforeScratch.reserve(visualOrderScratch.size());
reorderedFocusSuffixScratch.reserve(visualOrderScratch.size());
for (size_t i = 0; i < visualOrderScratch.size(); ++i) {
@@ -740,6 +943,7 @@ void ParsedText::extractLine(const size_t breakIndex, const int pageWidth, const
}
}
reorderedContinuesScratch.push_back(continues);
reorderedNoSpaceBeforeScratch.push_back(!continues && noSpaceBeforeVec[lastBreakAt + src]);
}
int reorderedWordWidthSum = 0;
@@ -747,7 +951,11 @@ void ParsedText::extractLine(const size_t breakIndex, const int pageWidth, const
int reorderedNaturalGaps = 0;
for (size_t wordIdx = 0; wordIdx < reorderedWidthsScratch.size(); wordIdx++) {
reorderedWordWidthSum += reorderedWidthsScratch[wordIdx];
if (wordIdx > 0 && !reorderedContinuesScratch[wordIdx]) {
if (wordIdx > 0 && reorderedNoSpaceBeforeScratch[wordIdx]) {
// Unicode break opportunity with no inserted Latin-style space. It is still
// a stretchable gap for justified CJK/Korean text.
reorderedGapCount++;
} else if (wordIdx > 0 && !reorderedContinuesScratch[wordIdx]) {
reorderedGapCount++;
reorderedNaturalGaps += renderer.getSpaceAdvance(fontId, lastCodepoint(reorderedWordsScratch[wordIdx - 1]),
firstCodepoint(reorderedWordsScratch[wordIdx]),
@@ -763,10 +971,9 @@ void ParsedText::extractLine(const size_t breakIndex, const int pageWidth, const
}
const int reorderedSpare = effectivePageWidth - reorderedWordWidthSum - reorderedNaturalGaps;
const int reorderedJustifyExtra =
(effectiveAlignment == CssTextAlign::Justify && !isLastLine && reorderedGapCount >= 1)
? reorderedSpare / static_cast<int>(reorderedGapCount)
: 0;
const int reorderedJustifyExtra = (effectiveAlignment == CssTextAlign::Justify && !isLastLine)
? computeJustifyExtra(reorderedSpare, reorderedGapCount)
: 0;
const int justifyContribution = (effectiveAlignment == CssTextAlign::Justify && !isLastLine)
? reorderedJustifyExtra * static_cast<int>(reorderedGapCount)
@@ -805,9 +1012,11 @@ void ParsedText::extractLine(const size_t breakIndex, const int pageWidth, const
}
xpos += advance;
} else if (wordIdx + 1 < reorderedWidthsScratch.size()) {
int gap = renderer.getSpaceAdvance(fontId, lastCodepoint(reorderedWordsScratch[wordIdx]),
firstCodepoint(reorderedWordsScratch[wordIdx + 1]),
reorderedStylesScratch[wordIdx]);
const bool nextNoSpace = reorderedNoSpaceBeforeScratch[wordIdx + 1];
int gap = nextNoSpace ? 0
: renderer.getSpaceAdvance(fontId, lastCodepoint(reorderedWordsScratch[wordIdx]),
firstCodepoint(reorderedWordsScratch[wordIdx + 1]),
reorderedStylesScratch[wordIdx]);
if (effectiveAlignment == CssTextAlign::Justify && !isLastLine) {
gap += reorderedJustifyExtra;
}
@@ -846,11 +1055,15 @@ void ParsedText::extractLine(const size_t breakIndex, const int pageWidth, const
xpos -= advance;
} else {
int gap = 0;
bool nextNoSpace = false;
if (wordIdx + 1 < lineWordCount) {
gap = renderer.getSpaceAdvance(fontId, lastCodepoint(lineWords[wordIdx]),
firstCodepoint(lineWords[wordIdx + 1]), lineWordStyles[wordIdx]);
nextNoSpace = noSpaceBeforeVec[lastBreakAt + wordIdx + 1];
gap = nextNoSpace
? 0
: renderer.getSpaceAdvance(fontId, lastCodepoint(lineWords[wordIdx]),
firstCodepoint(lineWords[wordIdx + 1]), lineWordStyles[wordIdx]);
}
if (effectiveAlignment == CssTextAlign::Justify && !isLastLine) {
if (wordIdx + 1 < lineWordCount && effectiveAlignment == CssTextAlign::Justify && !isLastLine) {
gap += justifyExtra;
}
xpos -= gap;
@@ -880,11 +1093,15 @@ void ParsedText::extractLine(const size_t breakIndex, const int pageWidth, const
xpos += advance;
} else {
int gap = 0;
bool nextNoSpace = false;
if (wordIdx + 1 < lineWordCount) {
gap = renderer.getSpaceAdvance(fontId, lastCodepoint(lineWords[wordIdx]),
firstCodepoint(lineWords[wordIdx + 1]), lineWordStyles[wordIdx]);
nextNoSpace = noSpaceBeforeVec[lastBreakAt + wordIdx + 1];
gap = nextNoSpace
? 0
: renderer.getSpaceAdvance(fontId, lastCodepoint(lineWords[wordIdx]),
firstCodepoint(lineWords[wordIdx + 1]), lineWordStyles[wordIdx]);
}
if (effectiveAlignment == CssTextAlign::Justify && !isLastLine) {
if (wordIdx + 1 < lineWordCount && effectiveAlignment == CssTextAlign::Justify && !isLastLine) {
gap += justifyExtra;
}
xpos += wordWidths[lastBreakAt + wordIdx] + gap;
+9 -4
View File
@@ -15,7 +15,8 @@ class GfxRenderer;
class ParsedText {
std::vector<std::string> words;
std::vector<EpdFontFamily::Style> wordStyles;
std::vector<bool> wordContinues; // true = word attaches to previous (no space before it)
std::vector<bool> wordContinues; // true = word attaches to previous with no break
std::vector<bool> wordNoSpaceBefore; // true = may break before token, but no synthetic space when joined
std::vector<bool> wordIsFocusSuffix; // true = token is the regular tail of a focus bold-prefix split
BlockStyle blockStyle;
bool extraParagraphSpacing;
@@ -27,18 +28,22 @@ class ParsedText {
std::vector<EpdFontFamily::Style> reorderedStylesScratch;
std::vector<uint16_t> reorderedWidthsScratch;
std::vector<bool> reorderedContinuesScratch;
std::vector<bool> reorderedNoSpaceBeforeScratch;
std::vector<bool> reorderedFocusSuffixScratch;
std::vector<uint16_t> visualOrderScratch;
int resolveFirstLineIndent(bool isFirstLine, const GfxRenderer& renderer, int fontId) const;
std::vector<size_t> computeLineBreaks(const GfxRenderer& renderer, int fontId, int pageWidth,
std::vector<uint16_t>& wordWidths, std::vector<bool>& continuesVec);
std::vector<uint16_t>& wordWidths, std::vector<bool>& continuesVec,
std::vector<bool>& noSpaceBeforeVec);
std::vector<size_t> computeHyphenatedLineBreaks(const GfxRenderer& renderer, int fontId, int pageWidth,
std::vector<uint16_t>& wordWidths, std::vector<bool>& continuesVec);
std::vector<uint16_t>& wordWidths, std::vector<bool>& continuesVec,
std::vector<bool>& noSpaceBeforeVec);
bool hyphenateWordAtIndex(size_t wordIndex, int availableWidth, const GfxRenderer& renderer, int fontId,
std::vector<uint16_t>& wordWidths, bool allowFallbackBreaks);
void extractLine(size_t breakIndex, int pageWidth, const std::vector<uint16_t>& wordWidths,
const std::vector<bool>& continuesVec, const std::vector<size_t>& lineBreakIndices,
const std::vector<bool>& continuesVec, const std::vector<bool>& noSpaceBeforeVec,
const std::vector<size_t>& lineBreakIndices,
const std::function<void(std::shared_ptr<TextBlock>)>& processLine, const GfxRenderer& renderer,
int fontId);
std::vector<uint16_t> calculateWordWidths(const GfxRenderer& renderer, int fontId);
+2 -1
View File
@@ -10,7 +10,8 @@
#include "parsers/ChapterHtmlSlimParser.h"
namespace {
constexpr uint8_t SECTION_FILE_VERSION = 26;
// v27: words NFC-composed at layout time; bump invalidates NFD section caches.
constexpr uint8_t SECTION_FILE_VERSION = 27;
constexpr uint32_t HEADER_SIZE = sizeof(uint8_t) + sizeof(int) + sizeof(float) + sizeof(bool) + sizeof(uint8_t) +
sizeof(uint16_t) + sizeof(uint16_t) + sizeof(uint16_t) + sizeof(bool) + sizeof(bool) +
sizeof(uint8_t) + sizeof(bool) + sizeof(uint32_t) + sizeof(uint32_t) +
+51 -48
View File
@@ -13,54 +13,57 @@ struct EntityPair {
// Sorted lexicographically by key to allow binary search.
static constexpr EntityPair ENTITY_LOOKUP[] = {
{"&AElig;", "Æ"}, {"&Aacute;", "Á"}, {"&Acirc;", "Â"}, {"&Agrave;", "À"}, {"&Alpha;", "Α"},
{"&Aring;", "Å"}, {"&Atilde;", "Ã"}, {"&Auml;", "Ä"}, {"&Beta;", "Β"}, {"&Ccedil;", "Ç"},
{"&Chi;", "Χ"}, {"&Dagger;", ""}, {"&Delta;", "Δ"}, {"&ETH;", "Ð"}, {"&Eacute;", "É"},
{"&Ecirc;", "Ê"}, {"&Egrave;", "È"}, {"&Epsilon;", "Ε"}, {"&Eta;", "Η"}, {"&Euml;", "Ë"},
{"&Gamma;", "Γ"}, {"&Iacute;", "Í"}, {"&Icirc;", "Î"}, {"&Igrave;", "Ì"}, {"&Iota;", "Ι"},
{"&Iuml;", "Ï"}, {"&Kappa;", "Κ"}, {"&Lambda;", "Λ"}, {"&Mu;", "Μ"}, {"&Ntilde;", "Ñ"},
{"&Nu;", "Ν"}, {"&OElig;", "Œ"}, {"&Oacute;", "Ó"}, {"&Ocirc;", "Ô"}, {"&Ograve;", "Ò"},
{"&Omega;", "Ω"}, {"&Omicron;", "Ο"}, {"&Oslash;", "Ø"}, {"&Otilde;", "Õ"}, {"&Ouml;", "Ö"},
{"&Phi;", "Φ"}, {"&Pi;", "Π"}, {"&Prime;", ""}, {"&Psi;", "Ψ"}, {"&Rho;", "Ρ"},
{"&Scaron;", "Š"}, {"&Sigma;", "Σ"}, {"&THORN;", "Þ"}, {"&Tau;", "Τ"}, {"&Theta;", "Θ"},
{"&Uacute;", "Ú"}, {"&Ucirc;", "Û"}, {"&Ugrave;", "Ù"}, {"&Upsilon;", "Υ"}, {"&Uuml;", "Ü"},
{"&Xi;", "Ξ"}, {"&Yacute;", "Ý"}, {"&Yuml;", "Ÿ"}, {"&Zeta;", "Ζ"}, {"&aacute;", "á"},
{"&acirc;", "â"}, {"&acute;", "´"}, {"&aelig;", "æ"}, {"&agrave;", "à"}, {"&alpha;", "α"},
{"&amp;", "&"}, {"&and;", ""}, {"&ang;", ""}, {"&aring;", "å"}, {"&asymp;", ""},
{"&atilde;", "ã"}, {"&auml;", "ä"}, {"&bdquo;", ""}, {"&beta;", "β"}, {"&brvbar;", "¦"},
{"&bull;", ""}, {"&cap;", ""}, {"&ccedil;", "ç"}, {"&cedil;", "¸"}, {"&cent;", "¢"},
{"&chi;", "χ"}, {"&circ;", "ˆ"}, {"&clubs;", ""}, {"&cong;", ""}, {"&copy;", "©"},
{"&crarr;", ""}, {"&cup;", ""}, {"&curren;", "¤"}, {"&dagger;", ""}, {"&darr;", ""},
{"&deg;", "°"}, {"&delta;", "δ"}, {"&diams;", ""}, {"&divide;", "÷"}, {"&eacute;", "é"},
{"&ecirc;", "ê"}, {"&egrave;", "è"}, {"&empty;", ""}, {"&emsp;", " "}, {"&ensp;", " "},
{"&epsilon;", "ε"}, {"&equiv;", ""}, {"&eta;", "η"}, {"&eth;", "ð"}, {"&euml;", "ë"},
{"&euro;", ""}, {"&exist;", ""}, {"&fnof;", "ƒ"}, {"&forall;", ""}, {"&frac12;", "½"},
{"&frac14;", "¼"}, {"&frac34;", "¾"}, {"&frasl;", ""}, {"&gamma;", "γ"}, {"&ge;", ""},
{"&gt;", ">"}, {"&harr;", ""}, {"&hearts;", ""}, {"&hellip;", ""}, {"&iacute;", "í"},
{"&icirc;", "î"}, {"&iexcl;", "¡"}, {"&igrave;", "ì"}, {"&infin;", ""}, {"&int;", ""},
{"&iota;", "ι"}, {"&iquest;", "¿"}, {"&isin;", ""}, {"&iuml;", "ï"}, {"&kappa;", "κ"},
{"&lambda;", "λ"}, {"&laquo;", "«"}, {"&larr;", ""}, {"&lceil;", ""}, {"&ldquo;", "\u201C"},
{"&le;", ""}, {"&lfloor;", ""}, {"&lowast;", ""}, {"&loz;", ""}, {"&lrm;", "\u200E"},
{"&lsaquo;", ""}, {"&lsquo;", "\u2018"}, {"&lt;", "<"}, {"&macr;", "¯"}, {"&mdash;", ""},
{"&micro;", "µ"}, {"&minus;", ""}, {"&mu;", "μ"}, {"&nabla;", ""}, {"&nbsp;", "\xC2\xA0"},
{"&ndash;", ""}, {"&ne;", ""}, {"&ni;", ""}, {"&not;", "¬"}, {"&notin;", ""},
{"&nsub;", ""}, {"&ntilde;", "ñ"}, {"&nu;", "ν"}, {"&oacute;", "ó"}, {"&ocirc;", "ô"},
{"&oelig;", "œ"}, {"&ograve;", "ò"}, {"&oline;", ""}, {"&omega;", "ω"}, {"&omicron;", "ο"},
{"&oplus;", ""}, {"&or;", ""}, {"&ordf;", "ª"}, {"&ordm;", "º"}, {"&oslash;", "ø"},
{"&otilde;", "õ"}, {"&otimes;", ""}, {"&ouml;", "ö"}, {"&para;", ""}, {"&part;", ""},
{"&permil;", ""}, {"&perp;", ""}, {"&phi;", "φ"}, {"&pi;", "π"}, {"&piv;", "ϖ"},
{"&plusmn;", "±"}, {"&pound;", "£"}, {"&prime;", ""}, {"&prod;", ""}, {"&prop;", ""},
{"&psi;", "ψ"}, {"&quot;", "\""}, {"&radic;", ""}, {"&raquo;", "»"}, {"&rarr;", ""},
{"&rceil;", ""}, {"&rdquo;", "\u201D"}, {"&reg;", "®"}, {"&rfloor;", ""}, {"&rho;", "ρ"},
{"&rlm;", "\u200F"}, {"&rsaquo;", ""}, {"&rsquo;", "\u2019"}, {"&sbquo;", ""}, {"&scaron;", "š"},
{"&sdot;", ""}, {"&sect;", "§"}, {"&shy;", "\xC2\xAD"}, {"&sigma;", "σ"}, {"&sigmaf;", "ς"},
{"&sim;", ""}, {"&spades;", ""}, {"&sub;", ""}, {"&sube;", ""}, {"&sum;", ""},
{"&sup1;", "¹"}, {"&sup2;", "²"}, {"&sup3;", "³"}, {"&sup;", ""}, {"&supe;", ""},
{"&szlig;", "ß"}, {"&tau;", "τ"}, {"&there4;", ""}, {"&theta;", "θ"}, {"&thetasym;", "ϑ"},
{"&thinsp;", " "}, {"&thorn;", "þ"}, {"&tilde;", "˜"}, {"&times;", "×"}, {"&trade;", ""},
{"&uacute;", "ú"}, {"&uarr;", ""}, {"&ucirc;", "û"}, {"&ugrave;", "ù"}, {"&uml;", "¨"},
{"&upsih;", "ϒ"}, {"&upsilon;", "υ"}, {"&uuml;", "ü"}, {"&xi;", "ξ"}, {"&yacute;", "ý"},
{"&yen;", "¥"}, {"&yuml;", "ÿ"}, {"&zeta;", "ζ"}, {"&zwj;", "\u200D"}, {"&zwnj;", "\u200C"},
{"&AElig;", "Æ"}, {"&Aacute;", "Á"}, {"&Acirc;", "Â"}, {"&Agrave;", "À"}, {"&Alpha;", "Α"},
{"&Aring;", "Å"}, {"&Atilde;", "Ã"}, {"&Auml;", "Ä"}, {"&Beta;", "Β"}, {"&Ccedil;", "Ç"},
{"&Chi;", "Χ"}, {"&Dagger;", ""}, {"&Delta;", "Δ"}, {"&ETH;", "Ð"}, {"&Eacute;", "É"},
{"&Ecirc;", "Ê"}, {"&Egrave;", "È"}, {"&Epsilon;", "Ε"}, {"&Eta;", "Η"}, {"&Euml;", "Ë"},
{"&Gamma;", "Γ"}, {"&Iacute;", "Í"}, {"&Icirc;", "Î"}, {"&Igrave;", "Ì"}, {"&Iota;", "Ι"},
{"&Iuml;", "Ï"}, {"&Kappa;", "Κ"}, {"&Lambda;", "Λ"}, {"&Mu;", "Μ"}, {"&Ntilde;", "Ñ"},
{"&Nu;", "Ν"}, {"&OElig;", "Œ"}, {"&Oacute;", "Ó"}, {"&Ocirc;", "Ô"}, {"&Ograve;", "Ò"},
{"&Omega;", "Ω"}, {"&Omicron;", "Ο"}, {"&Oslash;", "Ø"}, {"&Otilde;", "Õ"}, {"&Ouml;", "Ö"},
{"&Phi;", "Φ"}, {"&Pi;", "Π"}, {"&Prime;", ""}, {"&Psi;", "Ψ"}, {"&Rho;", "Ρ"},
{"&Scaron;", "Š"}, {"&Sigma;", "Σ"}, {"&THORN;", "Þ"}, {"&Tau;", "Τ"}, {"&Theta;", "Θ"},
{"&Uacute;", "Ú"}, {"&Ucirc;", "Û"}, {"&Ugrave;", "Ù"}, {"&Upsilon;", "Υ"}, {"&Uuml;", "Ü"},
{"&Xi;", "Ξ"}, {"&Yacute;", "Ý"}, {"&Yuml;", "Ÿ"}, {"&Zeta;", "Ζ"}, {"&aacute;", "á"},
{"&acirc;", "â"}, {"&acute;", "´"}, {"&aelig;", "æ"}, {"&agrave;", "à"}, {"&alefsym;", ""},
{"&alpha;", "α"}, {"&amp;", "&"}, {"&and;", ""}, {"&ang;", ""}, {"&aring;", "å"},
{"&asymp;", ""}, {"&atilde;", "ã"}, {"&auml;", "ä"}, {"&bdquo;", ""}, {"&beta;", "β"},
{"&brvbar;", "¦"}, {"&bull;", ""}, {"&cap;", ""}, {"&ccedil;", "ç"}, {"&cedil;", "¸"},
{"&cent;", "¢"}, {"&chi;", "χ"}, {"&circ;", "ˆ"}, {"&clubs;", ""}, {"&cong;", ""},
{"&copy;", "©"}, {"&crarr;", ""}, {"&cup;", ""}, {"&curren;", "¤"}, {"&dArr;", ""},
{"&dagger;", ""}, {"&darr;", ""}, {"&deg;", "°"}, {"&delta;", "δ"}, {"&diams;", ""},
{"&divide;", "÷"}, {"&eacute;", "é"}, {"&ecirc;", "ê"}, {"&egrave;", "è"}, {"&empty;", ""},
{"&emsp;", " "}, {"&ensp;", " "}, {"&epsilon;", "ε"}, {"&equiv;", ""}, {"&eta;", "η"},
{"&eth;", "ð"}, {"&euml;", "ë"}, {"&euro;", ""}, {"&exist;", ""}, {"&fnof;", "ƒ"},
{"&forall;", ""}, {"&frac12;", "½"}, {"&frac14;", "¼"}, {"&frac34;", "¾"}, {"&frasl;", ""},
{"&gamma;", "γ"}, {"&ge;", ""}, {"&gt;", ">"}, {"&hArr;", ""}, {"&harr;", ""},
{"&hearts;", ""}, {"&hellip;", ""}, {"&iacute;", "í"}, {"&icirc;", "î"}, {"&iexcl;", "¡"},
{"&igrave;", "ì"}, {"&image;", ""}, {"&infin;", ""}, {"&int;", ""}, {"&iota;", "ι"},
{"&iquest;", "¿"}, {"&isin;", ""}, {"&iuml;", "ï"}, {"&kappa;", "κ"}, {"&lArr;", ""},
{"&lambda;", "λ"}, {"&lang;", ""}, {"&laquo;", "«"}, {"&larr;", ""}, {"&lceil;", ""},
{"&ldquo;", "\u201C"}, {"&le;", ""}, {"&lfloor;", ""}, {"&lowast;", ""}, {"&loz;", ""},
{"&lrm;", "\u200E"}, {"&lsaquo;", ""}, {"&lsquo;", "\u2018"}, {"&lt;", "<"}, {"&macr;", "¯"},
{"&mdash;", ""}, {"&micro;", "µ"}, {"&middot;", "·"}, {"&minus;", ""}, {"&mu;", "μ"},
{"&nabla;", ""}, {"&nbsp;", "\xC2\xA0"}, {"&ndash;", ""}, {"&ne;", ""}, {"&ni;", ""},
{"&not;", "¬"}, {"&notin;", ""}, {"&nsub;", ""}, {"&ntilde;", "ñ"}, {"&nu;", "ν"},
{"&oacute;", "ó"}, {"&ocirc;", "ô"}, {"&oelig;", "œ"}, {"&ograve;", "ò"}, {"&oline;", ""},
{"&omega;", "ω"}, {"&omicron;", "ο"}, {"&oplus;", ""}, {"&or;", ""}, {"&ordf;", "ª"},
{"&ordm;", "º"}, {"&oslash;", "ø"}, {"&otilde;", "õ"}, {"&otimes;", ""}, {"&ouml;", "ö"},
{"&para;", ""}, {"&part;", ""}, {"&permil;", ""}, {"&perp;", ""}, {"&phi;", "φ"},
{"&pi;", "π"}, {"&piv;", "ϖ"}, {"&plusmn;", "±"}, {"&pound;", "£"}, {"&prime;", ""},
{"&prod;", ""}, {"&prop;", ""}, {"&psi;", "ψ"}, {"&quot;", "\""}, {"&rArr;", ""},
{"&radic;", ""}, {"&rang;", ""}, {"&raquo;", "»"}, {"&rarr;", ""}, {"&rceil;", ""},
{"&rdquo;", "\u201D"}, {"&real;", "\u211C"}, {"&reg;", "®"}, {"&rfloor;", ""}, {"&rho;", "ρ"},
{"&rlm;", "\u200F"}, {"&rsaquo;", ""}, {"&rsquo;", "\u2019"}, {"&sbquo;", ""}, {"&scaron;", "š"},
{"&sdot;", ""}, {"&sect;", "§"}, {"&shy;", "\xC2\xAD"}, {"&sigma;", "σ"}, {"&sigmaf;", "ς"},
{"&sim;", ""}, {"&spades;", ""}, {"&sub;", ""}, {"&sube;", ""}, {"&sum;", ""},
{"&sup1;", "¹"}, {"&sup2;", "²"}, {"&sup3;", "³"}, {"&sup;", ""}, {"&supe;", ""},
{"&szlig;", "ß"}, {"&tau;", "τ"}, {"&there4;", ""}, {"&theta;", "θ"}, {"&thetasym;", "ϑ"},
{"&thinsp;", " "}, {"&thorn;", "þ"}, {"&tilde;", "˜"}, {"&times;", "×"}, {"&trade;", ""},
{"&uArr;", ""}, {"&uacute;", "ú"}, {"&uarr;", ""}, {"&ucirc;", "û"}, {"&ugrave;", "ù"},
{"&uml;", "¨"}, {"&upsih;", "ϒ"}, {"&upsilon;", "υ"}, {"&uuml;", "ü"}, {"&weierp;", ""},
{"&xi;", "ξ"}, {"&yacute;", "ý"}, {"&yen;", "¥"}, {"&yuml;", "ÿ"}, {"&zeta;", "ζ"},
{"&zwj;", "\u200D"}, {"&zwnj;", "\u200C"},
};
// Verify the table is sorted at compile time.
+10
View File
@@ -1502,8 +1502,18 @@ int GfxRenderer::getTextAdvanceX(const int fontId, const char* text, EpdFontFami
int32_t widthFP = 0;
const bool isSupSub = (style & (EpdFontFamily::SUP | EpdFontFamily::SUB)) != 0;
const uint8_t styleIdx = resolveSdCardStyle(*sdIt->second, style);
const auto fontIt = fontMap.find(fontId);
if (fontIt == fontMap.end()) {
LOG_ERR("GFX", "Font %d not found", fontId);
return 0;
}
const auto& font = fontIt->second;
while (uint32_t cp = utf8NextCodepoint(reinterpret_cast<const uint8_t**>(&text))) {
int32_t advFP = sdIt->second->getAdvance(cp, styleIdx);
if (advFP == 0 && !utf8IsCombiningMark(cp)) {
const EpdGlyph* glyph = font.getGlyph(cp, style);
advFP = glyph ? glyph->advanceX : 0;
}
widthFP += isSupSub ? (advFP + 1) / 2 : advFP;
}
return fp4::toPixel(widthFP);
+1
View File
@@ -71,6 +71,7 @@ STR_SIDE_BTN_LAYOUT: "Бакавыя кнопкі"
STR_TOUCH_READER_CONTROLS: "Сэнсарнае кіраванне чытаннем"
STR_FRONT_BTN_FOLLOW_ORIENTATION: "Арыентаваць пярэднія кнопкі"
STR_LONG_PRESS_SKIP: "Доўгае націсканне - змена раздзела"
STR_FONT_PREVIEW_TEXT: "У Іўі худы жвавы чорт у зялёнай камізэльцы пабег пад'есці фаршу з юшкай"
STR_FONT_FAMILY: "Шрыфт чытання"
STR_FONT_SIZE: "Памер шрыфту інтэрфейсу"
STR_LINE_SPACING: "Міжрадковы інтэрвал"
+5
View File
@@ -78,6 +78,7 @@ STR_LONG_PRESS_BEHAVIOR: "Comportament de prémer llargament el 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_LINE_SPACING: "Interlineat del lector"
@@ -139,6 +140,8 @@ STR_INVERTED: "Invertit"
STR_LANDSCAPE_CCW: "Horitzontal antihorari"
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_NOTO_SERIF: "Noto Serif"
STR_NOTO_SANS: "Noto Sans"
@@ -295,6 +298,7 @@ STR_BOOK_S_STYLE: "Estil del llibre"
STR_EMBEDDED_STYLE: "Estil incrustat"
STR_FOCUS_READING: "Lectura enfocada"
STR_OPDS_SERVER_URL: "URL del servidor OPDS"
STR_PWR_BTN_FOOTNOTE_BACK: "Retorn ràpid des de les notes al peu"
STR_FOOTNOTES: "Notes al peu"
STR_NO_FOOTNOTES: "No hi ha notes al peu en aquesta pàgina"
STR_LINK: "[enllaç]"
@@ -339,6 +343,7 @@ 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_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_INSTALLED: "Instal·lat"
+1
View File
@@ -74,6 +74,7 @@ STR_LONG_PRESS_BEHAVIOR: "Chování při dlouhém stisknutí tlačítka"
STR_LONG_PRESS_BEHAVIOR_OFF: "VYP"
STR_LONG_PRESS_BEHAVIOR_SKIP: "Přeskočení kapitoly"
STR_LONG_PRESS_BEHAVIOR_ORIENTATION: "Změna orientace"
STR_FONT_PREVIEW_TEXT: "Příliš žluťoučký kůň úpěl ďábelské ódy"
STR_FONT_FAMILY: "Rodina písem čtečky"
STR_FONT_SIZE: "Velikost písma rozhraní"
STR_LINE_SPACING: "Řádkování čtečky"
+1
View File
@@ -78,6 +78,7 @@ STR_LONG_PRESS_BEHAVIOR: "Comportamiento al mantener pulsado el botón"
STR_LONG_PRESS_BEHAVIOR_OFF: "Desactivado"
STR_LONG_PRESS_BEHAVIOR_SKIP: "Saltar capítulo"
STR_LONG_PRESS_BEHAVIOR_ORIENTATION: "Cambio de orientación"
STR_FONT_PREVIEW_TEXT: "Høj bly gom vandt fræk sexquiz på wc"
STR_FONT_FAMILY: "Læser skrifttype"
STR_FONT_SIZE: "Læser skriftstørrelse"
STR_LINE_SPACING: "Linjeafstand"
+1
View File
@@ -78,6 +78,7 @@ STR_LONG_PRESS_BEHAVIOR: "Long-press button behavior"
STR_LONG_PRESS_BEHAVIOR_OFF: "OFF"
STR_LONG_PRESS_BEHAVIOR_SKIP: "Chapter skip"
STR_LONG_PRESS_BEHAVIOR_ORIENTATION: "Orientation change"
STR_FONT_PREVIEW_TEXT: "Pa's wijze lynx bezag vroom het fikse aquaduct"
STR_FONT_FAMILY: "Lettertype lezer"
STR_FONT_SIZE: "Lettergrootte lezer"
STR_LINE_SPACING: "Regelafstand lezer"
+4
View File
@@ -79,6 +79,8 @@ STR_LONG_PRESS_BEHAVIOR: "Long-press button behavior"
STR_LONG_PRESS_BEHAVIOR_OFF: "OFF"
STR_LONG_PRESS_BEHAVIOR_SKIP: "Chapter skip"
STR_LONG_PRESS_BEHAVIOR_ORIENTATION: "Orientation change"
STR_LONG_PRESS_MENU: "Long-press Menu"
STR_FONT_PREVIEW_TEXT: "The quick brown fox jumps over the lazy dog"
STR_FONT_FAMILY: "Reader Font Family"
STR_FONT_SIZE: "Reader Font Size"
STR_LINE_SPACING: "Reader Line Spacing"
@@ -141,6 +143,8 @@ STR_INVERTED: "Inverted"
STR_LANDSCAPE_CCW: "Landscape CCW"
STR_PREV_NEXT: "Prev/Next"
STR_NEXT_PREV: "Next/Prev"
STR_KOSYNC: "KOSync"
STR_BOOKMARK_OPTION: "Bookmark"
STR_DISABLED: "Disabled"
STR_NOTO_SERIF: "Noto Serif"
STR_NOTO_SANS: "Noto Sans"
+1
View File
@@ -74,6 +74,7 @@ STR_LONG_PRESS_BEHAVIOR: "Long-press button behavior"
STR_LONG_PRESS_BEHAVIOR_OFF: "OFF"
STR_LONG_PRESS_BEHAVIOR_SKIP: "Chapter skip"
STR_LONG_PRESS_BEHAVIOR_ORIENTATION: "Orientation change"
STR_FONT_PREVIEW_TEXT: "Törkylempijävongahdus"
STR_FONT_FAMILY: "Lukijan fonttiperhe"
STR_FONT_SIZE: "Käyttöliittymän fonttikoko"
STR_LINE_SPACING: "Lukijan riviväli"
+1
View File
@@ -78,6 +78,7 @@ STR_LONG_PRESS_BEHAVIOR: "Comportement lors d'un appui long"
STR_LONG_PRESS_BEHAVIOR_OFF: "Désactivé"
STR_LONG_PRESS_BEHAVIOR_SKIP: "Saut de chapitre"
STR_LONG_PRESS_BEHAVIOR_ORIENTATION: "Changement d'orientation"
STR_FONT_PREVIEW_TEXT: "Portez ce vieux whisky au juge blond qui fume"
STR_FONT_FAMILY: "Police de caractères du lecteur"
STR_FONT_SIZE: "Taille police lecteur"
STR_LINE_SPACING: "Interligne"
+1
View File
@@ -74,6 +74,7 @@ STR_LONG_PRESS_BEHAVIOR: "Verhalten bei langem Tastendruck"
STR_LONG_PRESS_BEHAVIOR_OFF: "AUS"
STR_LONG_PRESS_BEHAVIOR_SKIP: "Kapitel überspringen"
STR_LONG_PRESS_BEHAVIOR_ORIENTATION: "Ausrichtung ändern"
STR_FONT_PREVIEW_TEXT: "Victor jagt zwölf Boxkämpfer quer über den großen Sylter Deich"
STR_FONT_FAMILY: "Lese-Schriftfamilie"
STR_FONT_SIZE: "Schriftgröße"
STR_LINE_SPACING: "Lese-Zeilenabstand"
+1
View File
@@ -78,6 +78,7 @@ STR_LONG_PRESS_BEHAVIOR: "פעולת לחיצה ארוכה"
STR_LONG_PRESS_BEHAVIOR_OFF: "כבוי"
STR_LONG_PRESS_BEHAVIOR_SKIP: "דלג פרק"
STR_LONG_PRESS_BEHAVIOR_ORIENTATION: "שנה כיוון מסך"
STR_FONT_PREVIEW_TEXT: "דג סקרן שט בים מאוכזב ולפתע מצא חברה"
STR_FONT_FAMILY: "גופן הקריאה"
STR_FONT_SIZE: "גודל גופן"
STR_LINE_SPACING: "מרווח בין שורות"
+1
View File
@@ -75,6 +75,7 @@ STR_SIDE_BTN_LAYOUT: "Oldalsó gomb elrendezés (olvasó)"
STR_TOUCH_READER_CONTROLS: "Érintőképernyős vezérlés (olvasó)"
STR_FRONT_BTN_FOLLOW_ORIENTATION: "Elülső gombok tájolása"
STR_LONG_PRESS_SKIP: "Hosszú nyomás - fejezet ugrás"
STR_FONT_PREVIEW_TEXT: "Egy hűtlen vejét fülöncsípő, dühös mexikói úr Wesselényinél mázol Quitóban"
STR_FONT_FAMILY: "Olvasó betűkészlet"
STR_FONT_SIZE: "Olvasó betűméret"
STR_LINE_SPACING: "Olvasó sorköz"
+6 -1
View File
@@ -78,6 +78,8 @@ STR_LONG_PRESS_BEHAVIOR: "Press. lunga pul. laterali"
STR_LONG_PRESS_BEHAVIOR_OFF: "OFF"
STR_LONG_PRESS_BEHAVIOR_SKIP: "Salta capitolo"
STR_LONG_PRESS_BEHAVIOR_ORIENTATION: "Orientamento"
STR_LONG_PRESS_MENU: "Menu press. lunga"
STR_FONT_PREVIEW_TEXT: "Pranzo d'acqua fa volti sghembi"
STR_FONT_FAMILY: "Font lettore"
STR_FONT_SIZE: "Dimensione font"
STR_LINE_SPACING: "Interlinea lettore"
@@ -376,4 +378,7 @@ STR_CLOCK_SYNC_NO_WIFI: "Wi-Fi non connesso"
STR_HOLD_CONFIRM_TO_DELETE: "Tieni premuto Conferma per cancellare"
STR_NEXT_FIELD: "Succ."
STR_CURRENT_TIME: "Ora attuale: "
STR_DISABLED: "Disattivato"
STR_DISABLED: "Disattivato"
STR_BOOKMARK_OPTION: "Segnalibro"
STR_KOSYNC: "KOSync"
STR_PWR_BTN_FOOTNOTE_BACK: "Rientro rapido dalle note"
+1
View File
@@ -70,6 +70,7 @@ STR_SIDE_BTN_LAYOUT: "Бүйірлік түймелер орналасуы (оқ
STR_TOUCH_READER_CONTROLS: "Сенсорлық басқару (оқырман)"
STR_FRONT_BTN_FOLLOW_ORIENTATION: "Алдыңғы түймелерді бағдарлау"
STR_LONG_PRESS_SKIP: "Ұзақ басу арқылы тарау өткізу"
STR_FONT_PREVIEW_TEXT: "Канагаттандырылмагандыктарыныздан"
STR_FONT_FAMILY: "Оқырман қаріп тобы"
STR_FONT_SIZE: "Интерфейс қаріп өлшемі"
STR_LINE_SPACING: "Оқырман жол аралығы"
+1
View File
@@ -75,6 +75,7 @@ STR_SIDE_BTN_LAYOUT: "Šoniniai mygtukai"
STR_TOUCH_READER_CONTROLS: "Liečiamasis valdymas (skaityklė)"
STR_FRONT_BTN_FOLLOW_ORIENTATION: "Orientuoti priekinius mygtukus"
STR_LONG_PRESS_SKIP: "Praleisti skyrių (ilgai)"
STR_FONT_PREVIEW_TEXT: "Įlinkdama fechtuotojo špaga sublykčiojusi pragręžė apvalų arbūzą"
STR_FONT_FAMILY: "Šriftas"
STR_FONT_SIZE: "Šrifto dydis"
STR_LINE_SPACING: "Tarpai tarp eilučių"
+1
View File
@@ -78,6 +78,7 @@ STR_LONG_PRESS_BEHAVIOR: "Funkcja długiego przyciśnięcia"
STR_LONG_PRESS_BEHAVIOR_OFF: "Wył."
STR_LONG_PRESS_BEHAVIOR_SKIP: "Przeskocz rozdział"
STR_LONG_PRESS_BEHAVIOR_ORIENTATION: "Orientacja ekranu"
STR_FONT_PREVIEW_TEXT: "Pchnąć w tę łódź jeża lub ośm skrzyń fig"
STR_FONT_FAMILY: "Czcionka"
STR_FONT_SIZE: "Rozmiar czcionki"
STR_LINE_SPACING: "Odstępy między wierszami"
+1
View File
@@ -74,6 +74,7 @@ STR_LONG_PRESS_BEHAVIOR: "Comportamento do botão de premir e segurar"
STR_LONG_PRESS_BEHAVIOR_OFF: "Desligado"
STR_LONG_PRESS_BEHAVIOR_SKIP: "Saltar capítulo"
STR_LONG_PRESS_BEHAVIOR_ORIENTATION: "Alterar orientação"
STR_FONT_PREVIEW_TEXT: "Vejo galã sexy pôr quinze kiwis à força em baú achatado"
STR_FONT_FAMILY: "Fonte do leitor"
STR_FONT_SIZE: "Tam. fonte UI"
STR_LINE_SPACING: "Espaçamento entre linhas"
+1
View File
@@ -78,6 +78,7 @@ STR_LONG_PRESS_BEHAVIOR: "Comportament buton apăsat lung"
STR_LONG_PRESS_BEHAVIOR_OFF: "Dezactivat"
STR_LONG_PRESS_BEHAVIOR_SKIP: "Sărire capitol"
STR_LONG_PRESS_BEHAVIOR_ORIENTATION: "Schimbă orientarea"
STR_FONT_PREVIEW_TEXT: "Încă vând gem, whisky bej și tequila roz, preț fix"
STR_FONT_FAMILY: "Familie font lectură"
STR_FONT_SIZE: "Dimensiune font"
STR_LINE_SPACING: "Spaţiere între rânduri"
+5 -1
View File
@@ -79,6 +79,8 @@ STR_LONG_PRESS_BEHAVIOR: "Долгое нажатие"
STR_LONG_PRESS_BEHAVIOR_OFF: "Ничего"
STR_LONG_PRESS_BEHAVIOR_SKIP: "Пропуск главы"
STR_LONG_PRESS_BEHAVIOR_ORIENTATION: "Изменить ориентацию"
STR_LONG_PRESS_MENU: "Долгое нажатие меню"
STR_FONT_PREVIEW_TEXT: "Съешь ещё этих мягких французских булок, да выпей же чаю"
STR_FONT_FAMILY: "Шрифт чтения"
STR_FONT_SIZE: "Размер шрифта интерфейса"
STR_LINE_SPACING: "Межстрочный интервал"
@@ -142,7 +144,9 @@ STR_INVERTED: "Инверсия"
STR_LANDSCAPE_CCW: "Ландшафт (CCW)"
STR_PREV_NEXT: "Назад/Вперёд"
STR_NEXT_PREV: "Вперёд/Назад"
STR_DISABLED: "Выключены"
STR_KOSYNC: "KOSync"
STR_BOOKMARK_OPTION: "Закладка"
STR_DISABLED: "Выключено"
STR_NOTO_SERIF: "Noto Serif"
STR_NOTO_SANS: "Noto Sans"
STR_SMALL: "Маленький"
+1
View File
@@ -79,6 +79,7 @@ STR_LONG_PRESS_BEHAVIOR: "Správanie pri dlhom stlačení tlačidla"
STR_LONG_PRESS_BEHAVIOR_OFF: "VYP"
STR_LONG_PRESS_BEHAVIOR_SKIP: "Preskočiť kapitolu"
STR_LONG_PRESS_BEHAVIOR_ORIENTATION: "Zmeniť orientáciu"
STR_FONT_PREVIEW_TEXT: "Vypätá dcéra grófa Maxwella s IQ nižším ako kôň núti čeľaď hrýzť hŕbu jabĺk"
STR_FONT_FAMILY: "Rodina písiem čítačky"
STR_FONT_SIZE: "Veľkosť písma rozhrania"
STR_LINE_SPACING: "Riadkovanie čítačky"
+1
View File
@@ -75,6 +75,7 @@ STR_SIDE_BTN_LAYOUT: "Razpored stranskih gumbov"
STR_TOUCH_READER_CONTROLS: "Dotikalni nadzor (bralnik)"
STR_FRONT_BTN_FOLLOW_ORIENTATION: "Usmeri sprednje gumbe"
STR_LONG_PRESS_SKIP: "Dolgi pritisk za preskok poglavja"
STR_FONT_PREVIEW_TEXT: "V kožuščku hudobnega fanta stopiclja mizar"
STR_FONT_FAMILY: "Pisava bralnika"
STR_FONT_SIZE: "Velikost pisave"
STR_LINE_SPACING: "Razmik med vrsticami"
+6 -1
View File
@@ -78,6 +78,8 @@ STR_LONG_PRESS_BEHAVIOR: "Al mantener pulsado un botón"
STR_LONG_PRESS_BEHAVIOR_OFF: "No hacer nada"
STR_LONG_PRESS_BEHAVIOR_SKIP: "Saltar capítulo"
STR_LONG_PRESS_BEHAVIOR_ORIENTATION: "Cambiar orient."
STR_LONG_PRESS_MENU: "Función de pulsación larga"
STR_FONT_PREVIEW_TEXT: "Benjamín pidió una bebida de kiwi y fresa. Noé, sin vergüenza, la más exquisita champaña del menú"
STR_FONT_FAMILY: "Tipografía"
STR_FONT_SIZE: "Tamaño"
STR_LINE_SPACING: "Interlineado"
@@ -140,6 +142,8 @@ STR_INVERTED: "Invertido"
STR_LANDSCAPE_CCW: "Horizontal (antihorario)"
STR_PREV_NEXT: "Ant./Sig."
STR_NEXT_PREV: "Sig./Ant."
STR_KOSYNC: "KOSync"
STR_BOOKMARK_OPTION: "Marcador"
STR_DISABLED: "Desactivados"
STR_NOTO_SERIF: "Noto Serif"
STR_NOTO_SANS: "Noto Sans"
@@ -319,8 +323,9 @@ STR_BOOK_S_STYLE: "Estilo del libro"
STR_EMBEDDED_STYLE: "Estilo integrado"
STR_FOCUS_READING: "Lectura enfocada"
STR_OPDS_SERVER_URL: "URL del servidor OPDS"
STR_PWR_BTN_FOOTNOTE_BACK: "Retorno rápido desde las notas al pie"
STR_SET_SLEEP_COVER: "Pant. sus."
STR_FOOTNOTES: "Pie de página"
STR_FOOTNOTES: "Notas al pie"
STR_NO_FOOTNOTES: "No hay notas al pie de esta página"
STR_LINK: "[enlace]"
STR_SLEEP_TIMER_VALUE_FORMAT: "%u min."
+1
View File
@@ -79,6 +79,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_FONT_PREVIEW_TEXT: "Flygande bäckasiner söka hwila på mjuka tuvor"
STR_FONT_FAMILY: "Eboksläsarens typsnittsfamilj"
STR_FONT_SIZE: "Eboksläsarens typsnittsstorlek"
STR_LINE_SPACING: "Eboksläsarens linjemellanrum"
+1
View File
@@ -73,6 +73,7 @@ STR_LONG_PRESS_BEHAVIOR: "Long-press button behavior"
STR_LONG_PRESS_BEHAVIOR_OFF: "OFF"
STR_LONG_PRESS_BEHAVIOR_SKIP: "Chapter skip"
STR_LONG_PRESS_BEHAVIOR_ORIENTATION: "Orientation change"
STR_FONT_PREVIEW_TEXT: "Pijamalı hasta yağız şoföre çabucak güvendi"
STR_FONT_FAMILY: "Okuyucu Yazı Tipi Ailesi"
STR_FONT_SIZE: "Arayüz Yazı Boyutu"
STR_LINE_SPACING: "Okuyucu Satır Aralığı"
+28 -5
View File
@@ -61,6 +61,7 @@ STR_CAT_READER: "Читач"
STR_CAT_CONTROLS: "Кнопки"
STR_CAT_SYSTEM: "Система"
STR_SLEEP_SCREEN: "Екран у режимі сну"
STR_QUICK_RESUME_TIMEOUT: "Швидке поверн. після таймауту"
STR_SLEEP_COVER_MODE: "Режим заповнення"
STR_HIDE_BATTERY: "Приховати % батареї"
STR_EXTRA_SPACING: "Додатковий інтервал між абзацами"
@@ -78,6 +79,7 @@ STR_LONG_PRESS_BEHAVIOR: "Поведінка при довгому настик
STR_LONG_PRESS_BEHAVIOR_OFF: "Немає"
STR_LONG_PRESS_BEHAVIOR_SKIP: "Наступ. розділ (утримув.)"
STR_LONG_PRESS_BEHAVIOR_ORIENTATION: "Зміна орієнтації екрану"
STR_FONT_PREVIEW_TEXT: "Єхидна, ґава, їжак ще й шиплячі плазуни бігцем форсують Янцзи"
STR_FONT_FAMILY: "Шрифт"
STR_FONT_SIZE: "Розмір шрифту"
STR_LINE_SPACING: "Міжрядковий інтервал"
@@ -86,8 +88,8 @@ STR_PARA_ALIGNMENT: "Вирівнювання тексту"
STR_HYPHENATION: "Перенесення слів"
STR_TIME_TO_SLEEP: "Перехід в режим сну"
STR_SHOW_HIDDEN_FILES: "Показати приховані файли"
STR_REMOVE_READ_FROM_RECENTS: "Очищати прочитані книги зі списку останніх"
STR_MOVE_FINISHED_TO_READ: "Переміщати прочитані книги до теки Read"
STR_REMOVE_READ_FROM_RECENTS: "Приховувати прочитані книги"
STR_MOVE_FINISHED_TO_READ: "Переміщати прочит. в теку Read"
STR_REFRESH_FREQ: "Частота оновлення екрану"
STR_KOREADER_SYNC: "Синхронізація KOReader"
STR_CHECK_UPDATES: "Перевірити оновлення системи"
@@ -140,6 +142,7 @@ STR_INVERTED: "Перевернутий"
STR_LANDSCAPE_CCW: "Альбом. проти год."
STR_PREV_NEXT: "Попер/Наст"
STR_NEXT_PREV: "Наст/Попер"
STR_DISABLED: "Вимкнуто"
STR_NOTO_SERIF: "Noto Serif"
STR_NOTO_SANS: "Noto Sans"
STR_SMALL: "Малий"
@@ -174,6 +177,8 @@ STR_DOWNLOADING: "Завантаження..."
STR_DOWNLOAD_FAILED: "Завантаження не вдалося"
STR_ERROR_MSG: "Помилка:"
STR_UNNAMED: "Без назви"
STR_HOLD_CONFIRM_TO_DELETE: "Утрим. Підтвердити, щоб Видалити"
STR_BOOKMARK_INSTRUCTIONS: "Утрим. Підтвердити в книзі, щоб створ. закладку"
STR_NO_SERVER_URL: "URL сервера не налаштовано"
STR_FETCH_FEED_FAILED: "Не вдалося отримати стрічку"
STR_PARSE_FEED_FAILED: "Не вдалося розпарсити стрічку"
@@ -229,18 +234,34 @@ STR_BATTERY: "Акумулятор"
STR_XTC_STATUS_BAR: "XTC Рядок прогресу"
STR_BOTTOM: "Низ"
STR_TOP: "Верх"
STR_CLOCK: "Годинник"
STR_CLOCK_UTC_OFFSET: "Часовий пояс"
STR_CLOCK_FORMAT: "Формат Годинника"
STR_CLOCK_FORMAT_24H: "24-години"
STR_CLOCK_FORMAT_12H: "12-годин"
STR_CURRENT_TIME: "Поточний час:"
STR_NEXT_FIELD: "наступний"
STR_CLOCK_SYNC: "Синхр. Годинник"
STR_CLOCK_SYNC_NOW: "Синхр. годин. зараз"
STR_CLOCK_SYNCING: "Синхр. через NTP..."
STR_CLOCK_SYNC_OK: "Успішна синхр."
STR_CLOCK_SYNC_FAIL: "Невдала синхр. "
STR_CLOCK_SYNC_NO_WIFI: "Wi-Fi не під'єднано"
STR_CLOCK_SYNC_NO_WIFI_HINT: "Під'єднайтесь спершу до Wi-Fi, тоді спробуйте знову."
STR_CLOCK_SYNCED: "Годинник синхр."
STR_UI_THEME: "Тема інтерфейсу"
STR_THEME_CLASSIC: "Класична"
STR_THEME_LYRA: "Lyra"
STR_THEME_ROUNDEDRAFF: "RoundedRaff"
STR_THEME_LYRA_EXTENDED: "Lyra Extended"
STR_SUNLIGHT_FADING_FIX: "Виправлення вицвітання на сонці"
STR_QUICK_RESUME_TIMEOUT: "Швидке продовження після таймауту"
STR_REMAP_FRONT_BUTTONS: "Налаштувати передні кнопки"
STR_BOOKMARKS: "Закладки"
STR_BOOKMARK_ADDED: "Закладку додано"
STR_OPDS_BROWSER: "Браузер OPDS"
STR_SEARCH: "Пошук"
STR_COVER_CUSTOM: "Обкл. + власне"
STR_QUICK_RESUME: "Швидке продовження"
STR_QUICK_RESUME: "Швидке поверн."
STR_MENU_RECENT_BOOKS: "Останні книги"
STR_REMOVE_FROM_RECENTS: "Видалити з останніх книг?"
STR_NO_RECENT_BOOKS: "Немає останніх книг"
@@ -266,6 +287,7 @@ STR_GO_HOME_BUTTON: "На головну"
STR_SYNC_PROGRESS: "Прогрес синхронізації"
STR_DELETE_CACHE: "Видалити кеш книги"
STR_DELETE: "Видалити"
STR_CONFIRM_DELETE_BOOKMARK: "Видалити цю закладку?"
STR_DISPLAY_QR: "Показати сторінку як QR-код"
STR_CHAPTER_PREFIX: "Розділ: "
STR_PAGES_SEPARATOR: " сторінок | "
@@ -298,14 +320,15 @@ STR_BOOK_S_STYLE: "Стиль книги"
STR_EMBEDDED_STYLE: "Вбудований стиль"
STR_FOCUS_READING: "Фокусне читання"
STR_OPDS_SERVER_URL: "URL сервера OPDS"
STR_PWR_BTN_FOOTNOTE_BACK: "Швидке повернення з приміток"
STR_SET_SLEEP_COVER: "Як обкл."
STR_FOOTNOTES: "Примітки"
STR_NO_FOOTNOTES: "На цій сторінці немає приміток"
STR_LINK: "[посилання]"
STR_SCREENSHOT_BUTTON: "Знімок екрана"
STR_SLEEP_TIMER_VALUE_FORMAT: "%u хв"
STR_SLEEP_NEVER: "Ніколи"
STR_SLEEP_TIMER_STEP_HINT: "Вліво/Вправо: 1 хв Вгору/Вниз: 5 хв"
STR_SCREENSHOT_BUTTON: "Знімок екрана"
STR_ADD_SERVER: "Додати сервер"
STR_SERVER_NAME: "Назва сервера"
STR_NO_SERVERS: "Не налаштовано жодного сервера OPDS"
+5
View File
@@ -79,6 +79,8 @@ STR_LONG_PRESS_BEHAVIOR: "Comportament de prémer llargament el 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_LINE_SPACING: "Interlineat del lector"
@@ -143,6 +145,8 @@ STR_INVERTED: "Invertit"
STR_LANDSCAPE_CCW: "Horitzontal antihorari"
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_NOTO_SERIF: "Noto Serif"
STR_NOTO_SANS: "Noto Sans"
@@ -298,6 +302,7 @@ STR_BOOK_S_STYLE: "Estil del llibre"
STR_EMBEDDED_STYLE: "Estil incrustat"
STR_FOCUS_READING: "Lectura enfocada"
STR_OPDS_SERVER_URL: "URL del servidor OPDS"
STR_PWR_BTN_FOOTNOTE_BACK: "Retorn ràpid des de les notes al peu"
STR_FOOTNOTES: "Notes al peu"
STR_NO_FOOTNOTES: "No hi ha notes al peu en esta pàgina"
STR_LINK: "[enllaç]"
+1
View File
@@ -79,6 +79,7 @@ STR_LONG_PRESS_BEHAVIOR: "Hành vi nhấn giữ nút"
STR_LONG_PRESS_BEHAVIOR_OFF: "TẮT"
STR_LONG_PRESS_BEHAVIOR_SKIP: "Nhảy chương"
STR_LONG_PRESS_BEHAVIOR_ORIENTATION: "Đổi hướng"
STR_FONT_PREVIEW_TEXT: "Trường quê em do bố của em xây kĩ nên sạch và đẹp lắm"
STR_FONT_FAMILY: "Phông chữ trình đọc"
STR_FONT_SIZE: "Cỡ chữ trình đọc"
STR_LINE_SPACING: "Giãn dòng trình đọc"
+184 -47
View File
@@ -30,8 +30,7 @@ int parseIndex(const std::string& xpath, const char* prefix, bool last = false)
int parseCharOffset(const std::string& xpath) {
const size_t textPos = xpath.rfind("text()");
if (textPos == std::string::npos) return 0;
const size_t dotPos = xpath.find('.', textPos);
const size_t dotPos = (textPos != std::string::npos) ? xpath.find('.', textPos) : xpath.rfind('.');
if (dotPos == std::string::npos || dotPos + 1 >= xpath.size()) return 0;
int val = 0;
for (size_t i = dotPos + 1; i < xpath.size(); i++) {
@@ -104,7 +103,13 @@ bool isChapterStartXPath(const std::string& xpath) {
if (dotPos == std::string::npos || dotPos <= bodyContentStart || dotPos + 1 >= xpath.size()) {
return false;
}
if (xpath.find('/', bodyContentStart) != std::string::npos) {
size_t terminalEnd = dotPos;
static constexpr char kTextNode[] = "/text()";
const size_t textNodePos = xpath.rfind(kTextNode, dotPos);
if (textNodePos != std::string::npos && textNodePos >= bodyContentStart) {
terminalEnd = textNodePos;
}
if (xpath.find('/', bodyContentStart) < terminalEnd) {
return false;
}
@@ -122,7 +127,7 @@ struct XPathStep {
static constexpr int MAX_XPATH_DEPTH = 16;
// Parse the XPath segment between /body/DocFragment[N]/body/ and text()[N].offset
// Parse the XPath segment between /body/DocFragment[N]/body/ and the terminal position
// into an ordered sequence of steps. Returns step count, 0 on failure.
// Example input: "/body/DocFragment[1]/body/div[1]/ul/li[4]/text()[1].51"
// Fills steps with: {div,1}, {ul,1}, {li,4}
@@ -136,13 +141,20 @@ int parseXPathSteps(const std::string& xpath, XPathStep steps[MAX_XPATH_DEPTH])
if (xpath.compare(afterBracket + 1, strlen(kBody), kBody) != 0) return 0;
size_t pos = afterBracket + 1 + strlen(kBody);
const size_t textPos = xpath.rfind("/text()");
if (textPos == std::string::npos || textPos <= pos) return 0;
size_t stepsEnd = xpath.rfind("/text()");
if (stepsEnd == std::string::npos) {
stepsEnd = xpath.rfind('.');
if (stepsEnd == std::string::npos || stepsEnd <= pos || stepsEnd + 1 >= xpath.size()) return 0;
for (size_t i = stepsEnd + 1; i < xpath.size(); i++) {
if (xpath[i] < '0' || xpath[i] > '9') return 0;
}
}
if (stepsEnd <= pos) return 0;
int count = 0;
while (pos < textPos && count < MAX_XPATH_DEPTH) {
while (pos < stepsEnd && count < MAX_XPATH_DEPTH) {
const size_t slash = xpath.find('/', pos);
const size_t segEnd = (slash < textPos) ? slash : textPos;
const size_t segEnd = (slash < stepsEnd) ? slash : stepsEnd;
XPathStep& step = steps[count];
const size_t bracket = xpath.find('[', pos);
@@ -166,7 +178,7 @@ int parseXPathSteps(const std::string& xpath, XPathStep steps[MAX_XPATH_DEPTH])
}
count++;
pos = (slash < textPos) ? slash + 1 : textPos;
pos = (slash < stepsEnd) ? slash + 1 : stepsEnd;
}
return count;
}
@@ -223,10 +235,148 @@ class ParagraphStreamer final : public Print {
char capturedAnchorId[MAX_ANCHOR_ID] = {};
int capturedAnchorIdLen = 0;
bool capturingAnchorTag = false;
enum IdScanState { ID_SCAN, ID_I, ID_D, ID_EQ, ID_IN_VALUE_D, ID_IN_VALUE_S } idState = ID_SCAN;
enum AnchorAttrState {
ATTR_FIND_NAME,
ATTR_READ_NAME,
ATTR_AFTER_NAME,
ATTR_BEFORE_VALUE,
ATTR_CAPTURE_D,
ATTR_CAPTURE_S
} attrState = ATTR_FIND_NAME;
uint8_t attrNameLen = 0;
bool currentAttrIsId = false;
bool inAttrQuote =
false; // true while inside a quoted attribute value (prevents '/' from being treated as self-close)
char attrQuoteChar = 0;
uint8_t nonVisibleDepth = 0;
bool isNonVisibleTag() const {
return strcasecmp(tagName, "head") == 0 || strcasecmp(tagName, "style") == 0 ||
strcasecmp(tagName, "script") == 0 || strcasecmp(tagName, "title") == 0;
}
static bool isAttrWhitespace(uint8_t c) { return c == ' ' || c == '\t' || c == '\n' || c == '\r'; }
static bool isAttrNameChar(uint8_t c) {
return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '_' || c == '-' ||
c == ':' || c == '.';
}
void resetAnchorAttrScan() {
attrState = ATTR_FIND_NAME;
attrNameLen = 0;
currentAttrIsId = false;
}
void finishCapturedAnchorId() {
capturedAnchorId[capturedAnchorIdLen] = '\0';
capturingAnchorTag = false;
resetAnchorAttrScan();
}
void beginAnchorIdScan() {
capturingAnchorTag = true;
resetAnchorAttrScan();
}
void endAnchorIdScan() {
if (capturingAnchorTag) {
capturedAnchorIdLen = 0;
}
capturingAnchorTag = false;
resetAnchorAttrScan();
}
void appendCapturedAnchorId(uint8_t c) {
if (capturedAnchorIdLen + 1 < MAX_ANCHOR_ID) {
capturedAnchorId[capturedAnchorIdLen++] = c;
}
}
void scanAnchorAttribute(uint8_t c) {
switch (attrState) {
case ATTR_FIND_NAME:
if (isAttrNameChar(c)) {
attrState = ATTR_READ_NAME;
attrNameLen = 1;
currentAttrIsId = c == 'i';
}
break;
case ATTR_READ_NAME:
if (isAttrNameChar(c)) {
if (attrNameLen == 1) {
currentAttrIsId = currentAttrIsId && c == 'd';
} else {
currentAttrIsId = false;
}
attrNameLen++;
} else {
currentAttrIsId = currentAttrIsId && attrNameLen == 2;
if (isAttrWhitespace(c)) {
attrState = ATTR_AFTER_NAME;
} else if (c == '=') {
attrState = ATTR_BEFORE_VALUE;
} else {
resetAnchorAttrScan();
}
}
break;
case ATTR_AFTER_NAME:
if (isAttrWhitespace(c)) {
break;
}
if (c == '=') {
attrState = ATTR_BEFORE_VALUE;
} else if (isAttrNameChar(c)) {
attrState = ATTR_READ_NAME;
attrNameLen = 1;
currentAttrIsId = c == 'i';
} else {
resetAnchorAttrScan();
}
break;
case ATTR_BEFORE_VALUE:
if (isAttrWhitespace(c)) {
break;
}
if (currentAttrIsId && c == '"') {
capturedAnchorIdLen = 0;
attrState = ATTR_CAPTURE_D;
} else if (currentAttrIsId && c == '\'') {
capturedAnchorIdLen = 0;
attrState = ATTR_CAPTURE_S;
} else if (c == '"') {
attrState = ATTR_CAPTURE_D;
} else if (c == '\'') {
attrState = ATTR_CAPTURE_S;
} else {
resetAnchorAttrScan();
}
break;
case ATTR_CAPTURE_D:
if (c == '"') {
if (currentAttrIsId) {
finishCapturedAnchorId();
} else {
resetAnchorAttrScan();
}
} else if (currentAttrIsId) {
appendCapturedAnchorId(c);
}
break;
case ATTR_CAPTURE_S:
if (c == '\'') {
if (currentAttrIsId) {
finishCapturedAnchorId();
} else {
resetAnchorAttrScan();
}
} else if (currentAttrIsId) {
appendCapturedAnchorId(c);
}
break;
}
}
void onVisibleCodepoint() {
totalVisChars++;
@@ -286,15 +436,19 @@ class ParagraphStreamer final : public Print {
void onOpenTag() {
htmlDepth++;
if (nonVisibleDepth > 0 || isNonVisibleTag()) {
nonVisibleDepth++;
return;
}
if (stepCount == 0) {
if (strcasecmp(tagName, "p") == 0) onLegacyP();
return;
}
// Capture <a id> inside the fully-matched element even after target char is found
// Capture a child <a id> inside the fully-matched element even after target char is found.
if (revPFound && matchedDepth == stepCount && capturedAnchorIdLen == 0 && strcasecmp(tagName, "a") == 0) {
capturingAnchorTag = true;
idState = ID_SCAN;
beginAnchorIdScan();
}
if (revDone) return;
@@ -315,6 +469,7 @@ class ParagraphStreamer final : public Print {
stepEnteredAtDepth[matchedDepth] = htmlDepth;
matchedDepth++;
if (matchedDepth == stepCount) {
beginAnchorIdScan();
paragraphAtMatch = pCount;
liCountAtMatch = liCount;
revPFound = true;
@@ -332,6 +487,12 @@ class ParagraphStreamer final : public Print {
}
void onCloseTag() {
if (nonVisibleDepth > 0) {
nonVisibleDepth--;
if (htmlDepth > 0) htmlDepth--;
return;
}
// Legacy mode: each direct child element closing advances the text node index.
if (stepCount == 0 && revPFound && !revDone && paragraphHtmlDepth >= 0 && htmlDepth == paragraphHtmlDepth + 1) {
currentTextNode++;
@@ -419,42 +580,12 @@ class ParagraphStreamer final : public Print {
attrQuoteChar = 0;
}
if (capturingAnchorTag) {
switch (idState) {
case ID_SCAN:
idState = (c == 'i' || c == 'I') ? ID_I : ID_SCAN;
break;
case ID_I:
idState = (c == 'd' || c == 'D') ? ID_D : ID_SCAN;
break;
case ID_D:
idState = (c == '=') ? ID_EQ : ID_SCAN;
break;
case ID_EQ:
if (c == '"')
idState = ID_IN_VALUE_D;
else if (c == '\'')
idState = ID_IN_VALUE_S;
break;
case ID_IN_VALUE_D:
if (c == '"') {
capturedAnchorId[capturedAnchorIdLen] = '\0';
capturingAnchorTag = false;
} else if (capturedAnchorIdLen + 1 < MAX_ANCHOR_ID)
capturedAnchorId[capturedAnchorIdLen++] = c;
break;
case ID_IN_VALUE_S:
if (c == '\'') {
capturedAnchorId[capturedAnchorIdLen] = '\0';
capturingAnchorTag = false;
} else if (capturedAnchorIdLen + 1 < MAX_ANCHOR_ID)
capturedAnchorId[capturedAnchorIdLen++] = c;
break;
}
scanAnchorAttribute(c);
}
// Only treat '/' as self-closing when outside a quoted attribute value.
if (c == '/' && !inAttrQuote) {
endAnchorIdScan();
onCloseTag();
capturingAnchorTag = false;
}
break;
}
@@ -512,10 +643,13 @@ class ParagraphStreamer final : public Print {
tagNameLen = 0;
tagIsClose = false;
capturingAnchorTag = false;
idState = ID_SCAN;
resetAnchorAttrScan();
inAttrQuote = false;
attrQuoteChar = 0;
} else if (c == '>') {
if (tagState == TAG_ATTRS) {
endAnchorIdScan();
}
globalInTag = false;
inAttrQuote = false;
if (tagState == TAG_IN_NAME && tagNameLen > 0) {
@@ -529,6 +663,9 @@ class ParagraphStreamer final : public Print {
tagState = TAG_IDLE;
} else if (globalInTag) {
processByteInTag(c);
} else if (nonVisibleDepth > 0) {
// Ignore head/style/script/title text. KOReader XPaths are body-relative, and CSS text
// should not contribute to intra-spine progress.
} else {
if (c == '&') {
globalInEntity = true;
@@ -762,4 +899,4 @@ std::string ProgressMapper::generateXPath(const std::shared_ptr<Epub>& epub, int
const int p = s.paragraphCount();
return (p > 0) ? base + "/p[" + std::to_string(p) + "]" : base;
}
}
+68
View File
@@ -1,5 +1,73 @@
#include "Utf8.h"
#include "Utf8ComposeTable.h"
namespace {
// Look up the canonical composition of (base + combining mark), or 0 if none.
uint32_t utf8ComposePair(const uint32_t base, const uint32_t mark) {
if (base > 0xFFFF || mark > 0xFFFF) return 0;
int lo = 0;
int hi = kUtf8ComposeTableSize - 1;
while (lo <= hi) {
const int mid = (lo + hi) / 2;
const Utf8ComposeEntry& e = kUtf8ComposeTable[mid];
if (e.base < base || (e.base == base && e.mark < mark)) {
lo = mid + 1;
} else if (e.base > base || (e.base == base && e.mark > mark)) {
hi = mid - 1;
} else {
return e.composed;
}
}
return 0;
}
} // namespace
std::string utf8ComposeNfc(const std::string& in) {
// Fast path: NFC composition can only change text that contains a combining
// diacritical mark U+0300-036F (UTF-8 lead byte 0xCC or 0xCD). Plain ASCII and
// already-precomposed (NFC) text -- the vast majority of words -- have none, so
// return them untouched without walking codepoints or allocating. A 0xCD that is
// actually a non-combining codepoint just falls through to the full pass below.
bool maybeHasMarks = false;
for (const unsigned char c : in) {
if (c == 0xCC || c == 0xCD) {
maybeHasMarks = true;
break;
}
}
if (!maybeHasMarks) return in;
std::string out;
out.reserve(in.size());
const unsigned char* p = reinterpret_cast<const unsigned char*>(in.c_str());
uint32_t base = 0;
bool haveBase = false;
while (*p) {
const uint32_t cp = utf8NextCodepoint(&p);
if (cp == 0) break;
if (utf8IsCombiningMark(cp)) {
const uint32_t composed = haveBase ? utf8ComposePair(base, cp) : 0;
if (composed) {
base = composed; // keep accumulating further marks onto the composed char
continue;
}
// No composition: flush the pending base, then emit the mark unchanged.
if (haveBase) {
utf8AppendCodepoint(base, out);
haveBase = false;
}
utf8AppendCodepoint(cp, out);
} else {
if (haveBase) utf8AppendCodepoint(base, out);
base = cp;
haveBase = true;
}
}
if (haveBase) utf8AppendCodepoint(base, out);
return out;
}
int utf8CodepointLen(const unsigned char c) {
if (c < 0x80) return 1; // 0xxxxxxx
if ((c >> 5) == 0x6) return 2; // 110xxxxx
+10 -1
View File
@@ -12,6 +12,12 @@ size_t utf8RemoveLastChar(std::string& str);
// Truncate string by removing N UTF-8 codepoints from the end.
void utf8TruncateChars(std::string& str, size_t numChars);
// Canonical composition (NFC) for the Latin / Vietnamese range: precomposes a
// base letter followed by combining diacritical mark(s) into a single codepoint.
// Needed because the device fonts have no combining-mark positioning, so text
// stored in NFD (e.g. some EPUB chapter titles) otherwise renders broken.
std::string utf8ComposeNfc(const std::string& in);
// Truncate a raw char buffer to the last complete UTF-8 codepoint boundary.
// Returns the new length (<= len). If the buffer ends mid-sequence, the
// incomplete trailing bytes are excluded.
@@ -21,12 +27,15 @@ int utf8SafeTruncateBuffer(const char* buf, int len);
// Covers CJK Unified Ideographs, Hiragana, Katakana, Hangul Syllables, CJK punctuation,
// and fullwidth forms — the ranges where word boundaries are implicit per character.
inline bool utf8IsCjkBreakable(const uint32_t cp) {
return (cp >= 0x3000 && cp <= 0x303F) // CJK Symbols and Punctuation
return (cp >= 0x1100 && cp <= 0x11FF) // Hangul Jamo
|| (cp >= 0x3000 && cp <= 0x303F) // CJK Symbols and Punctuation
|| (cp >= 0x3040 && cp <= 0x309F) // Hiragana
|| (cp >= 0x30A0 && cp <= 0x30FF) // Katakana
|| (cp >= 0x3130 && cp <= 0x318F) // Hangul Compatibility Jamo
|| (cp >= 0x3400 && cp <= 0x4DBF) // CJK Extension A
|| (cp >= 0x4E00 && cp <= 0x9FFF) // CJK Unified Ideographs
|| (cp >= 0xAC00 && cp <= 0xD7AF) // Hangul Syllables
|| (cp >= 0xD7B0 && cp <= 0xD7FF) // Hangul Jamo Extended-B
|| (cp >= 0xF900 && cp <= 0xFAFF) // CJK Compatibility Ideographs
|| (cp >= 0xFE30 && cp <= 0xFE4F) // CJK Compatibility Forms
|| (cp >= 0xFF01 && cp <= 0xFF60) // Fullwidth Latin / Punctuation
+229
View File
@@ -0,0 +1,229 @@
// Auto-generated canonical composition (NFC) table for the Latin / Vietnamese
// range (combining marks U+0300-U+036F). Generated from Python unicodedata.
// Used by utf8ComposeNfc() to precompose decomposed (NFD) text so the device
// fonts (which lack combining-mark positioning) render it correctly.
#pragma once
#include <cstdint>
struct Utf8ComposeEntry {
uint16_t base;
uint16_t mark;
uint16_t composed;
};
// Sorted by (base, mark) for binary search.
static constexpr Utf8ComposeEntry kUtf8ComposeTable[] = {
{0x0041, 0x0300, 0x00C0}, {0x0041, 0x0301, 0x00C1}, {0x0041, 0x0302, 0x00C2}, {0x0041, 0x0303, 0x00C3},
{0x0041, 0x0304, 0x0100}, {0x0041, 0x0306, 0x0102}, {0x0041, 0x0307, 0x0226}, {0x0041, 0x0308, 0x00C4},
{0x0041, 0x0309, 0x1EA2}, {0x0041, 0x030A, 0x00C5}, {0x0041, 0x030C, 0x01CD}, {0x0041, 0x030F, 0x0200},
{0x0041, 0x0311, 0x0202}, {0x0041, 0x0323, 0x1EA0}, {0x0041, 0x0325, 0x1E00}, {0x0041, 0x0328, 0x0104},
{0x0041, 0x0340, 0x00C0}, {0x0041, 0x0341, 0x00C1}, {0x0042, 0x0307, 0x1E02}, {0x0042, 0x0323, 0x1E04},
{0x0042, 0x0331, 0x1E06}, {0x0043, 0x0301, 0x0106}, {0x0043, 0x0302, 0x0108}, {0x0043, 0x0307, 0x010A},
{0x0043, 0x030C, 0x010C}, {0x0043, 0x0327, 0x00C7}, {0x0043, 0x0341, 0x0106}, {0x0044, 0x0307, 0x1E0A},
{0x0044, 0x030C, 0x010E}, {0x0044, 0x0323, 0x1E0C}, {0x0044, 0x0327, 0x1E10}, {0x0044, 0x032D, 0x1E12},
{0x0044, 0x0331, 0x1E0E}, {0x0045, 0x0300, 0x00C8}, {0x0045, 0x0301, 0x00C9}, {0x0045, 0x0302, 0x00CA},
{0x0045, 0x0303, 0x1EBC}, {0x0045, 0x0304, 0x0112}, {0x0045, 0x0306, 0x0114}, {0x0045, 0x0307, 0x0116},
{0x0045, 0x0308, 0x00CB}, {0x0045, 0x0309, 0x1EBA}, {0x0045, 0x030C, 0x011A}, {0x0045, 0x030F, 0x0204},
{0x0045, 0x0311, 0x0206}, {0x0045, 0x0323, 0x1EB8}, {0x0045, 0x0327, 0x0228}, {0x0045, 0x0328, 0x0118},
{0x0045, 0x032D, 0x1E18}, {0x0045, 0x0330, 0x1E1A}, {0x0045, 0x0340, 0x00C8}, {0x0045, 0x0341, 0x00C9},
{0x0046, 0x0307, 0x1E1E}, {0x0047, 0x0301, 0x01F4}, {0x0047, 0x0302, 0x011C}, {0x0047, 0x0304, 0x1E20},
{0x0047, 0x0306, 0x011E}, {0x0047, 0x0307, 0x0120}, {0x0047, 0x030C, 0x01E6}, {0x0047, 0x0327, 0x0122},
{0x0047, 0x0341, 0x01F4}, {0x0048, 0x0302, 0x0124}, {0x0048, 0x0307, 0x1E22}, {0x0048, 0x0308, 0x1E26},
{0x0048, 0x030C, 0x021E}, {0x0048, 0x0323, 0x1E24}, {0x0048, 0x0327, 0x1E28}, {0x0048, 0x032E, 0x1E2A},
{0x0049, 0x0300, 0x00CC}, {0x0049, 0x0301, 0x00CD}, {0x0049, 0x0302, 0x00CE}, {0x0049, 0x0303, 0x0128},
{0x0049, 0x0304, 0x012A}, {0x0049, 0x0306, 0x012C}, {0x0049, 0x0307, 0x0130}, {0x0049, 0x0308, 0x00CF},
{0x0049, 0x0309, 0x1EC8}, {0x0049, 0x030C, 0x01CF}, {0x0049, 0x030F, 0x0208}, {0x0049, 0x0311, 0x020A},
{0x0049, 0x0323, 0x1ECA}, {0x0049, 0x0328, 0x012E}, {0x0049, 0x0330, 0x1E2C}, {0x0049, 0x0340, 0x00CC},
{0x0049, 0x0341, 0x00CD}, {0x0049, 0x0344, 0x1E2E}, {0x004A, 0x0302, 0x0134}, {0x004B, 0x0301, 0x1E30},
{0x004B, 0x030C, 0x01E8}, {0x004B, 0x0323, 0x1E32}, {0x004B, 0x0327, 0x0136}, {0x004B, 0x0331, 0x1E34},
{0x004B, 0x0341, 0x1E30}, {0x004C, 0x0301, 0x0139}, {0x004C, 0x030C, 0x013D}, {0x004C, 0x0323, 0x1E36},
{0x004C, 0x0327, 0x013B}, {0x004C, 0x032D, 0x1E3C}, {0x004C, 0x0331, 0x1E3A}, {0x004C, 0x0341, 0x0139},
{0x004D, 0x0301, 0x1E3E}, {0x004D, 0x0307, 0x1E40}, {0x004D, 0x0323, 0x1E42}, {0x004D, 0x0341, 0x1E3E},
{0x004E, 0x0300, 0x01F8}, {0x004E, 0x0301, 0x0143}, {0x004E, 0x0303, 0x00D1}, {0x004E, 0x0307, 0x1E44},
{0x004E, 0x030C, 0x0147}, {0x004E, 0x0323, 0x1E46}, {0x004E, 0x0327, 0x0145}, {0x004E, 0x032D, 0x1E4A},
{0x004E, 0x0331, 0x1E48}, {0x004E, 0x0340, 0x01F8}, {0x004E, 0x0341, 0x0143}, {0x004F, 0x0300, 0x00D2},
{0x004F, 0x0301, 0x00D3}, {0x004F, 0x0302, 0x00D4}, {0x004F, 0x0303, 0x00D5}, {0x004F, 0x0304, 0x014C},
{0x004F, 0x0306, 0x014E}, {0x004F, 0x0307, 0x022E}, {0x004F, 0x0308, 0x00D6}, {0x004F, 0x0309, 0x1ECE},
{0x004F, 0x030B, 0x0150}, {0x004F, 0x030C, 0x01D1}, {0x004F, 0x030F, 0x020C}, {0x004F, 0x0311, 0x020E},
{0x004F, 0x031B, 0x01A0}, {0x004F, 0x0323, 0x1ECC}, {0x004F, 0x0328, 0x01EA}, {0x004F, 0x0340, 0x00D2},
{0x004F, 0x0341, 0x00D3}, {0x0050, 0x0301, 0x1E54}, {0x0050, 0x0307, 0x1E56}, {0x0050, 0x0341, 0x1E54},
{0x0052, 0x0301, 0x0154}, {0x0052, 0x0307, 0x1E58}, {0x0052, 0x030C, 0x0158}, {0x0052, 0x030F, 0x0210},
{0x0052, 0x0311, 0x0212}, {0x0052, 0x0323, 0x1E5A}, {0x0052, 0x0327, 0x0156}, {0x0052, 0x0331, 0x1E5E},
{0x0052, 0x0341, 0x0154}, {0x0053, 0x0301, 0x015A}, {0x0053, 0x0302, 0x015C}, {0x0053, 0x0307, 0x1E60},
{0x0053, 0x030C, 0x0160}, {0x0053, 0x0323, 0x1E62}, {0x0053, 0x0326, 0x0218}, {0x0053, 0x0327, 0x015E},
{0x0053, 0x0341, 0x015A}, {0x0054, 0x0307, 0x1E6A}, {0x0054, 0x030C, 0x0164}, {0x0054, 0x0323, 0x1E6C},
{0x0054, 0x0326, 0x021A}, {0x0054, 0x0327, 0x0162}, {0x0054, 0x032D, 0x1E70}, {0x0054, 0x0331, 0x1E6E},
{0x0055, 0x0300, 0x00D9}, {0x0055, 0x0301, 0x00DA}, {0x0055, 0x0302, 0x00DB}, {0x0055, 0x0303, 0x0168},
{0x0055, 0x0304, 0x016A}, {0x0055, 0x0306, 0x016C}, {0x0055, 0x0308, 0x00DC}, {0x0055, 0x0309, 0x1EE6},
{0x0055, 0x030A, 0x016E}, {0x0055, 0x030B, 0x0170}, {0x0055, 0x030C, 0x01D3}, {0x0055, 0x030F, 0x0214},
{0x0055, 0x0311, 0x0216}, {0x0055, 0x031B, 0x01AF}, {0x0055, 0x0323, 0x1EE4}, {0x0055, 0x0324, 0x1E72},
{0x0055, 0x0328, 0x0172}, {0x0055, 0x032D, 0x1E76}, {0x0055, 0x0330, 0x1E74}, {0x0055, 0x0340, 0x00D9},
{0x0055, 0x0341, 0x00DA}, {0x0055, 0x0344, 0x01D7}, {0x0056, 0x0303, 0x1E7C}, {0x0056, 0x0323, 0x1E7E},
{0x0057, 0x0300, 0x1E80}, {0x0057, 0x0301, 0x1E82}, {0x0057, 0x0302, 0x0174}, {0x0057, 0x0307, 0x1E86},
{0x0057, 0x0308, 0x1E84}, {0x0057, 0x0323, 0x1E88}, {0x0057, 0x0340, 0x1E80}, {0x0057, 0x0341, 0x1E82},
{0x0058, 0x0307, 0x1E8A}, {0x0058, 0x0308, 0x1E8C}, {0x0059, 0x0300, 0x1EF2}, {0x0059, 0x0301, 0x00DD},
{0x0059, 0x0302, 0x0176}, {0x0059, 0x0303, 0x1EF8}, {0x0059, 0x0304, 0x0232}, {0x0059, 0x0307, 0x1E8E},
{0x0059, 0x0308, 0x0178}, {0x0059, 0x0309, 0x1EF6}, {0x0059, 0x0323, 0x1EF4}, {0x0059, 0x0340, 0x1EF2},
{0x0059, 0x0341, 0x00DD}, {0x005A, 0x0301, 0x0179}, {0x005A, 0x0302, 0x1E90}, {0x005A, 0x0307, 0x017B},
{0x005A, 0x030C, 0x017D}, {0x005A, 0x0323, 0x1E92}, {0x005A, 0x0331, 0x1E94}, {0x005A, 0x0341, 0x0179},
{0x0061, 0x0300, 0x00E0}, {0x0061, 0x0301, 0x00E1}, {0x0061, 0x0302, 0x00E2}, {0x0061, 0x0303, 0x00E3},
{0x0061, 0x0304, 0x0101}, {0x0061, 0x0306, 0x0103}, {0x0061, 0x0307, 0x0227}, {0x0061, 0x0308, 0x00E4},
{0x0061, 0x0309, 0x1EA3}, {0x0061, 0x030A, 0x00E5}, {0x0061, 0x030C, 0x01CE}, {0x0061, 0x030F, 0x0201},
{0x0061, 0x0311, 0x0203}, {0x0061, 0x0323, 0x1EA1}, {0x0061, 0x0325, 0x1E01}, {0x0061, 0x0328, 0x0105},
{0x0061, 0x0340, 0x00E0}, {0x0061, 0x0341, 0x00E1}, {0x0062, 0x0307, 0x1E03}, {0x0062, 0x0323, 0x1E05},
{0x0062, 0x0331, 0x1E07}, {0x0063, 0x0301, 0x0107}, {0x0063, 0x0302, 0x0109}, {0x0063, 0x0307, 0x010B},
{0x0063, 0x030C, 0x010D}, {0x0063, 0x0327, 0x00E7}, {0x0063, 0x0341, 0x0107}, {0x0064, 0x0307, 0x1E0B},
{0x0064, 0x030C, 0x010F}, {0x0064, 0x0323, 0x1E0D}, {0x0064, 0x0327, 0x1E11}, {0x0064, 0x032D, 0x1E13},
{0x0064, 0x0331, 0x1E0F}, {0x0065, 0x0300, 0x00E8}, {0x0065, 0x0301, 0x00E9}, {0x0065, 0x0302, 0x00EA},
{0x0065, 0x0303, 0x1EBD}, {0x0065, 0x0304, 0x0113}, {0x0065, 0x0306, 0x0115}, {0x0065, 0x0307, 0x0117},
{0x0065, 0x0308, 0x00EB}, {0x0065, 0x0309, 0x1EBB}, {0x0065, 0x030C, 0x011B}, {0x0065, 0x030F, 0x0205},
{0x0065, 0x0311, 0x0207}, {0x0065, 0x0323, 0x1EB9}, {0x0065, 0x0327, 0x0229}, {0x0065, 0x0328, 0x0119},
{0x0065, 0x032D, 0x1E19}, {0x0065, 0x0330, 0x1E1B}, {0x0065, 0x0340, 0x00E8}, {0x0065, 0x0341, 0x00E9},
{0x0066, 0x0307, 0x1E1F}, {0x0067, 0x0301, 0x01F5}, {0x0067, 0x0302, 0x011D}, {0x0067, 0x0304, 0x1E21},
{0x0067, 0x0306, 0x011F}, {0x0067, 0x0307, 0x0121}, {0x0067, 0x030C, 0x01E7}, {0x0067, 0x0327, 0x0123},
{0x0067, 0x0341, 0x01F5}, {0x0068, 0x0302, 0x0125}, {0x0068, 0x0307, 0x1E23}, {0x0068, 0x0308, 0x1E27},
{0x0068, 0x030C, 0x021F}, {0x0068, 0x0323, 0x1E25}, {0x0068, 0x0327, 0x1E29}, {0x0068, 0x032E, 0x1E2B},
{0x0068, 0x0331, 0x1E96}, {0x0069, 0x0300, 0x00EC}, {0x0069, 0x0301, 0x00ED}, {0x0069, 0x0302, 0x00EE},
{0x0069, 0x0303, 0x0129}, {0x0069, 0x0304, 0x012B}, {0x0069, 0x0306, 0x012D}, {0x0069, 0x0308, 0x00EF},
{0x0069, 0x0309, 0x1EC9}, {0x0069, 0x030C, 0x01D0}, {0x0069, 0x030F, 0x0209}, {0x0069, 0x0311, 0x020B},
{0x0069, 0x0323, 0x1ECB}, {0x0069, 0x0328, 0x012F}, {0x0069, 0x0330, 0x1E2D}, {0x0069, 0x0340, 0x00EC},
{0x0069, 0x0341, 0x00ED}, {0x0069, 0x0344, 0x1E2F}, {0x006A, 0x0302, 0x0135}, {0x006A, 0x030C, 0x01F0},
{0x006B, 0x0301, 0x1E31}, {0x006B, 0x030C, 0x01E9}, {0x006B, 0x0323, 0x1E33}, {0x006B, 0x0327, 0x0137},
{0x006B, 0x0331, 0x1E35}, {0x006B, 0x0341, 0x1E31}, {0x006C, 0x0301, 0x013A}, {0x006C, 0x030C, 0x013E},
{0x006C, 0x0323, 0x1E37}, {0x006C, 0x0327, 0x013C}, {0x006C, 0x032D, 0x1E3D}, {0x006C, 0x0331, 0x1E3B},
{0x006C, 0x0341, 0x013A}, {0x006D, 0x0301, 0x1E3F}, {0x006D, 0x0307, 0x1E41}, {0x006D, 0x0323, 0x1E43},
{0x006D, 0x0341, 0x1E3F}, {0x006E, 0x0300, 0x01F9}, {0x006E, 0x0301, 0x0144}, {0x006E, 0x0303, 0x00F1},
{0x006E, 0x0307, 0x1E45}, {0x006E, 0x030C, 0x0148}, {0x006E, 0x0323, 0x1E47}, {0x006E, 0x0327, 0x0146},
{0x006E, 0x032D, 0x1E4B}, {0x006E, 0x0331, 0x1E49}, {0x006E, 0x0340, 0x01F9}, {0x006E, 0x0341, 0x0144},
{0x006F, 0x0300, 0x00F2}, {0x006F, 0x0301, 0x00F3}, {0x006F, 0x0302, 0x00F4}, {0x006F, 0x0303, 0x00F5},
{0x006F, 0x0304, 0x014D}, {0x006F, 0x0306, 0x014F}, {0x006F, 0x0307, 0x022F}, {0x006F, 0x0308, 0x00F6},
{0x006F, 0x0309, 0x1ECF}, {0x006F, 0x030B, 0x0151}, {0x006F, 0x030C, 0x01D2}, {0x006F, 0x030F, 0x020D},
{0x006F, 0x0311, 0x020F}, {0x006F, 0x031B, 0x01A1}, {0x006F, 0x0323, 0x1ECD}, {0x006F, 0x0328, 0x01EB},
{0x006F, 0x0340, 0x00F2}, {0x006F, 0x0341, 0x00F3}, {0x0070, 0x0301, 0x1E55}, {0x0070, 0x0307, 0x1E57},
{0x0070, 0x0341, 0x1E55}, {0x0072, 0x0301, 0x0155}, {0x0072, 0x0307, 0x1E59}, {0x0072, 0x030C, 0x0159},
{0x0072, 0x030F, 0x0211}, {0x0072, 0x0311, 0x0213}, {0x0072, 0x0323, 0x1E5B}, {0x0072, 0x0327, 0x0157},
{0x0072, 0x0331, 0x1E5F}, {0x0072, 0x0341, 0x0155}, {0x0073, 0x0301, 0x015B}, {0x0073, 0x0302, 0x015D},
{0x0073, 0x0307, 0x1E61}, {0x0073, 0x030C, 0x0161}, {0x0073, 0x0323, 0x1E63}, {0x0073, 0x0326, 0x0219},
{0x0073, 0x0327, 0x015F}, {0x0073, 0x0341, 0x015B}, {0x0074, 0x0307, 0x1E6B}, {0x0074, 0x0308, 0x1E97},
{0x0074, 0x030C, 0x0165}, {0x0074, 0x0323, 0x1E6D}, {0x0074, 0x0326, 0x021B}, {0x0074, 0x0327, 0x0163},
{0x0074, 0x032D, 0x1E71}, {0x0074, 0x0331, 0x1E6F}, {0x0075, 0x0300, 0x00F9}, {0x0075, 0x0301, 0x00FA},
{0x0075, 0x0302, 0x00FB}, {0x0075, 0x0303, 0x0169}, {0x0075, 0x0304, 0x016B}, {0x0075, 0x0306, 0x016D},
{0x0075, 0x0308, 0x00FC}, {0x0075, 0x0309, 0x1EE7}, {0x0075, 0x030A, 0x016F}, {0x0075, 0x030B, 0x0171},
{0x0075, 0x030C, 0x01D4}, {0x0075, 0x030F, 0x0215}, {0x0075, 0x0311, 0x0217}, {0x0075, 0x031B, 0x01B0},
{0x0075, 0x0323, 0x1EE5}, {0x0075, 0x0324, 0x1E73}, {0x0075, 0x0328, 0x0173}, {0x0075, 0x032D, 0x1E77},
{0x0075, 0x0330, 0x1E75}, {0x0075, 0x0340, 0x00F9}, {0x0075, 0x0341, 0x00FA}, {0x0075, 0x0344, 0x01D8},
{0x0076, 0x0303, 0x1E7D}, {0x0076, 0x0323, 0x1E7F}, {0x0077, 0x0300, 0x1E81}, {0x0077, 0x0301, 0x1E83},
{0x0077, 0x0302, 0x0175}, {0x0077, 0x0307, 0x1E87}, {0x0077, 0x0308, 0x1E85}, {0x0077, 0x030A, 0x1E98},
{0x0077, 0x0323, 0x1E89}, {0x0077, 0x0340, 0x1E81}, {0x0077, 0x0341, 0x1E83}, {0x0078, 0x0307, 0x1E8B},
{0x0078, 0x0308, 0x1E8D}, {0x0079, 0x0300, 0x1EF3}, {0x0079, 0x0301, 0x00FD}, {0x0079, 0x0302, 0x0177},
{0x0079, 0x0303, 0x1EF9}, {0x0079, 0x0304, 0x0233}, {0x0079, 0x0307, 0x1E8F}, {0x0079, 0x0308, 0x00FF},
{0x0079, 0x0309, 0x1EF7}, {0x0079, 0x030A, 0x1E99}, {0x0079, 0x0323, 0x1EF5}, {0x0079, 0x0340, 0x1EF3},
{0x0079, 0x0341, 0x00FD}, {0x007A, 0x0301, 0x017A}, {0x007A, 0x0302, 0x1E91}, {0x007A, 0x0307, 0x017C},
{0x007A, 0x030C, 0x017E}, {0x007A, 0x0323, 0x1E93}, {0x007A, 0x0331, 0x1E95}, {0x007A, 0x0341, 0x017A},
{0x00A8, 0x0300, 0x1FED}, {0x00A8, 0x0301, 0x0385}, {0x00A8, 0x0340, 0x1FED}, {0x00A8, 0x0341, 0x0385},
{0x00A8, 0x0342, 0x1FC1}, {0x00C2, 0x0300, 0x1EA6}, {0x00C2, 0x0301, 0x1EA4}, {0x00C2, 0x0303, 0x1EAA},
{0x00C2, 0x0309, 0x1EA8}, {0x00C2, 0x0323, 0x1EAC}, {0x00C2, 0x0340, 0x1EA6}, {0x00C2, 0x0341, 0x1EA4},
{0x00C4, 0x0304, 0x01DE}, {0x00C5, 0x0301, 0x01FA}, {0x00C5, 0x0341, 0x01FA}, {0x00C6, 0x0301, 0x01FC},
{0x00C6, 0x0304, 0x01E2}, {0x00C6, 0x0341, 0x01FC}, {0x00C7, 0x0301, 0x1E08}, {0x00C7, 0x0341, 0x1E08},
{0x00CA, 0x0300, 0x1EC0}, {0x00CA, 0x0301, 0x1EBE}, {0x00CA, 0x0303, 0x1EC4}, {0x00CA, 0x0309, 0x1EC2},
{0x00CA, 0x0323, 0x1EC6}, {0x00CA, 0x0340, 0x1EC0}, {0x00CA, 0x0341, 0x1EBE}, {0x00CF, 0x0301, 0x1E2E},
{0x00CF, 0x0341, 0x1E2E}, {0x00D2, 0x031B, 0x1EDC}, {0x00D3, 0x031B, 0x1EDA}, {0x00D4, 0x0300, 0x1ED2},
{0x00D4, 0x0301, 0x1ED0}, {0x00D4, 0x0303, 0x1ED6}, {0x00D4, 0x0309, 0x1ED4}, {0x00D4, 0x0323, 0x1ED8},
{0x00D4, 0x0340, 0x1ED2}, {0x00D4, 0x0341, 0x1ED0}, {0x00D5, 0x0301, 0x1E4C}, {0x00D5, 0x0304, 0x022C},
{0x00D5, 0x0308, 0x1E4E}, {0x00D5, 0x031B, 0x1EE0}, {0x00D5, 0x0341, 0x1E4C}, {0x00D6, 0x0304, 0x022A},
{0x00D8, 0x0301, 0x01FE}, {0x00D8, 0x0341, 0x01FE}, {0x00D9, 0x031B, 0x1EEA}, {0x00DA, 0x031B, 0x1EE8},
{0x00DC, 0x0300, 0x01DB}, {0x00DC, 0x0301, 0x01D7}, {0x00DC, 0x0304, 0x01D5}, {0x00DC, 0x030C, 0x01D9},
{0x00DC, 0x0340, 0x01DB}, {0x00DC, 0x0341, 0x01D7}, {0x00E2, 0x0300, 0x1EA7}, {0x00E2, 0x0301, 0x1EA5},
{0x00E2, 0x0303, 0x1EAB}, {0x00E2, 0x0309, 0x1EA9}, {0x00E2, 0x0323, 0x1EAD}, {0x00E2, 0x0340, 0x1EA7},
{0x00E2, 0x0341, 0x1EA5}, {0x00E4, 0x0304, 0x01DF}, {0x00E5, 0x0301, 0x01FB}, {0x00E5, 0x0341, 0x01FB},
{0x00E6, 0x0301, 0x01FD}, {0x00E6, 0x0304, 0x01E3}, {0x00E6, 0x0341, 0x01FD}, {0x00E7, 0x0301, 0x1E09},
{0x00E7, 0x0341, 0x1E09}, {0x00EA, 0x0300, 0x1EC1}, {0x00EA, 0x0301, 0x1EBF}, {0x00EA, 0x0303, 0x1EC5},
{0x00EA, 0x0309, 0x1EC3}, {0x00EA, 0x0323, 0x1EC7}, {0x00EA, 0x0340, 0x1EC1}, {0x00EA, 0x0341, 0x1EBF},
{0x00EF, 0x0301, 0x1E2F}, {0x00EF, 0x0341, 0x1E2F}, {0x00F2, 0x031B, 0x1EDD}, {0x00F3, 0x031B, 0x1EDB},
{0x00F4, 0x0300, 0x1ED3}, {0x00F4, 0x0301, 0x1ED1}, {0x00F4, 0x0303, 0x1ED7}, {0x00F4, 0x0309, 0x1ED5},
{0x00F4, 0x0323, 0x1ED9}, {0x00F4, 0x0340, 0x1ED3}, {0x00F4, 0x0341, 0x1ED1}, {0x00F5, 0x0301, 0x1E4D},
{0x00F5, 0x0304, 0x022D}, {0x00F5, 0x0308, 0x1E4F}, {0x00F5, 0x031B, 0x1EE1}, {0x00F5, 0x0341, 0x1E4D},
{0x00F6, 0x0304, 0x022B}, {0x00F8, 0x0301, 0x01FF}, {0x00F8, 0x0341, 0x01FF}, {0x00F9, 0x031B, 0x1EEB},
{0x00FA, 0x031B, 0x1EE9}, {0x00FC, 0x0300, 0x01DC}, {0x00FC, 0x0301, 0x01D8}, {0x00FC, 0x0304, 0x01D6},
{0x00FC, 0x030C, 0x01DA}, {0x00FC, 0x0340, 0x01DC}, {0x00FC, 0x0341, 0x01D8}, {0x0102, 0x0300, 0x1EB0},
{0x0102, 0x0301, 0x1EAE}, {0x0102, 0x0303, 0x1EB4}, {0x0102, 0x0309, 0x1EB2}, {0x0102, 0x0323, 0x1EB6},
{0x0102, 0x0340, 0x1EB0}, {0x0102, 0x0341, 0x1EAE}, {0x0103, 0x0300, 0x1EB1}, {0x0103, 0x0301, 0x1EAF},
{0x0103, 0x0303, 0x1EB5}, {0x0103, 0x0309, 0x1EB3}, {0x0103, 0x0323, 0x1EB7}, {0x0103, 0x0340, 0x1EB1},
{0x0103, 0x0341, 0x1EAF}, {0x0106, 0x0327, 0x1E08}, {0x0107, 0x0327, 0x1E09}, {0x0112, 0x0300, 0x1E14},
{0x0112, 0x0301, 0x1E16}, {0x0112, 0x0340, 0x1E14}, {0x0112, 0x0341, 0x1E16}, {0x0113, 0x0300, 0x1E15},
{0x0113, 0x0301, 0x1E17}, {0x0113, 0x0340, 0x1E15}, {0x0113, 0x0341, 0x1E17}, {0x0114, 0x0327, 0x1E1C},
{0x0115, 0x0327, 0x1E1D}, {0x014C, 0x0300, 0x1E50}, {0x014C, 0x0301, 0x1E52}, {0x014C, 0x0328, 0x01EC},
{0x014C, 0x0340, 0x1E50}, {0x014C, 0x0341, 0x1E52}, {0x014D, 0x0300, 0x1E51}, {0x014D, 0x0301, 0x1E53},
{0x014D, 0x0328, 0x01ED}, {0x014D, 0x0340, 0x1E51}, {0x014D, 0x0341, 0x1E53}, {0x015A, 0x0307, 0x1E64},
{0x015B, 0x0307, 0x1E65}, {0x0160, 0x0307, 0x1E66}, {0x0161, 0x0307, 0x1E67}, {0x0168, 0x0301, 0x1E78},
{0x0168, 0x031B, 0x1EEE}, {0x0168, 0x0341, 0x1E78}, {0x0169, 0x0301, 0x1E79}, {0x0169, 0x031B, 0x1EEF},
{0x0169, 0x0341, 0x1E79}, {0x016A, 0x0308, 0x1E7A}, {0x016B, 0x0308, 0x1E7B}, {0x017F, 0x0307, 0x1E9B},
{0x01A0, 0x0300, 0x1EDC}, {0x01A0, 0x0301, 0x1EDA}, {0x01A0, 0x0303, 0x1EE0}, {0x01A0, 0x0309, 0x1EDE},
{0x01A0, 0x0323, 0x1EE2}, {0x01A0, 0x0340, 0x1EDC}, {0x01A0, 0x0341, 0x1EDA}, {0x01A1, 0x0300, 0x1EDD},
{0x01A1, 0x0301, 0x1EDB}, {0x01A1, 0x0303, 0x1EE1}, {0x01A1, 0x0309, 0x1EDF}, {0x01A1, 0x0323, 0x1EE3},
{0x01A1, 0x0340, 0x1EDD}, {0x01A1, 0x0341, 0x1EDB}, {0x01AF, 0x0300, 0x1EEA}, {0x01AF, 0x0301, 0x1EE8},
{0x01AF, 0x0303, 0x1EEE}, {0x01AF, 0x0309, 0x1EEC}, {0x01AF, 0x0323, 0x1EF0}, {0x01AF, 0x0340, 0x1EEA},
{0x01AF, 0x0341, 0x1EE8}, {0x01B0, 0x0300, 0x1EEB}, {0x01B0, 0x0301, 0x1EE9}, {0x01B0, 0x0303, 0x1EEF},
{0x01B0, 0x0309, 0x1EED}, {0x01B0, 0x0323, 0x1EF1}, {0x01B0, 0x0340, 0x1EEB}, {0x01B0, 0x0341, 0x1EE9},
{0x01B7, 0x030C, 0x01EE}, {0x01EA, 0x0304, 0x01EC}, {0x01EB, 0x0304, 0x01ED}, {0x0226, 0x0304, 0x01E0},
{0x0227, 0x0304, 0x01E1}, {0x0228, 0x0306, 0x1E1C}, {0x0229, 0x0306, 0x1E1D}, {0x022E, 0x0304, 0x0230},
{0x022F, 0x0304, 0x0231}, {0x0292, 0x030C, 0x01EF}, {0x0391, 0x0300, 0x1FBA}, {0x0391, 0x0301, 0x0386},
{0x0391, 0x0304, 0x1FB9}, {0x0391, 0x0306, 0x1FB8}, {0x0391, 0x0313, 0x1F08}, {0x0391, 0x0314, 0x1F09},
{0x0391, 0x0340, 0x1FBA}, {0x0391, 0x0341, 0x0386}, {0x0391, 0x0343, 0x1F08}, {0x0391, 0x0345, 0x1FBC},
{0x0395, 0x0300, 0x1FC8}, {0x0395, 0x0301, 0x0388}, {0x0395, 0x0313, 0x1F18}, {0x0395, 0x0314, 0x1F19},
{0x0395, 0x0340, 0x1FC8}, {0x0395, 0x0341, 0x0388}, {0x0395, 0x0343, 0x1F18}, {0x0397, 0x0300, 0x1FCA},
{0x0397, 0x0301, 0x0389}, {0x0397, 0x0313, 0x1F28}, {0x0397, 0x0314, 0x1F29}, {0x0397, 0x0340, 0x1FCA},
{0x0397, 0x0341, 0x0389}, {0x0397, 0x0343, 0x1F28}, {0x0397, 0x0345, 0x1FCC}, {0x0399, 0x0300, 0x1FDA},
{0x0399, 0x0301, 0x038A}, {0x0399, 0x0304, 0x1FD9}, {0x0399, 0x0306, 0x1FD8}, {0x0399, 0x0308, 0x03AA},
{0x0399, 0x0313, 0x1F38}, {0x0399, 0x0314, 0x1F39}, {0x0399, 0x0340, 0x1FDA}, {0x0399, 0x0341, 0x038A},
{0x0399, 0x0343, 0x1F38}, {0x039F, 0x0300, 0x1FF8}, {0x039F, 0x0301, 0x038C}, {0x039F, 0x0313, 0x1F48},
{0x039F, 0x0314, 0x1F49}, {0x039F, 0x0340, 0x1FF8}, {0x039F, 0x0341, 0x038C}, {0x039F, 0x0343, 0x1F48},
{0x03A1, 0x0314, 0x1FEC}, {0x03A5, 0x0300, 0x1FEA}, {0x03A5, 0x0301, 0x038E}, {0x03A5, 0x0304, 0x1FE9},
{0x03A5, 0x0306, 0x1FE8}, {0x03A5, 0x0308, 0x03AB}, {0x03A5, 0x0314, 0x1F59}, {0x03A5, 0x0340, 0x1FEA},
{0x03A5, 0x0341, 0x038E}, {0x03A9, 0x0300, 0x1FFA}, {0x03A9, 0x0301, 0x038F}, {0x03A9, 0x0313, 0x1F68},
{0x03A9, 0x0314, 0x1F69}, {0x03A9, 0x0340, 0x1FFA}, {0x03A9, 0x0341, 0x038F}, {0x03A9, 0x0343, 0x1F68},
{0x03A9, 0x0345, 0x1FFC}, {0x03AC, 0x0345, 0x1FB4}, {0x03AE, 0x0345, 0x1FC4}, {0x03B1, 0x0300, 0x1F70},
{0x03B1, 0x0301, 0x03AC}, {0x03B1, 0x0304, 0x1FB1}, {0x03B1, 0x0306, 0x1FB0}, {0x03B1, 0x0313, 0x1F00},
{0x03B1, 0x0314, 0x1F01}, {0x03B1, 0x0340, 0x1F70}, {0x03B1, 0x0341, 0x03AC}, {0x03B1, 0x0342, 0x1FB6},
{0x03B1, 0x0343, 0x1F00}, {0x03B1, 0x0345, 0x1FB3}, {0x03B5, 0x0300, 0x1F72}, {0x03B5, 0x0301, 0x03AD},
{0x03B5, 0x0313, 0x1F10}, {0x03B5, 0x0314, 0x1F11}, {0x03B5, 0x0340, 0x1F72}, {0x03B5, 0x0341, 0x03AD},
{0x03B5, 0x0343, 0x1F10}, {0x03B7, 0x0300, 0x1F74}, {0x03B7, 0x0301, 0x03AE}, {0x03B7, 0x0313, 0x1F20},
{0x03B7, 0x0314, 0x1F21}, {0x03B7, 0x0340, 0x1F74}, {0x03B7, 0x0341, 0x03AE}, {0x03B7, 0x0342, 0x1FC6},
{0x03B7, 0x0343, 0x1F20}, {0x03B7, 0x0345, 0x1FC3}, {0x03B9, 0x0300, 0x1F76}, {0x03B9, 0x0301, 0x03AF},
{0x03B9, 0x0304, 0x1FD1}, {0x03B9, 0x0306, 0x1FD0}, {0x03B9, 0x0308, 0x03CA}, {0x03B9, 0x0313, 0x1F30},
{0x03B9, 0x0314, 0x1F31}, {0x03B9, 0x0340, 0x1F76}, {0x03B9, 0x0341, 0x03AF}, {0x03B9, 0x0342, 0x1FD6},
{0x03B9, 0x0343, 0x1F30}, {0x03B9, 0x0344, 0x0390}, {0x03BF, 0x0300, 0x1F78}, {0x03BF, 0x0301, 0x03CC},
{0x03BF, 0x0313, 0x1F40}, {0x03BF, 0x0314, 0x1F41}, {0x03BF, 0x0340, 0x1F78}, {0x03BF, 0x0341, 0x03CC},
{0x03BF, 0x0343, 0x1F40}, {0x03C1, 0x0313, 0x1FE4}, {0x03C1, 0x0314, 0x1FE5}, {0x03C1, 0x0343, 0x1FE4},
{0x03C5, 0x0300, 0x1F7A}, {0x03C5, 0x0301, 0x03CD}, {0x03C5, 0x0304, 0x1FE1}, {0x03C5, 0x0306, 0x1FE0},
{0x03C5, 0x0308, 0x03CB}, {0x03C5, 0x0313, 0x1F50}, {0x03C5, 0x0314, 0x1F51}, {0x03C5, 0x0340, 0x1F7A},
{0x03C5, 0x0341, 0x03CD}, {0x03C5, 0x0342, 0x1FE6}, {0x03C5, 0x0343, 0x1F50}, {0x03C5, 0x0344, 0x03B0},
{0x03C9, 0x0300, 0x1F7C}, {0x03C9, 0x0301, 0x03CE}, {0x03C9, 0x0313, 0x1F60}, {0x03C9, 0x0314, 0x1F61},
{0x03C9, 0x0340, 0x1F7C}, {0x03C9, 0x0341, 0x03CE}, {0x03C9, 0x0342, 0x1FF6}, {0x03C9, 0x0343, 0x1F60},
{0x03C9, 0x0345, 0x1FF3}, {0x03CA, 0x0300, 0x1FD2}, {0x03CA, 0x0301, 0x0390}, {0x03CA, 0x0340, 0x1FD2},
{0x03CA, 0x0341, 0x0390}, {0x03CA, 0x0342, 0x1FD7}, {0x03CB, 0x0300, 0x1FE2}, {0x03CB, 0x0301, 0x03B0},
{0x03CB, 0x0340, 0x1FE2}, {0x03CB, 0x0341, 0x03B0}, {0x03CB, 0x0342, 0x1FE7}, {0x03CE, 0x0345, 0x1FF4},
{0x03D2, 0x0301, 0x03D3}, {0x03D2, 0x0308, 0x03D4}, {0x03D2, 0x0341, 0x03D3}, {0x0406, 0x0308, 0x0407},
{0x0410, 0x0306, 0x04D0}, {0x0410, 0x0308, 0x04D2}, {0x0413, 0x0301, 0x0403}, {0x0413, 0x0341, 0x0403},
{0x0415, 0x0300, 0x0400}, {0x0415, 0x0306, 0x04D6}, {0x0415, 0x0308, 0x0401}, {0x0415, 0x0340, 0x0400},
{0x0416, 0x0306, 0x04C1}, {0x0416, 0x0308, 0x04DC}, {0x0417, 0x0308, 0x04DE}, {0x0418, 0x0300, 0x040D},
{0x0418, 0x0304, 0x04E2}, {0x0418, 0x0306, 0x0419}, {0x0418, 0x0308, 0x04E4}, {0x0418, 0x0340, 0x040D},
{0x041A, 0x0301, 0x040C}, {0x041A, 0x0341, 0x040C}, {0x041E, 0x0308, 0x04E6}, {0x0423, 0x0304, 0x04EE},
{0x0423, 0x0306, 0x040E}, {0x0423, 0x0308, 0x04F0}, {0x0423, 0x030B, 0x04F2}, {0x0427, 0x0308, 0x04F4},
{0x042B, 0x0308, 0x04F8}, {0x042D, 0x0308, 0x04EC}, {0x0430, 0x0306, 0x04D1}, {0x0430, 0x0308, 0x04D3},
{0x0433, 0x0301, 0x0453}, {0x0433, 0x0341, 0x0453}, {0x0435, 0x0300, 0x0450}, {0x0435, 0x0306, 0x04D7},
{0x0435, 0x0308, 0x0451}, {0x0435, 0x0340, 0x0450}, {0x0436, 0x0306, 0x04C2}, {0x0436, 0x0308, 0x04DD},
{0x0437, 0x0308, 0x04DF}, {0x0438, 0x0300, 0x045D}, {0x0438, 0x0304, 0x04E3}, {0x0438, 0x0306, 0x0439},
{0x0438, 0x0308, 0x04E5}, {0x0438, 0x0340, 0x045D}, {0x043A, 0x0301, 0x045C}, {0x043A, 0x0341, 0x045C},
{0x043E, 0x0308, 0x04E7}, {0x0443, 0x0304, 0x04EF}, {0x0443, 0x0306, 0x045E}, {0x0443, 0x0308, 0x04F1},
{0x0443, 0x030B, 0x04F3}, {0x0447, 0x0308, 0x04F5}, {0x044B, 0x0308, 0x04F9}, {0x044D, 0x0308, 0x04ED},
{0x0456, 0x0308, 0x0457}, {0x0474, 0x030F, 0x0476}, {0x0475, 0x030F, 0x0477}, {0x04D8, 0x0308, 0x04DA},
{0x04D9, 0x0308, 0x04DB}, {0x04E8, 0x0308, 0x04EA}, {0x04E9, 0x0308, 0x04EB}, {0x1E36, 0x0304, 0x1E38},
{0x1E37, 0x0304, 0x1E39}, {0x1E5A, 0x0304, 0x1E5C}, {0x1E5B, 0x0304, 0x1E5D}, {0x1E60, 0x0323, 0x1E68},
{0x1E61, 0x0323, 0x1E69}, {0x1E62, 0x0307, 0x1E68}, {0x1E63, 0x0307, 0x1E69}, {0x1EA0, 0x0302, 0x1EAC},
{0x1EA0, 0x0306, 0x1EB6}, {0x1EA1, 0x0302, 0x1EAD}, {0x1EA1, 0x0306, 0x1EB7}, {0x1EB8, 0x0302, 0x1EC6},
{0x1EB9, 0x0302, 0x1EC7}, {0x1ECC, 0x0302, 0x1ED8}, {0x1ECC, 0x031B, 0x1EE2}, {0x1ECD, 0x0302, 0x1ED9},
{0x1ECD, 0x031B, 0x1EE3}, {0x1ECE, 0x031B, 0x1EDE}, {0x1ECF, 0x031B, 0x1EDF}, {0x1EE4, 0x031B, 0x1EF0},
{0x1EE5, 0x031B, 0x1EF1}, {0x1EE6, 0x031B, 0x1EEC}, {0x1EE7, 0x031B, 0x1EED},
};
static constexpr int kUtf8ComposeTableSize = sizeof(kUtf8ComposeTable) / sizeof(kUtf8ComposeTable[0]);
+15 -18
View File
@@ -397,37 +397,34 @@ uint8_t* ZipFile::readFileToMemory(const char* filename, size_t* size, const boo
// Continue out of block with data set
} else if (fileStat.method == ZIP_METHOD_DEFLATED) {
// Read out deflated content from file
const auto deflatedData = static_cast<uint8_t*>(malloc(deflatedDataSize));
if (deflatedData == nullptr) {
LOG_ERR("ZIP", "Failed to allocate memory for decompression buffer");
auto* fileReadBuffer = static_cast<uint8_t*>(malloc(1024));
if (!fileReadBuffer) {
LOG_ERR("ZIP", "Failed to allocate memory for zip file read buffer");
free(data);
return nullptr;
}
const size_t dataRead = file.read(deflatedData, deflatedDataSize);
ZipInflateCtx ctx;
ctx.file = &file;
ctx.fileRemaining = deflatedDataSize;
ctx.readBuf = fileReadBuffer;
ctx.readBufSize = 1024;
if (dataRead != deflatedDataSize) {
LOG_ERR("ZIP", "Failed to read data, expected %d got %d", deflatedDataSize, dataRead);
free(deflatedData);
if (!ctx.reader.init(true)) {
LOG_ERR("ZIP", "Failed to init inflate reader");
free(fileReadBuffer);
free(data);
return nullptr;
}
ctx.reader.setReadCallback(zipReadCallback);
bool success = false;
{
InflateReader r;
r.init(false);
r.setSource(deflatedData, deflatedDataSize);
success = r.read(data, inflatedDataSize);
}
free(deflatedData);
if (!success) {
if (!ctx.reader.read(data, inflatedDataSize)) {
LOG_ERR("ZIP", "Failed to inflate file");
free(fileReadBuffer);
free(data);
return nullptr;
}
free(fileReadBuffer);
// Continue out of block with data set
} else {