From 22f3575064536a8590c46d640557e2c5fa70311a Mon Sep 17 00:00:00 2001 From: Justin Mitchell Date: Wed, 17 Jun 2026 10:27:03 -0400 Subject: [PATCH] feat: Support for Korean line breaks and glyph spacing (#2288) Co-authored-by: Uri Tauber --- lib/EpdFont/EpdFont.cpp | 3 + lib/EpdFont/SdCardFont.cpp | 122 ++++++---- lib/EpdFont/SdCardFont.h | 14 +- lib/EpdFont/SdCardFontManager.cpp | 14 +- lib/EpdFont/SdCardFontManager.h | 10 +- lib/EpdFont/SdCardFontRegistry.cpp | 34 +++ lib/EpdFont/SdCardFontRegistry.h | 1 + lib/EpdFont/scripts/fontconvert.py | 2 +- lib/Epub/Epub/ParsedText.cpp | 273 ++++++++++++++++++++--- lib/Epub/Epub/ParsedText.h | 13 +- lib/GfxRenderer/GfxRenderer.cpp | 10 + lib/Utf8/Utf8.h | 5 +- lib/ZipFile/ZipFile.cpp | 33 ++- src/SdCardFontSystem.cpp | 10 +- src/activities/ActivityManager.cpp | 1 + src/activities/reader/ReaderActivity.cpp | 19 +- 16 files changed, 443 insertions(+), 121 deletions(-) diff --git a/lib/EpdFont/EpdFont.cpp b/lib/EpdFont/EpdFont.cpp index 599554b7..6055e845 100644 --- a/lib/EpdFont/EpdFont.cpp +++ b/lib/EpdFont/EpdFont.cpp @@ -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; } diff --git a/lib/EpdFont/SdCardFont.cpp b/lib/EpdFont/SdCardFont.cpp index 1c48f278..1609ab3a 100644 --- a/lib/EpdFont/SdCardFont.cpp +++ b/lib/EpdFont/SdCardFont.cpp @@ -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(s.fullIntervals), intervalsBytes) != static_cast(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(&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(iv.first), static_cast(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(iv.first), static_cast(iv.last)); - file.close(); + if (file.read(reinterpret_cast(&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(iv.first), static_cast(iv.last), + static_cast(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(s.fullIntervals), intervalsBytes) != static_cast(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(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(interval.offset + (codepoint - interval.first)); + const uint32_t offset = s.intervalsAreBmp16 ? s.bmpIntervals[mid].offset : s.fullIntervals[mid].offset; + return static_cast(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++) { diff --git a/lib/EpdFont/SdCardFont.h b/lib/EpdFont/SdCardFont.h index d4d680ed..443fee68 100644 --- a/lib/EpdFont/SdCardFont.h +++ b/lib/EpdFont/SdCardFont.h @@ -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 diff --git a/lib/EpdFont/SdCardFontManager.cpp b/lib/EpdFont/SdCardFontManager.cpp index a6032336..2d221386 100644 --- a/lib/EpdFont/SdCardFontManager.cpp +++ b/lib/EpdFont/SdCardFontManager.cpp @@ -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()); diff --git a/lib/EpdFont/SdCardFontManager.h b/lib/EpdFont/SdCardFontManager.h index aec07472..f9d2b3f2 100644 --- a/lib/EpdFont/SdCardFontManager.h +++ b/lib/EpdFont/SdCardFontManager.h @@ -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_; }; diff --git a/lib/EpdFont/SdCardFontRegistry.cpp b/lib/EpdFont/SdCardFontRegistry.cpp index fe01c62e..07966a85 100644 --- a/lib/EpdFont/SdCardFontRegistry.cpp +++ b/lib/EpdFont/SdCardFontRegistry.cpp @@ -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; diff --git a/lib/EpdFont/SdCardFontRegistry.h b/lib/EpdFont/SdCardFontRegistry.h index f96035ed..57220f4d 100644 --- a/lib/EpdFont/SdCardFontRegistry.h +++ b/lib/EpdFont/SdCardFontRegistry.h @@ -18,6 +18,7 @@ struct SdCardFontFamilyInfo { std::vector 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 availableSizes() const; }; diff --git a/lib/EpdFont/scripts/fontconvert.py b/lib/EpdFont/scripts/fontconvert.py index a55fab75..26df3b8a 100755 --- a/lib/EpdFont/scripts/fontconvert.py +++ b/lib/EpdFont/scripts/fontconvert.py @@ -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)) diff --git a/lib/Epub/Epub/ParsedText.cpp b/lib/Epub/Epub/ParsedText.cpp index b66b36ee..124a857d 100644 --- a/lib/Epub/Epub/ParsedText.cpp +++ b/lib/Epub/Epub/ParsedText.cpp @@ -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(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 cjkCharacterBreakByteOffsets(const std::string& text) { + struct CodepointBoundary { + uint32_t cp; + size_t endOffset; + }; + + std::vector codepoints; + codepoints.reserve(text.size()); + bool hasCjkBreakable = false; + + const auto* ptr = reinterpret_cast(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(ptr - start)}); + } + + if (!hasCjkBreakable || codepoints.size() < 2) return {}; + + std::vector 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(gapCount); +} + // Removes every soft hyphen in-place so rendered glyphs match measured widths. void stripSoftHyphensInPlace(std::string& word) { size_t pos = 0; @@ -132,12 +261,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 +337,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 +371,7 @@ void ParsedText::addWord(std::string word, const EpdFontFamily::Style fontStyle, words.emplace_back(segment); wordStyles.push_back(static_cast(baseStyle | EpdFontFamily::BOLD)); wordContinues.push_back(attach); + wordNoSpaceBefore.push_back(noSpaceBefore); wordIsFocusSuffix.push_back(false); } else { countPtr = reinterpret_cast(segment.data()); @@ -210,12 +384,14 @@ void ParsedText::addWord(std::string word, const EpdFontFamily::Style fontStyle, words.emplace_back(segment.substr(0, splitByteOffset)); wordStyles.push_back(static_cast(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 +419,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 +432,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(segmentStart), segmentLen); - processSegment(segment, inWordSegment, isFirstSegment ? attachToPrevious : true); + processSegment(segment, inWordSegment, isFirstSegment ? effectiveAttachToPrevious : true, + isFirstSegment ? effectiveNoSpaceBefore : false); if (wordStartsRtl) { hasRtlWord = true; } @@ -324,14 +502,16 @@ void ParsedText::layoutAndExtractLines(const GfxRenderer& renderer, const int fo std::vector 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 +520,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 +537,8 @@ std::vector ParsedText::calculateWordWidths(const GfxRenderer& rendere } std::vector ParsedText::computeLineBreaks(const GfxRenderer& renderer, const int fontId, const int pageWidth, - std::vector& wordWidths, std::vector& continuesVec) { + std::vector& wordWidths, std::vector& continuesVec, + std::vector& noSpaceBeforeVec) { if (words.empty()) { return {}; } @@ -395,7 +577,9 @@ std::vector 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(i) && !continuesVec[j]) { + if (j > static_cast(i) && noSpaceBeforeVec[j]) { + gap = 0; + } else if (j > static_cast(i) && !continuesVec[j]) { gap = renderer.getSpaceAdvance(fontId, lastCodepoint(words[j - 1]), firstCodepoint(words[j]), wordStyles[j - 1]); } else if (j > static_cast(i) && continuesVec[j]) { @@ -470,7 +654,8 @@ std::vector ParsedText::computeLineBreaks(const GfxRenderer& renderer, c // Builds break indices while opportunistically splitting the word that would overflow the current line. std::vector ParsedText::computeHyphenatedLineBreaks(const GfxRenderer& renderer, const int fontId, const int pageWidth, std::vector& wordWidths, - std::vector& continuesVec) { + std::vector& continuesVec, + std::vector& noSpaceBeforeVec) { const int firstLineIndent = resolveFirstLineIndent(true, renderer, fontId); std::vector lineBreakIndices; @@ -488,7 +673,9 @@ std::vector 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 +805,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(chosenWidth); @@ -627,7 +815,8 @@ bool ParsedText::hyphenateWordAtIndex(const size_t wordIndex, const int availabl } void ParsedText::extractLine(const size_t breakIndex, const int pageWidth, const std::vector& wordWidths, - const std::vector& continuesVec, const std::vector& lineBreakIndices, + const std::vector& continuesVec, const std::vector& noSpaceBeforeVec, + const std::vector& lineBreakIndices, const std::function)>& processLine, const GfxRenderer& renderer, const int fontId) { const size_t lineBreak = lineBreakIndices[breakIndex]; @@ -660,7 +849,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 +882,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(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 +902,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 +935,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 +943,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 +963,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(reorderedGapCount) - : 0; + const int reorderedJustifyExtra = (effectiveAlignment == CssTextAlign::Justify && !isLastLine) + ? computeJustifyExtra(reorderedSpare, reorderedGapCount) + : 0; const int justifyContribution = (effectiveAlignment == CssTextAlign::Justify && !isLastLine) ? reorderedJustifyExtra * static_cast(reorderedGapCount) @@ -805,9 +1004,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 +1047,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 +1085,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; diff --git a/lib/Epub/Epub/ParsedText.h b/lib/Epub/Epub/ParsedText.h index 9c3af7cb..81fbe69b 100644 --- a/lib/Epub/Epub/ParsedText.h +++ b/lib/Epub/Epub/ParsedText.h @@ -15,7 +15,8 @@ class GfxRenderer; class ParsedText { std::vector words; std::vector wordStyles; - std::vector wordContinues; // true = word attaches to previous (no space before it) + std::vector wordContinues; // true = word attaches to previous with no break + std::vector wordNoSpaceBefore; // true = may break before token, but no synthetic space when joined std::vector 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 reorderedStylesScratch; std::vector reorderedWidthsScratch; std::vector reorderedContinuesScratch; + std::vector reorderedNoSpaceBeforeScratch; std::vector reorderedFocusSuffixScratch; std::vector visualOrderScratch; int resolveFirstLineIndent(bool isFirstLine, const GfxRenderer& renderer, int fontId) const; std::vector computeLineBreaks(const GfxRenderer& renderer, int fontId, int pageWidth, - std::vector& wordWidths, std::vector& continuesVec); + std::vector& wordWidths, std::vector& continuesVec, + std::vector& noSpaceBeforeVec); std::vector computeHyphenatedLineBreaks(const GfxRenderer& renderer, int fontId, int pageWidth, - std::vector& wordWidths, std::vector& continuesVec); + std::vector& wordWidths, std::vector& continuesVec, + std::vector& noSpaceBeforeVec); bool hyphenateWordAtIndex(size_t wordIndex, int availableWidth, const GfxRenderer& renderer, int fontId, std::vector& wordWidths, bool allowFallbackBreaks); void extractLine(size_t breakIndex, int pageWidth, const std::vector& wordWidths, - const std::vector& continuesVec, const std::vector& lineBreakIndices, + const std::vector& continuesVec, const std::vector& noSpaceBeforeVec, + const std::vector& lineBreakIndices, const std::function)>& processLine, const GfxRenderer& renderer, int fontId); std::vector calculateWordWidths(const GfxRenderer& renderer, int fontId); diff --git a/lib/GfxRenderer/GfxRenderer.cpp b/lib/GfxRenderer/GfxRenderer.cpp index 759b2ad4..fd89bbc6 100644 --- a/lib/GfxRenderer/GfxRenderer.cpp +++ b/lib/GfxRenderer/GfxRenderer.cpp @@ -1437,8 +1437,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(&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); diff --git a/lib/Utf8/Utf8.h b/lib/Utf8/Utf8.h index e7238f85..2d3375fd 100644 --- a/lib/Utf8/Utf8.h +++ b/lib/Utf8/Utf8.h @@ -21,12 +21,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 diff --git a/lib/ZipFile/ZipFile.cpp b/lib/ZipFile/ZipFile.cpp index 1c3964a1..711c05c7 100644 --- a/lib/ZipFile/ZipFile.cpp +++ b/lib/ZipFile/ZipFile.cpp @@ -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(malloc(deflatedDataSize)); - if (deflatedData == nullptr) { - LOG_ERR("ZIP", "Failed to allocate memory for decompression buffer"); + auto* fileReadBuffer = static_cast(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 { diff --git a/src/SdCardFontSystem.cpp b/src/SdCardFontSystem.cpp index b771bede..79aa1226 100644 --- a/src/SdCardFontSystem.cpp +++ b/src/SdCardFontSystem.cpp @@ -5,12 +5,16 @@ #include "CrossPointSettings.h" +namespace { + static uint8_t fontSizeEnumFromSettings() { uint8_t e = SETTINGS.fontSize; if (e >= CrossPointSettings::FONT_SIZE_COUNT) e = 1; // default to MEDIUM return e; } +} // namespace + void SdCardFontSystem::begin(GfxRenderer& renderer) { registry_.discover(); @@ -74,10 +78,8 @@ void SdCardFontSystem::ensureLoaded(GfxRenderer& renderer) { SETTINGS.sdFontFamilyName[0] = '\0'; return; } - auto sizes = family->availableSizes(); - uint8_t idx = sizeEnum; - if (idx >= sizes.size()) idx = sizes.size() - 1; - uint8_t wantedPt = sizes.empty() ? 0 : sizes[idx]; + const auto* selected = family->findClosestReaderSize(sizeEnum); + const uint8_t wantedPt = selected ? selected->pointSize : 0; if (!registryWasDirty && wantedPt == manager_.currentPointSize()) return; LOG_DBG("SDFS", "Reloading %s: size %u -> %u (enum %u)%s", wantedFamily, manager_.currentPointSize(), wantedPt, sizeEnum, registryWasDirty ? " [registry dirty]" : ""); diff --git a/src/activities/ActivityManager.cpp b/src/activities/ActivityManager.cpp index 524ed3e0..96e85460 100644 --- a/src/activities/ActivityManager.cpp +++ b/src/activities/ActivityManager.cpp @@ -1,5 +1,6 @@ #include "ActivityManager.h" +#include #include #include diff --git a/src/activities/reader/ReaderActivity.cpp b/src/activities/reader/ReaderActivity.cpp index 6894ee70..e4c040df 100644 --- a/src/activities/reader/ReaderActivity.cpp +++ b/src/activities/reader/ReaderActivity.cpp @@ -2,6 +2,7 @@ #include #include +#include #include "CrossPointSettings.h" #include "Epub.h" @@ -29,7 +30,11 @@ std::unique_ptr ReaderActivity::loadEpub(const std::string& path) { return nullptr; } - auto epub = std::unique_ptr(new Epub(path, "/.crosspoint")); + auto epub = makeUniqueNoThrow(path, "/.crosspoint"); + if (!epub) { + LOG_ERR("READER", "Failed to allocate EPUB object"); + return nullptr; + } if (epub->load(true, SETTINGS.embeddedStyle == 0)) { return epub; } @@ -44,7 +49,11 @@ std::unique_ptr ReaderActivity::loadXtc(const std::string& path) { return nullptr; } - auto xtc = std::unique_ptr(new Xtc(path, "/.crosspoint")); + auto xtc = makeUniqueNoThrow(path, "/.crosspoint"); + if (!xtc) { + LOG_ERR("READER", "Failed to allocate XTC object"); + return nullptr; + } if (xtc->load()) { return xtc; } @@ -59,7 +68,11 @@ std::unique_ptr ReaderActivity::loadTxt(const std::string& path) { return nullptr; } - auto txt = std::unique_ptr(new Txt(path, "/.crosspoint")); + auto txt = makeUniqueNoThrow(path, "/.crosspoint"); + if (!txt) { + LOG_ERR("READER", "Failed to allocate TXT object"); + return nullptr; + } if (txt->load()) { return txt; }