Merge remote-tracking branch 'origin/develop' into feat-bluetooth
This commit is contained in:
+19
-59
@@ -153,18 +153,11 @@ bool Epub::parseTocNcxFile() const {
|
||||
|
||||
LOG_DBG("EBP", "Parsing toc ncx file: %s", tocNcxItem.c_str());
|
||||
|
||||
const auto tmpNcxPath = getCachePath() + "/toc.ncx";
|
||||
HalFile tempNcxFile;
|
||||
if (!Storage.openFileForWrite("EBP", tmpNcxPath, tempNcxFile)) {
|
||||
size_t ncxSize;
|
||||
if (!getItemSize(tocNcxItem, &ncxSize)) {
|
||||
LOG_ERR("EBP", "Could not get size of toc ncx file");
|
||||
return false;
|
||||
}
|
||||
readItemContentsToStream(tocNcxItem, tempNcxFile, 1024);
|
||||
// Explicitly close() file before reopening for reading
|
||||
tempNcxFile.close();
|
||||
if (!Storage.openFileForRead("EBP", tmpNcxPath, tempNcxFile)) {
|
||||
return false;
|
||||
}
|
||||
const auto ncxSize = tempNcxFile.size();
|
||||
|
||||
TocNcxParser ncxParser(contentBasePath, ncxSize, bookMetadataCache.get());
|
||||
|
||||
@@ -173,29 +166,13 @@ bool Epub::parseTocNcxFile() const {
|
||||
return false;
|
||||
}
|
||||
|
||||
const auto ncxBuffer = static_cast<uint8_t*>(malloc(1024));
|
||||
if (!ncxBuffer) {
|
||||
LOG_ERR("EBP", "Could not allocate memory for toc ncx parser");
|
||||
// Stream the decompressed NCX straight into the parser instead of round-tripping
|
||||
// through a temp file on the SD card (decompress -> write -> reopen -> reread -> delete).
|
||||
if (!readItemContentsToStream(tocNcxItem, ncxParser, 1024)) {
|
||||
LOG_ERR("EBP", "Could not read toc ncx file");
|
||||
return false;
|
||||
}
|
||||
|
||||
while (tempNcxFile.available()) {
|
||||
const auto readSize = tempNcxFile.read(ncxBuffer, 1024);
|
||||
if (readSize == 0) break;
|
||||
const auto processedSize = ncxParser.write(ncxBuffer, readSize);
|
||||
|
||||
if (processedSize != readSize) {
|
||||
LOG_ERR("EBP", "Could not process all toc ncx data");
|
||||
free(ncxBuffer);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
free(ncxBuffer);
|
||||
// Explicitly close() file before calling Storage.remove()
|
||||
tempNcxFile.close();
|
||||
Storage.remove(tmpNcxPath.c_str());
|
||||
|
||||
LOG_DBG("EBP", "Parsed TOC items");
|
||||
return true;
|
||||
}
|
||||
@@ -209,18 +186,11 @@ bool Epub::parseTocNavFile() const {
|
||||
|
||||
LOG_DBG("EBP", "Parsing toc nav file: %s", tocNavItem.c_str());
|
||||
|
||||
const auto tmpNavPath = getCachePath() + "/toc.nav";
|
||||
HalFile tempNavFile;
|
||||
if (!Storage.openFileForWrite("EBP", tmpNavPath, tempNavFile)) {
|
||||
size_t navSize;
|
||||
if (!getItemSize(tocNavItem, &navSize)) {
|
||||
LOG_ERR("EBP", "Could not get size of toc nav file");
|
||||
return false;
|
||||
}
|
||||
readItemContentsToStream(tocNavItem, tempNavFile, 1024);
|
||||
// Explicitly close() file before reopening for reading
|
||||
tempNavFile.close();
|
||||
if (!Storage.openFileForRead("EBP", tmpNavPath, tempNavFile)) {
|
||||
return false;
|
||||
}
|
||||
const auto navSize = tempNavFile.size();
|
||||
|
||||
// Note: We can't use `contentBasePath` here as the nav file may be in a different folder to the content.opf
|
||||
// and the HTMLX nav file will have hrefs relative to itself
|
||||
@@ -232,28 +202,13 @@ bool Epub::parseTocNavFile() const {
|
||||
return false;
|
||||
}
|
||||
|
||||
const auto navBuffer = static_cast<uint8_t*>(malloc(1024));
|
||||
if (!navBuffer) {
|
||||
LOG_ERR("EBP", "Could not allocate memory for toc nav parser");
|
||||
// Stream the decompressed nav document straight into the parser instead of round-tripping
|
||||
// through a temp file on the SD card (decompress -> write -> reopen -> reread -> delete).
|
||||
if (!readItemContentsToStream(tocNavItem, navParser, 1024)) {
|
||||
LOG_ERR("EBP", "Could not read toc nav file");
|
||||
return false;
|
||||
}
|
||||
|
||||
while (tempNavFile.available()) {
|
||||
const auto readSize = tempNavFile.read(navBuffer, 1024);
|
||||
const auto processedSize = navParser.write(navBuffer, readSize);
|
||||
|
||||
if (processedSize != readSize) {
|
||||
LOG_ERR("EBP", "Could not process all toc nav data");
|
||||
free(navBuffer);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
free(navBuffer);
|
||||
// Explicitly close() file before calling Storage.remove()
|
||||
tempNavFile.close();
|
||||
Storage.remove(tmpNavPath.c_str());
|
||||
|
||||
LOG_DBG("EBP", "Parsed TOC nav items");
|
||||
return true;
|
||||
}
|
||||
@@ -396,6 +351,11 @@ bool Epub::load(const bool buildIfMissing, const bool skipLoadingCss) {
|
||||
Storage.removeDir((cachePath + "/sections").c_str());
|
||||
}
|
||||
}
|
||||
// Release the resolved CSS rule map: it is only needed transiently while building
|
||||
// section caches, and createSectionFile reloads it from cache on demand. Holding it
|
||||
// resident pins tens of KB for the whole reading session (more on warm resume into
|
||||
// an already-cached chapter, where createSectionFile never runs to clear it).
|
||||
cssParser->clear();
|
||||
LOG_DBG("EBP", "Loaded ePub: %s", filepath.c_str());
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#include "BookMetadataCache.h"
|
||||
|
||||
#include <BufferedFile.h>
|
||||
#include <Logging.h>
|
||||
#include <Serialization.h>
|
||||
#include <Utf8.h>
|
||||
@@ -14,6 +15,52 @@ constexpr uint8_t BOOK_CACHE_VERSION = 8; // v8: TOC/book titles stored NFC-com
|
||||
constexpr char bookBinFile[] = "/book.bin";
|
||||
constexpr char tmpSpineBinFile[] = "/spine.bin.tmp";
|
||||
constexpr char tmpTocBinFile[] = "/toc.bin.tmp";
|
||||
// Buffer size for the buildBookBin streams. 3 buffers x 4KB, transient (freed on
|
||||
// return); 4KB = 8 SD sectors per transfer, enough to stop the sector-cache thrash.
|
||||
constexpr size_t BUILD_IO_BUFFER_SIZE = 4096;
|
||||
|
||||
// Entry (de)serializers, templated so they run over HalFile and the Buffered*
|
||||
// wrappers alike (two instantiations each -- a few hundred bytes of flash, in
|
||||
// exchange for the build path streaming at SD speed instead of per-pod).
|
||||
template <typename F>
|
||||
uint32_t writeSpineEntryTo(F& file, const BookMetadataCache::SpineEntry& entry) {
|
||||
const uint32_t pos = file.position();
|
||||
serialization::writeString(file, entry.href);
|
||||
serialization::writePod(file, entry.cumulativeSize);
|
||||
serialization::writePod(file, entry.tocIndex);
|
||||
return pos;
|
||||
}
|
||||
|
||||
template <typename F>
|
||||
uint32_t writeTocEntryTo(F& file, const BookMetadataCache::TocEntry& entry) {
|
||||
const uint32_t pos = file.position();
|
||||
serialization::writeString(file, entry.title);
|
||||
serialization::writeString(file, entry.href);
|
||||
serialization::writeString(file, entry.anchor);
|
||||
serialization::writePod(file, entry.level);
|
||||
serialization::writePod(file, entry.spineIndex);
|
||||
return pos;
|
||||
}
|
||||
|
||||
template <typename F>
|
||||
BookMetadataCache::SpineEntry readSpineEntryFrom(F& file) {
|
||||
BookMetadataCache::SpineEntry entry;
|
||||
serialization::readString(file, entry.href);
|
||||
serialization::readPod(file, entry.cumulativeSize);
|
||||
serialization::readPod(file, entry.tocIndex);
|
||||
return entry;
|
||||
}
|
||||
|
||||
template <typename F>
|
||||
BookMetadataCache::TocEntry readTocEntryFrom(F& file) {
|
||||
BookMetadataCache::TocEntry entry;
|
||||
serialization::readString(file, entry.title);
|
||||
serialization::readString(file, entry.href);
|
||||
serialization::readString(file, entry.anchor);
|
||||
serialization::readPod(file, entry.level);
|
||||
serialization::readPod(file, entry.spineIndex);
|
||||
return entry;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
/* ============= WRITING / BUILDING FUNCTIONS ================ */
|
||||
@@ -30,13 +77,23 @@ bool BookMetadataCache::beginContentOpfPass() {
|
||||
LOG_DBG("BMC", "Beginning content opf pass");
|
||||
|
||||
// Open spine file for writing
|
||||
return Storage.openFileForWrite("BMC", cachePath + tmpSpineBinFile, spineFile);
|
||||
if (!Storage.openFileForWrite("BMC", cachePath + tmpSpineBinFile, spineFile)) {
|
||||
return false;
|
||||
}
|
||||
// Wrapper OOM is fine: createSpineEntry falls back to unbuffered writes.
|
||||
passOut = makeUniqueNoThrow<serialization::BufferedFileWriter>(spineFile, BUILD_IO_BUFFER_SIZE);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool BookMetadataCache::endContentOpfPass() {
|
||||
const bool flushed = !passOut || passOut->flush();
|
||||
passOut.reset();
|
||||
// Explicit close() required: member variable persists beyond function scope
|
||||
spineFile.close();
|
||||
return true;
|
||||
if (!flushed) {
|
||||
LOG_ERR("BMC", "Failed writing spine tmp file");
|
||||
}
|
||||
return flushed;
|
||||
}
|
||||
|
||||
bool BookMetadataCache::beginTocPass() {
|
||||
@@ -74,10 +131,17 @@ bool BookMetadataCache::beginTocPass() {
|
||||
useSpineHrefIndex = false;
|
||||
}
|
||||
|
||||
// Wrapper OOM is fine: createTocEntry falls back to unbuffered writes.
|
||||
passOut = makeUniqueNoThrow<serialization::BufferedFileWriter>(tocFile, BUILD_IO_BUFFER_SIZE);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool BookMetadataCache::endTocPass() {
|
||||
const bool flushed = !passOut || passOut->flush();
|
||||
passOut.reset();
|
||||
if (!flushed) {
|
||||
LOG_ERR("BMC", "Failed writing toc tmp file");
|
||||
}
|
||||
// Explicit close() required: member variables persist beyond function scope
|
||||
tocFile.close();
|
||||
spineFile.close();
|
||||
@@ -86,7 +150,7 @@ bool BookMetadataCache::endTocPass() {
|
||||
spineHrefIndex.shrink_to_fit();
|
||||
useSpineHrefIndex = false;
|
||||
|
||||
return true;
|
||||
return flushed;
|
||||
}
|
||||
|
||||
bool BookMetadataCache::endWrite() {
|
||||
@@ -119,6 +183,14 @@ bool BookMetadataCache::buildBookBin(const std::string& epubPath, const BookMeta
|
||||
return false;
|
||||
}
|
||||
|
||||
// Buffered streams for the whole build: every access below is sequential per
|
||||
// file, but interleaved ACROSS files, which thrashes SdFat's single shared
|
||||
// sector cache when unbuffered (one 512B SD transaction per 4-byte pod --
|
||||
// measured 31s for a 1,732-spine omnibus). Three 4KB buffers, freed on return.
|
||||
serialization::BufferedFileWriter bookOut(bookFile, BUILD_IO_BUFFER_SIZE);
|
||||
serialization::BufferedFileReader spineIn(spineFile, BUILD_IO_BUFFER_SIZE);
|
||||
serialization::BufferedFileReader tocIn(tocFile, BUILD_IO_BUFFER_SIZE);
|
||||
|
||||
constexpr uint32_t headerASize =
|
||||
sizeof(BOOK_CACHE_VERSION) + /* LUT Offset */ sizeof(uint32_t) + sizeof(spineCount) + sizeof(tocCount);
|
||||
const uint32_t metadataSize = metadata.title.size() + metadata.author.size() + metadata.language.size() +
|
||||
@@ -128,31 +200,34 @@ bool BookMetadataCache::buildBookBin(const std::string& epubPath, const BookMeta
|
||||
const uint32_t lutOffset = headerASize + metadataSize;
|
||||
|
||||
// Header A
|
||||
serialization::writePod(bookFile, BOOK_CACHE_VERSION);
|
||||
serialization::writePod(bookFile, lutOffset);
|
||||
serialization::writePod(bookFile, spineCount);
|
||||
serialization::writePod(bookFile, tocCount);
|
||||
serialization::writePod(bookOut, BOOK_CACHE_VERSION);
|
||||
serialization::writePod(bookOut, lutOffset);
|
||||
serialization::writePod(bookOut, spineCount);
|
||||
serialization::writePod(bookOut, tocCount);
|
||||
// Metadata
|
||||
serialization::writeString(bookFile, metadata.title);
|
||||
serialization::writeString(bookFile, metadata.author);
|
||||
serialization::writeString(bookFile, metadata.language);
|
||||
serialization::writeString(bookFile, metadata.coverItemHref);
|
||||
serialization::writeString(bookFile, metadata.textReferenceHref);
|
||||
serialization::writeString(bookOut, metadata.title);
|
||||
serialization::writeString(bookOut, metadata.author);
|
||||
serialization::writeString(bookOut, metadata.language);
|
||||
serialization::writeString(bookOut, metadata.coverItemHref);
|
||||
serialization::writeString(bookOut, metadata.textReferenceHref);
|
||||
|
||||
// Loop through spine entries, writing LUT positions
|
||||
spineFile.seek(0);
|
||||
spineIn.seek(0);
|
||||
for (int i = 0; i < spineCount; i++) {
|
||||
uint32_t pos = spineFile.position();
|
||||
auto spineEntry = readSpineEntry(spineFile);
|
||||
serialization::writePod(bookFile, pos + lutOffset + lutSize);
|
||||
const uint32_t pos = spineIn.position();
|
||||
readSpineEntryFrom(spineIn);
|
||||
serialization::writePod(bookOut, pos + lutOffset + lutSize);
|
||||
}
|
||||
// Total size of the spine tmp file: entries land in book.bin after the toc LUT
|
||||
// and the full spine block, so toc LUT positions are offset by it.
|
||||
const auto spineBytes = static_cast<uint32_t>(spineIn.position());
|
||||
|
||||
// Loop through toc entries, writing LUT positions
|
||||
tocFile.seek(0);
|
||||
tocIn.seek(0);
|
||||
for (int i = 0; i < tocCount; i++) {
|
||||
uint32_t pos = tocFile.position();
|
||||
auto tocEntry = readTocEntry(tocFile);
|
||||
serialization::writePod(bookFile, pos + lutOffset + lutSize + static_cast<uint32_t>(spineFile.position()));
|
||||
const uint32_t pos = tocIn.position();
|
||||
readTocEntryFrom(tocIn);
|
||||
serialization::writePod(bookOut, pos + lutOffset + lutSize + spineBytes);
|
||||
}
|
||||
|
||||
// LUTs complete
|
||||
@@ -160,9 +235,9 @@ bool BookMetadataCache::buildBookBin(const std::string& epubPath, const BookMeta
|
||||
|
||||
// Build spineIndex->tocIndex mapping in one pass (O(n) instead of O(n*m))
|
||||
std::deque<int16_t> spineToTocIndex(spineCount, -1);
|
||||
tocFile.seek(0);
|
||||
tocIn.seek(0);
|
||||
for (int j = 0; j < tocCount; j++) {
|
||||
auto tocEntry = readTocEntry(tocFile);
|
||||
auto tocEntry = readTocEntryFrom(tocIn);
|
||||
if (tocEntry.spineIndex >= 0 && tocEntry.spineIndex < spineCount) {
|
||||
if (spineToTocIndex[tocEntry.spineIndex] == -1) {
|
||||
spineToTocIndex[tocEntry.spineIndex] = static_cast<int16_t>(j);
|
||||
@@ -197,9 +272,9 @@ bool BookMetadataCache::buildBookBin(const std::string& epubPath, const BookMeta
|
||||
std::deque<ZipFile::SizeTarget> targets;
|
||||
targets.resize(spineCount);
|
||||
|
||||
spineFile.seek(0);
|
||||
spineIn.seek(0);
|
||||
for (int i = 0; i < spineCount; i++) {
|
||||
auto entry = readSpineEntry(spineFile);
|
||||
auto entry = readSpineEntryFrom(spineIn);
|
||||
std::string path = FsHelpers::normalisePath(entry.href);
|
||||
|
||||
ZipFile::SizeTarget t;
|
||||
@@ -224,10 +299,10 @@ bool BookMetadataCache::buildBookBin(const std::string& epubPath, const BookMeta
|
||||
}
|
||||
|
||||
uint32_t cumSize = 0;
|
||||
spineFile.seek(0);
|
||||
spineIn.seek(0);
|
||||
int lastSpineTocIndex = -1;
|
||||
for (int i = 0; i < spineCount; i++) {
|
||||
auto spineEntry = readSpineEntry(spineFile);
|
||||
auto spineEntry = readSpineEntryFrom(spineIn);
|
||||
|
||||
spineEntry.tocIndex = spineToTocIndex[i];
|
||||
|
||||
@@ -260,23 +335,33 @@ bool BookMetadataCache::buildBookBin(const std::string& epubPath, const BookMeta
|
||||
spineEntry.cumulativeSize = cumSize;
|
||||
|
||||
// Write out spine data to book.bin
|
||||
writeSpineEntry(bookFile, spineEntry);
|
||||
writeSpineEntryTo(bookOut, spineEntry);
|
||||
}
|
||||
// Close opened zip file
|
||||
zip.close();
|
||||
|
||||
// Loop through toc entries from toc file writing to book.bin
|
||||
tocFile.seek(0);
|
||||
tocIn.seek(0);
|
||||
for (int i = 0; i < tocCount; i++) {
|
||||
auto tocEntry = readTocEntry(tocFile);
|
||||
writeTocEntry(bookFile, tocEntry);
|
||||
auto tocEntry = readTocEntryFrom(tocIn);
|
||||
writeTocEntryTo(bookOut, tocEntry);
|
||||
}
|
||||
|
||||
const bool written = bookOut.flush();
|
||||
|
||||
// Explicit close() required: member variables persist beyond function scope
|
||||
bookFile.close();
|
||||
spineFile.close();
|
||||
tocFile.close();
|
||||
|
||||
if (!written) {
|
||||
// A short write (card full/removed) would leave a truncated book.bin that
|
||||
// still passes the version check on load; remove it so the next open rebuilds.
|
||||
LOG_ERR("BMC", "Failed writing book.bin, removing truncated file");
|
||||
Storage.remove((cachePath + bookBinFile).c_str());
|
||||
return false;
|
||||
}
|
||||
|
||||
LOG_DBG("BMC", "Successfully built book.bin");
|
||||
return true;
|
||||
}
|
||||
@@ -294,21 +379,11 @@ bool BookMetadataCache::cleanupTmpFiles() const {
|
||||
}
|
||||
|
||||
uint32_t BookMetadataCache::writeSpineEntry(HalFile& file, const SpineEntry& entry) const {
|
||||
const uint32_t pos = file.position();
|
||||
serialization::writeString(file, entry.href);
|
||||
serialization::writePod(file, entry.cumulativeSize);
|
||||
serialization::writePod(file, entry.tocIndex);
|
||||
return pos;
|
||||
return writeSpineEntryTo(file, entry);
|
||||
}
|
||||
|
||||
uint32_t BookMetadataCache::writeTocEntry(HalFile& file, const TocEntry& entry) const {
|
||||
const uint32_t pos = file.position();
|
||||
serialization::writeString(file, entry.title);
|
||||
serialization::writeString(file, entry.href);
|
||||
serialization::writeString(file, entry.anchor);
|
||||
serialization::writePod(file, entry.level);
|
||||
serialization::writePod(file, entry.spineIndex);
|
||||
return pos;
|
||||
return writeTocEntryTo(file, entry);
|
||||
}
|
||||
|
||||
// Note: for the LUT to be accurate, this **MUST** be called for all spine items before `addTocEntry` is ever called
|
||||
@@ -320,7 +395,11 @@ void BookMetadataCache::createSpineEntry(const std::string& href) {
|
||||
}
|
||||
|
||||
const SpineEntry entry(href, 0, -1);
|
||||
writeSpineEntry(spineFile, entry);
|
||||
if (passOut) {
|
||||
writeSpineEntryTo(*passOut, entry);
|
||||
} else {
|
||||
writeSpineEntry(spineFile, entry);
|
||||
}
|
||||
spineCount++;
|
||||
}
|
||||
|
||||
@@ -368,7 +447,11 @@ void BookMetadataCache::createTocEntry(const std::string& title, const std::stri
|
||||
// Compose the title to NFC at index time so the cache stores precomposed glyphs;
|
||||
// device fonts have no combining-mark positioning, so NFD titles render broken.
|
||||
const TocEntry entry(utf8ComposeNfc(title), href, anchor, level, spineIndex);
|
||||
writeTocEntry(tocFile, entry);
|
||||
if (passOut) {
|
||||
writeTocEntryTo(*passOut, entry);
|
||||
} else {
|
||||
writeTocEntry(tocFile, entry);
|
||||
}
|
||||
tocCount++;
|
||||
}
|
||||
|
||||
@@ -442,19 +525,7 @@ BookMetadataCache::TocEntry BookMetadataCache::getTocEntry(const int index) {
|
||||
}
|
||||
|
||||
BookMetadataCache::SpineEntry BookMetadataCache::readSpineEntry(HalFile& file) const {
|
||||
SpineEntry entry;
|
||||
serialization::readString(file, entry.href);
|
||||
serialization::readPod(file, entry.cumulativeSize);
|
||||
serialization::readPod(file, entry.tocIndex);
|
||||
return entry;
|
||||
return readSpineEntryFrom(file);
|
||||
}
|
||||
|
||||
BookMetadataCache::TocEntry BookMetadataCache::readTocEntry(HalFile& file) const {
|
||||
TocEntry entry;
|
||||
serialization::readString(file, entry.title);
|
||||
serialization::readString(file, entry.href);
|
||||
serialization::readString(file, entry.anchor);
|
||||
serialization::readPod(file, entry.level);
|
||||
serialization::readPod(file, entry.spineIndex);
|
||||
return entry;
|
||||
}
|
||||
BookMetadataCache::TocEntry BookMetadataCache::readTocEntry(HalFile& file) const { return readTocEntryFrom(file); }
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
#pragma once
|
||||
|
||||
#include <BufferedFile.h>
|
||||
#include <HalStorage.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <deque>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
class BookMetadataCache {
|
||||
@@ -54,6 +56,11 @@ class BookMetadataCache {
|
||||
// Temp file handles during build
|
||||
HalFile spineFile;
|
||||
HalFile tocFile;
|
||||
// Buffers the per-entry tmp-file writes during the OPF/TOC passes: those
|
||||
// writes interleave with zip-inflate SD reads, and unbuffered they thrash
|
||||
// SdFat's shared sector cache (one 512B transaction per 4-byte pod). One
|
||||
// wrapper serves whichever pass is active (spine, then toc).
|
||||
std::unique_ptr<serialization::BufferedFileWriter> passOut;
|
||||
|
||||
// Index for fast href→spineIndex lookup (used only for large EPUBs)
|
||||
struct SpineHrefIndexEntry {
|
||||
|
||||
@@ -57,6 +57,10 @@ void PageImage::render(GfxRenderer& renderer, const int fontId, const int xOffse
|
||||
imageBlock->render(renderer, xPos + xOffset, yPos + yOffset);
|
||||
}
|
||||
|
||||
void PageImage::renderPlaceholder(GfxRenderer& renderer, const int xOffset, const int yOffset) const {
|
||||
imageBlock->renderPlaceholder(renderer, xPos + xOffset, yPos + yOffset);
|
||||
}
|
||||
|
||||
bool PageImage::serialize(HalFile& file) {
|
||||
serialization::writePod(file, xPos);
|
||||
serialization::writePod(file, yPos);
|
||||
@@ -125,6 +129,17 @@ void Page::renderImages(GfxRenderer& renderer, const int fontId, const int xOffs
|
||||
[](const PageElement& element) { return element.getTag() == TAG_PageImage; });
|
||||
}
|
||||
|
||||
void Page::renderWithImagePlaceholders(GfxRenderer& renderer, const int fontId, const int xOffset,
|
||||
const int yOffset) const {
|
||||
for (const auto& element : elements) {
|
||||
if (element->getTag() == TAG_PageImage) {
|
||||
static_cast<const PageImage&>(*element).renderPlaceholder(renderer, xOffset, yOffset);
|
||||
} else {
|
||||
element->render(renderer, fontId, xOffset, yOffset);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool Page::serialize(HalFile& file) const {
|
||||
const uint16_t count = elements.size();
|
||||
serialization::writePod(file, count);
|
||||
|
||||
@@ -50,6 +50,7 @@ class PageImage final : public PageElement {
|
||||
PageImage(std::shared_ptr<ImageBlock> block, const int16_t xPos, const int16_t yPos)
|
||||
: PageElement(xPos, yPos), imageBlock(std::move(block)) {}
|
||||
void render(GfxRenderer& renderer, int fontId, int xOffset, int yOffset) override;
|
||||
void renderPlaceholder(GfxRenderer& renderer, int xOffset, int yOffset) const;
|
||||
bool serialize(HalFile& file) override;
|
||||
PageElementTag getTag() const override { return TAG_PageImage; }
|
||||
static std::unique_ptr<PageImage> deserialize(HalFile& file);
|
||||
@@ -89,6 +90,7 @@ class Page {
|
||||
|
||||
void render(GfxRenderer& renderer, int fontId, int xOffset, int yOffset) const;
|
||||
void renderImages(GfxRenderer& renderer, int fontId, int xOffset, int yOffset) const;
|
||||
void renderWithImagePlaceholders(GfxRenderer& renderer, int fontId, int xOffset, int yOffset) const;
|
||||
bool serialize(HalFile& file) const;
|
||||
static std::unique_ptr<Page> deserialize(HalFile& file);
|
||||
|
||||
@@ -98,6 +100,13 @@ class Page {
|
||||
[](const std::shared_ptr<PageElement>& el) { return el->getTag() == TAG_PageImage; });
|
||||
}
|
||||
|
||||
bool hasImagesNeedingDecode() const {
|
||||
return std::any_of(elements.begin(), elements.end(), [](const std::shared_ptr<PageElement>& element) {
|
||||
return element->getTag() == TAG_PageImage &&
|
||||
static_cast<const PageImage&>(*element).getImageBlock().needsDecode();
|
||||
});
|
||||
}
|
||||
|
||||
// Get bounding box of all images on the page (union of image rects)
|
||||
// Returns false if no images. Coordinates are relative to page origin.
|
||||
bool getImageBoundingBox(int16_t& outX, int16_t& outY, int16_t& outW, int16_t& outH) const {
|
||||
|
||||
@@ -11,9 +11,13 @@
|
||||
#include "parsers/ChapterHtmlSlimParser.h"
|
||||
|
||||
namespace {
|
||||
// v28: text decoration bits now include line-through in serialized wordStyles.
|
||||
// v29: TextBlock word data stored as one flat arena (offset table + NUL-terminated
|
||||
// text blob) instead of length-prefixed strings and per-field arrays.
|
||||
constexpr uint8_t SECTION_FILE_VERSION = 29;
|
||||
// v30: Arabic shaping changed both drawing and measurement (getTextAdvanceX now
|
||||
// measures the shaped visual text); cached word positions from v29 no longer
|
||||
// match what drawText renders.
|
||||
constexpr uint8_t SECTION_FILE_VERSION = 30;
|
||||
// Written into the version field while a build is in progress; patched to
|
||||
// SECTION_FILE_VERSION only when the build is finalized. An abandoned /
|
||||
// crash-interrupted .bin therefore carries version 0, which loadSectionFile rejects
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
#include <Logging.h>
|
||||
#include <Serialization.h>
|
||||
|
||||
#include <cstdlib>
|
||||
|
||||
#include "Epub/converters/DirectPixelWriter.h"
|
||||
#include "Epub/converters/ImageDecoderFactory.h"
|
||||
|
||||
@@ -29,6 +31,54 @@ std::string getCachePath(const std::string& imagePath) {
|
||||
return imagePath + ".pxc";
|
||||
}
|
||||
|
||||
bool readValidCacheHeader(HalFile& cacheFile, const int expectedWidth, const int expectedHeight, uint16_t& cachedWidth,
|
||||
uint16_t& cachedHeight) {
|
||||
if (cacheFile.read(&cachedWidth, 2) != 2 || cacheFile.read(&cachedHeight, 2) != 2) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const int widthDiff = abs(cachedWidth - expectedWidth);
|
||||
const int heightDiff = abs(cachedHeight - expectedHeight);
|
||||
if (widthDiff > 1 || heightDiff > 1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const size_t bytesPerRow = (cachedWidth + 3) / 4;
|
||||
const size_t expectedSize = 4 + bytesPerRow * cachedHeight;
|
||||
return cacheFile.size() >= expectedSize;
|
||||
}
|
||||
|
||||
// Pages are deserialized afresh on each visit. Keep a bounded, allocation-free
|
||||
// record so an image that failed renders its placeholder directly for the rest
|
||||
// of the reader session instead of paying another placeholder refresh and
|
||||
// decode. The reader clears this on entry so transient memory/storage failures
|
||||
// are retried.
|
||||
constexpr size_t MAX_SESSION_IMAGE_FAILURES = 16;
|
||||
uint64_t failedImageHashes[MAX_SESSION_IMAGE_FAILURES];
|
||||
size_t failedImageCount = 0;
|
||||
|
||||
uint64_t imagePathHash(const std::string& path) {
|
||||
uint64_t hash = 14695981039346656037ull;
|
||||
for (const char c : path) {
|
||||
hash ^= static_cast<uint8_t>(c);
|
||||
hash *= 1099511628211ull;
|
||||
}
|
||||
return hash;
|
||||
}
|
||||
|
||||
bool imageFailedThisSession(const std::string& path) {
|
||||
const uint64_t hash = imagePathHash(path);
|
||||
for (size_t i = 0; i < failedImageCount; i++) {
|
||||
if (failedImageHashes[i] == hash) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void rememberImageFailure(const std::string& path) {
|
||||
if (failedImageCount == MAX_SESSION_IMAGE_FAILURES || imageFailedThisSession(path)) return;
|
||||
failedImageHashes[failedImageCount++] = imagePathHash(path);
|
||||
}
|
||||
|
||||
bool renderFromCache(GfxRenderer& renderer, const std::string& cachePath, int x, int y, int expectedWidth,
|
||||
int expectedHeight) {
|
||||
HalFile cacheFile;
|
||||
@@ -37,16 +87,8 @@ bool renderFromCache(GfxRenderer& renderer, const std::string& cachePath, int x,
|
||||
}
|
||||
|
||||
uint16_t cachedWidth, cachedHeight;
|
||||
if (cacheFile.read(&cachedWidth, 2) != 2 || cacheFile.read(&cachedHeight, 2) != 2) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Verify dimensions are close (allow 1 pixel tolerance for rounding differences)
|
||||
int widthDiff = abs(cachedWidth - expectedWidth);
|
||||
int heightDiff = abs(cachedHeight - expectedHeight);
|
||||
if (widthDiff > 1 || heightDiff > 1) {
|
||||
LOG_ERR("IMG", "Cache dimension mismatch: %dx%d vs %dx%d", cachedWidth, cachedHeight, expectedWidth,
|
||||
expectedHeight);
|
||||
if (!readValidCacheHeader(cacheFile, expectedWidth, expectedHeight, cachedWidth, cachedHeight)) {
|
||||
LOG_ERR("IMG", "Invalid image cache: %s", cachePath.c_str());
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -119,6 +161,28 @@ bool renderFromCache(GfxRenderer& renderer, const std::string& cachePath, int x,
|
||||
|
||||
} // namespace
|
||||
|
||||
bool ImageBlock::hasValidCache() const {
|
||||
const auto cachePath = getCachePath(imagePath);
|
||||
HalFile cacheFile;
|
||||
if (!Storage.openFileForRead("IMG", cachePath, cacheFile)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
uint16_t cachedWidth, cachedHeight;
|
||||
return readValidCacheHeader(cacheFile, width, height, cachedWidth, cachedHeight);
|
||||
}
|
||||
|
||||
bool ImageBlock::needsDecode() const { return !imageFailedThisSession(imagePath) && !hasValidCache(); }
|
||||
|
||||
void ImageBlock::clearSessionRenderFailures() { failedImageCount = 0; }
|
||||
|
||||
void ImageBlock::renderPlaceholder(GfxRenderer& renderer, const int x, const int y) const {
|
||||
renderer.fillRect(x, y, width, height, true);
|
||||
if (width > 2 && height > 2) {
|
||||
renderer.fillRect(x + 1, y + 1, width - 2, height - 2, false);
|
||||
}
|
||||
}
|
||||
|
||||
void ImageBlock::render(GfxRenderer& renderer, const int x, const int y) {
|
||||
// The font-prewarm scan pass only accumulates glyphs; an image contributes
|
||||
// none, and its DirectPixelWriter output bypasses the renderer's scan-mode
|
||||
@@ -150,6 +214,11 @@ void ImageBlock::render(GfxRenderer& renderer, const int x, const int y) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (imageFailedThisSession(imagePath)) {
|
||||
renderPlaceholder(renderer, x, y);
|
||||
return;
|
||||
}
|
||||
|
||||
// Try to render from cache first
|
||||
std::string cachePath = getCachePath(imagePath);
|
||||
if (renderFromCache(renderer, cachePath, x, y, width, height)) {
|
||||
@@ -161,6 +230,8 @@ void ImageBlock::render(GfxRenderer& renderer, const int x, const int y) {
|
||||
HalFile file;
|
||||
if (!Storage.openFileForRead("IMG", imagePath, file)) {
|
||||
LOG_ERR("IMG", "Image file not found: %s", imagePath.c_str());
|
||||
rememberImageFailure(imagePath);
|
||||
renderPlaceholder(renderer, x, y);
|
||||
return;
|
||||
}
|
||||
size_t fileSize = file.size();
|
||||
@@ -168,6 +239,8 @@ void ImageBlock::render(GfxRenderer& renderer, const int x, const int y) {
|
||||
|
||||
if (fileSize == 0) {
|
||||
LOG_ERR("IMG", "Image file is empty: %s", imagePath.c_str());
|
||||
rememberImageFailure(imagePath);
|
||||
renderPlaceholder(renderer, x, y);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -187,6 +260,8 @@ void ImageBlock::render(GfxRenderer& renderer, const int x, const int y) {
|
||||
ImageToFramebufferDecoder* decoder = ImageDecoderFactory::getDecoder(imagePath);
|
||||
if (!decoder) {
|
||||
LOG_ERR("IMG", "No decoder found for image: %s", imagePath.c_str());
|
||||
rememberImageFailure(imagePath);
|
||||
renderPlaceholder(renderer, x, y);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -195,6 +270,8 @@ void ImageBlock::render(GfxRenderer& renderer, const int x, const int y) {
|
||||
bool success = decoder->decodeToFramebuffer(imagePath, renderer, config);
|
||||
if (!success) {
|
||||
LOG_ERR("IMG", "Failed to decode image: %s", imagePath.c_str());
|
||||
rememberImageFailure(imagePath);
|
||||
renderPlaceholder(renderer, x, y);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -16,6 +16,10 @@ class ImageBlock final : public Block {
|
||||
int16_t getHeight() const { return height; }
|
||||
|
||||
bool imageExists() const;
|
||||
bool hasValidCache() const;
|
||||
bool needsDecode() const;
|
||||
void renderPlaceholder(GfxRenderer& renderer, int x, int y) const;
|
||||
static void clearSessionRenderFailures();
|
||||
|
||||
BlockType getType() override { return IMAGE_BLOCK; }
|
||||
bool isEmpty() override { return false; }
|
||||
|
||||
@@ -99,24 +99,58 @@ int bytesPerPixelFromType(int pixelType) {
|
||||
}
|
||||
}
|
||||
|
||||
int requiredPngInternalBufferBytes(int srcWidth, int pixelType) {
|
||||
int packedRowBytes(int srcWidth, int bitsPerSample) { return (srcWidth * bitsPerSample + 7) / 8; }
|
||||
|
||||
int requiredPngInternalBufferBytes(int srcWidth, int pixelType, int bitsPerSample) {
|
||||
// +1 filter byte per scanline, *2 for current+previous lines, +32 for alignment margin.
|
||||
int pitch = srcWidth * bytesPerPixelFromType(pixelType);
|
||||
if ((pixelType == PNG_PIXEL_GRAYSCALE || pixelType == PNG_PIXEL_INDEXED) && bitsPerSample < 8) {
|
||||
pitch = packedRowBytes(srcWidth, bitsPerSample);
|
||||
}
|
||||
return ((pitch + 1) * 2) + 32;
|
||||
}
|
||||
|
||||
bool isSupportedBitDepth(int pixelType, int bitsPerSample) {
|
||||
if (bitsPerSample == 8) return true;
|
||||
if (bitsPerSample != 1 && bitsPerSample != 2 && bitsPerSample != 4) return false;
|
||||
return pixelType == PNG_PIXEL_GRAYSCALE || pixelType == PNG_PIXEL_INDEXED;
|
||||
}
|
||||
|
||||
uint8_t readPackedSample(const uint8_t* pixels, int x, int bitsPerSample) {
|
||||
if (bitsPerSample == 8) return pixels[x];
|
||||
|
||||
const int bitOffset = x * bitsPerSample;
|
||||
const int shift = 8 - bitsPerSample - (bitOffset & 7);
|
||||
const uint8_t mask = (1U << bitsPerSample) - 1;
|
||||
return (pixels[bitOffset >> 3] >> shift) & mask;
|
||||
}
|
||||
|
||||
uint8_t expandSampleToByte(uint8_t sample, int bitsPerSample) {
|
||||
if (bitsPerSample == 8) return sample;
|
||||
const uint8_t maxSample = (1U << bitsPerSample) - 1;
|
||||
return static_cast<uint8_t>((sample * 255U) / maxSample);
|
||||
}
|
||||
|
||||
// Convert entire source line to grayscale with alpha blending to white background.
|
||||
// Low-bit-depth grayscale/indexed scanlines are packed most-significant sample first.
|
||||
// For indexed PNGs with tRNS chunk, alpha values are stored at palette[768] onwards.
|
||||
// Processing the whole line at once improves cache locality and reduces per-pixel overhead.
|
||||
void convertLineToGray(uint8_t* pPixels, uint8_t* grayLine, int width, int pixelType, uint8_t* palette, int hasAlpha) {
|
||||
void convertLineToGray(const uint8_t* pPixels, uint8_t* grayLine, int width, int pixelType, int bitsPerSample,
|
||||
uint8_t* palette, int hasAlpha) {
|
||||
switch (pixelType) {
|
||||
case PNG_PIXEL_GRAYSCALE:
|
||||
memcpy(grayLine, pPixels, width);
|
||||
if (bitsPerSample == 8) {
|
||||
memcpy(grayLine, pPixels, width);
|
||||
} else {
|
||||
for (int x = 0; x < width; x++) {
|
||||
grayLine[x] = expandSampleToByte(readPackedSample(pPixels, x, bitsPerSample), bitsPerSample);
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case PNG_PIXEL_TRUECOLOR:
|
||||
for (int x = 0; x < width; x++) {
|
||||
uint8_t* p = &pPixels[x * 3];
|
||||
const uint8_t* p = &pPixels[x * 3];
|
||||
grayLine[x] = (uint8_t)((p[0] * 77 + p[1] * 150 + p[2] * 29) >> 8);
|
||||
}
|
||||
break;
|
||||
@@ -125,7 +159,7 @@ void convertLineToGray(uint8_t* pPixels, uint8_t* grayLine, int width, int pixel
|
||||
if (palette) {
|
||||
if (hasAlpha) {
|
||||
for (int x = 0; x < width; x++) {
|
||||
uint8_t idx = pPixels[x];
|
||||
uint8_t idx = readPackedSample(pPixels, x, bitsPerSample);
|
||||
uint8_t* p = &palette[idx * 3];
|
||||
uint8_t gray = (uint8_t)((p[0] * 77 + p[1] * 150 + p[2] * 29) >> 8);
|
||||
uint8_t alpha = palette[768 + idx];
|
||||
@@ -133,12 +167,15 @@ void convertLineToGray(uint8_t* pPixels, uint8_t* grayLine, int width, int pixel
|
||||
}
|
||||
} else {
|
||||
for (int x = 0; x < width; x++) {
|
||||
uint8_t* p = &palette[pPixels[x] * 3];
|
||||
uint8_t idx = readPackedSample(pPixels, x, bitsPerSample);
|
||||
uint8_t* p = &palette[idx * 3];
|
||||
grayLine[x] = (uint8_t)((p[0] * 77 + p[1] * 150 + p[2] * 29) >> 8);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
memcpy(grayLine, pPixels, width);
|
||||
for (int x = 0; x < width; x++) {
|
||||
grayLine[x] = expandSampleToByte(readPackedSample(pPixels, x, bitsPerSample), bitsPerSample);
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
@@ -152,7 +189,7 @@ void convertLineToGray(uint8_t* pPixels, uint8_t* grayLine, int width, int pixel
|
||||
|
||||
case PNG_PIXEL_TRUECOLOR_ALPHA:
|
||||
for (int x = 0; x < width; x++) {
|
||||
uint8_t* p = &pPixels[x * 4];
|
||||
const uint8_t* p = &pPixels[x * 4];
|
||||
uint8_t gray = (uint8_t)((p[0] * 77 + p[1] * 150 + p[2] * 29) >> 8);
|
||||
uint8_t alpha = p[3];
|
||||
grayLine[x] = (uint8_t)((gray * alpha + 255 * (255 - alpha)) / 255);
|
||||
@@ -172,74 +209,83 @@ int pngDrawCallback(PNGDRAW* pDraw) {
|
||||
int srcY = pDraw->y;
|
||||
int srcWidth = ctx->srcWidth;
|
||||
|
||||
// Calculate destination Y with scaling
|
||||
int dstY = (int)(srcY * ctx->scale);
|
||||
// Map source rows with the exact output-height ratio. During downscaling,
|
||||
// multiple source rows can select the same output row; during upscaling, one
|
||||
// source row must be repeated across every output row in its range. Emitting
|
||||
// only the first row of an upscale leaves zero-filled (black) gaps in the
|
||||
// streamed pixel cache.
|
||||
int firstDstY = (srcY * ctx->dstHeight) / ctx->srcHeight;
|
||||
int endDstY = firstDstY + 1;
|
||||
if (ctx->dstHeight > ctx->srcHeight) {
|
||||
endDstY = ((srcY + 1) * ctx->dstHeight) / ctx->srcHeight;
|
||||
}
|
||||
|
||||
// Skip if we already rendered this destination row (multiple source rows map to same dest)
|
||||
if (dstY == ctx->lastDstY) return 1;
|
||||
ctx->lastDstY = dstY;
|
||||
|
||||
// Check bounds
|
||||
if (dstY >= ctx->dstHeight) return 1;
|
||||
|
||||
int outY = ctx->config->y + dstY;
|
||||
if (outY >= ctx->screenHeight) return 1;
|
||||
if (firstDstY <= ctx->lastDstY) firstDstY = ctx->lastDstY + 1;
|
||||
if (firstDstY >= endDstY || firstDstY >= ctx->dstHeight) return 1;
|
||||
if (endDstY > ctx->dstHeight) endDstY = ctx->dstHeight;
|
||||
|
||||
// Convert entire source line to grayscale (improves cache locality)
|
||||
convertLineToGray(pDraw->pPixels, ctx->grayLineBuffer, srcWidth, pDraw->iPixelType, pDraw->pPalette,
|
||||
convertLineToGray(pDraw->pPixels, ctx->grayLineBuffer, srcWidth, pDraw->iPixelType, pDraw->iBpp, pDraw->pPalette,
|
||||
pDraw->iHasAlpha);
|
||||
|
||||
// Render scaled row using Bresenham-style integer stepping (no floating-point division)
|
||||
// Render scaled rows using Bresenham-style integer stepping (no floating-point division)
|
||||
int dstWidth = ctx->dstWidth;
|
||||
int outXBase = ctx->config->x;
|
||||
int screenWidth = ctx->screenWidth;
|
||||
bool useDithering = ctx->config->useDithering;
|
||||
bool caching = ctx->caching;
|
||||
|
||||
// Pre-compute orientation and render-mode state once per row
|
||||
// Pre-compute orientation and render-mode state once per callback.
|
||||
DirectPixelWriter pw;
|
||||
pw.init(*ctx->renderer);
|
||||
pw.beginRow(outY);
|
||||
|
||||
// The cache streams to disk one row at a time. Flushing rows below this one
|
||||
// (PNGdec delivers scanlines top to bottom) repositions the single-row band.
|
||||
// A flush failure stops caching for the rest of the decode so we never write
|
||||
// past the band buffer; finalize() then drops the partial file.
|
||||
DirectCacheWriter cw;
|
||||
if (caching) {
|
||||
if (!ctx->cache.advanceTo(dstY)) {
|
||||
caching = false;
|
||||
ctx->caching = false;
|
||||
} else {
|
||||
cw.init(ctx->cache.buffer, ctx->cache.bytesPerRow, ctx->cache.bandRows, ctx->cache.originX);
|
||||
cw.beginRow(outY, ctx->config->y + ctx->cache.bandStart);
|
||||
}
|
||||
}
|
||||
for (int dstY = firstDstY; dstY < endDstY; dstY++) {
|
||||
ctx->lastDstY = dstY;
|
||||
int outY = ctx->config->y + dstY;
|
||||
if (outY >= ctx->screenHeight) continue;
|
||||
|
||||
int srcX = 0;
|
||||
int error = 0;
|
||||
pw.beginRow(outY);
|
||||
|
||||
for (int dstX = 0; dstX < dstWidth; dstX++) {
|
||||
int outX = outXBase + dstX;
|
||||
if (outX < screenWidth) {
|
||||
uint8_t gray = ctx->grayLineBuffer[srcX];
|
||||
|
||||
uint8_t ditheredGray;
|
||||
if (useDithering) {
|
||||
ditheredGray = applyBayerDither4Level(gray, outX, outY);
|
||||
// The cache streams to disk one row at a time. Flushing rows below this one
|
||||
// (PNGdec delivers scanlines top to bottom) repositions the single-row band.
|
||||
// A flush failure stops caching for the rest of the decode so we never write
|
||||
// past the band buffer; finalize() then drops the partial file.
|
||||
bool caching = ctx->caching;
|
||||
DirectCacheWriter cw;
|
||||
if (caching) {
|
||||
if (!ctx->cache.advanceTo(dstY)) {
|
||||
caching = false;
|
||||
ctx->caching = false;
|
||||
} else {
|
||||
ditheredGray = gray / 85;
|
||||
if (ditheredGray > 3) ditheredGray = 3;
|
||||
cw.init(ctx->cache.buffer, ctx->cache.bytesPerRow, ctx->cache.bandRows, ctx->cache.originX);
|
||||
cw.beginRow(outY, ctx->config->y + ctx->cache.bandStart);
|
||||
}
|
||||
pw.writePixel(outX, ditheredGray);
|
||||
if (caching) cw.writePixel(outX, ditheredGray);
|
||||
}
|
||||
|
||||
// Bresenham-style stepping: advance srcX based on ratio srcWidth/dstWidth
|
||||
error += srcWidth;
|
||||
while (error >= dstWidth) {
|
||||
error -= dstWidth;
|
||||
srcX++;
|
||||
int srcX = 0;
|
||||
int error = 0;
|
||||
|
||||
for (int dstX = 0; dstX < dstWidth; dstX++) {
|
||||
int outX = outXBase + dstX;
|
||||
if (outX < screenWidth) {
|
||||
uint8_t gray = ctx->grayLineBuffer[srcX];
|
||||
|
||||
uint8_t ditheredGray;
|
||||
if (useDithering) {
|
||||
ditheredGray = applyBayerDither4Level(gray, outX, outY);
|
||||
} else {
|
||||
ditheredGray = gray / 85;
|
||||
if (ditheredGray > 3) ditheredGray = 3;
|
||||
}
|
||||
pw.writePixel(outX, ditheredGray);
|
||||
if (caching) cw.writePixel(outX, ditheredGray);
|
||||
}
|
||||
|
||||
// Bresenham-style stepping: advance srcX based on ratio srcWidth/dstWidth
|
||||
error += srcWidth;
|
||||
while (error >= dstWidth) {
|
||||
error -= dstWidth;
|
||||
srcX++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -332,30 +378,44 @@ bool PngToFramebufferConverter::decodeToFramebuffer(const std::string& imagePath
|
||||
}
|
||||
ctx.lastDstY = -1; // Reset row tracking
|
||||
|
||||
LOG_DBG("PNG", "PNG %dx%d -> %dx%d (scale %.2f), bpp: %d", ctx.srcWidth, ctx.srcHeight, ctx.dstWidth, ctx.dstHeight,
|
||||
ctx.scale, png->getBpp());
|
||||
|
||||
const int pixelType = png->getPixelType();
|
||||
const int requiredInternal = requiredPngInternalBufferBytes(ctx.srcWidth, pixelType);
|
||||
const int bitsPerSample = png->getBpp();
|
||||
LOG_DBG("PNG", "PNG %dx%d -> %dx%d (scale %.2f), type: %d, bpp: %d", ctx.srcWidth, ctx.srcHeight, ctx.dstWidth,
|
||||
ctx.dstHeight, ctx.scale, pixelType, bitsPerSample);
|
||||
|
||||
const int requiredInternal = requiredPngInternalBufferBytes(ctx.srcWidth, pixelType, bitsPerSample);
|
||||
if (requiredInternal > PNG_MAX_BUFFERED_PIXELS) {
|
||||
LOG_ERR("PNG",
|
||||
"PNG row buffer too small: need %d bytes for width=%d type=%d, configured PNG_MAX_BUFFERED_PIXELS=%d",
|
||||
requiredInternal, ctx.srcWidth, pixelType, PNG_MAX_BUFFERED_PIXELS);
|
||||
LOG_ERR(
|
||||
"PNG",
|
||||
"PNG row buffer too small: need %d bytes for width=%d type=%d bpp=%d, configured PNG_MAX_BUFFERED_PIXELS=%d",
|
||||
requiredInternal, ctx.srcWidth, pixelType, bitsPerSample, PNG_MAX_BUFFERED_PIXELS);
|
||||
LOG_ERR("PNG", "Aborting decode to avoid PNGdec internal buffer overflow");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (png->getBpp() != 8) {
|
||||
warnUnsupportedFeature("bit depth (" + std::to_string(png->getBpp()) + "bpp)", imagePath);
|
||||
if (!isSupportedBitDepth(pixelType, bitsPerSample)) {
|
||||
warnUnsupportedFeature(
|
||||
"bit depth (" + std::to_string(bitsPerSample) + "bpp) for pixel type " + std::to_string(pixelType), imagePath);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Allocate grayscale line buffer on demand (~3.2 KB) - freed after decode
|
||||
const size_t grayBufSize = PNG_MAX_BUFFERED_PIXELS / 2;
|
||||
ctx.grayLineBuffer = static_cast<uint8_t*>(malloc(grayBufSize));
|
||||
if (!ctx.grayLineBuffer) {
|
||||
// The converter expands each source row to 8-bit grayscale before dithering,
|
||||
// so this scratch buffer is sized by source pixels even when PNGdec reads a
|
||||
// packed 1/2/4-bit row internally.
|
||||
constexpr size_t MAX_GRAY_LINE_BUFFER_BYTES = PNG_MAX_BUFFERED_PIXELS / 2;
|
||||
const size_t grayBufSize = static_cast<size_t>(ctx.srcWidth);
|
||||
if (grayBufSize > MAX_GRAY_LINE_BUFFER_BYTES) {
|
||||
LOG_ERR("PNG", "Expanded gray row too wide: need %u bytes for width=%d, max=%u", static_cast<unsigned>(grayBufSize),
|
||||
ctx.srcWidth, static_cast<unsigned>(MAX_GRAY_LINE_BUFFER_BYTES));
|
||||
return false;
|
||||
}
|
||||
|
||||
auto grayLineBuffer = makeUniqueNoThrow<uint8_t[]>(grayBufSize);
|
||||
if (!grayLineBuffer) {
|
||||
LOG_ERR("PNG", "Failed to allocate gray line buffer");
|
||||
return false;
|
||||
}
|
||||
ctx.grayLineBuffer = grayLineBuffer.get();
|
||||
|
||||
// Stream the pixel cache to disk. PNGdec delivers source scanlines top to
|
||||
// bottom and we emit at most one (downscaled) output row per callback, so the
|
||||
@@ -375,7 +435,6 @@ bool PngToFramebufferConverter::decodeToFramebuffer(const std::string& imagePath
|
||||
rc = png->decode(&ctx, 0);
|
||||
unsigned long decodeTime = millis() - decodeStart;
|
||||
|
||||
free(ctx.grayLineBuffer);
|
||||
ctx.grayLineBuffer = nullptr;
|
||||
|
||||
if (rc != PNG_SUCCESS) {
|
||||
|
||||
@@ -803,6 +803,9 @@ bool CssParser::loadFromCache() {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Size the bucket array up front to avoid incremental rehashes while loading rules.
|
||||
rulesBySelector_.reserve(ruleCount);
|
||||
|
||||
auto hasRemainingBytes = [&file](const size_t neededBytes) -> bool {
|
||||
return static_cast<size_t>(file.available()) >= neededBytes;
|
||||
};
|
||||
|
||||
@@ -24,7 +24,7 @@ struct Iso639Mapping {
|
||||
};
|
||||
static constexpr Iso639Mapping kIso639Mappings[] = {{"eng", "en"}, {"fra", "fr"}, {"fre", "fr"}, {"deu", "de"},
|
||||
{"ger", "de"}, {"rus", "ru"}, {"spa", "es"}, {"ita", "it"},
|
||||
{"ukr", "uk"}, {"swe", "sv"}};
|
||||
{"ukr", "uk"}, {"swe", "sv"}, {"fin", "fi"}};
|
||||
|
||||
// Maps a BCP-47 or ISO 639-2 language tag to a language-specific hyphenator.
|
||||
const LanguageHyphenator* hyphenatorForLanguage(const std::string& langTag) {
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
#include "generated/hyph-de.trie.h"
|
||||
#include "generated/hyph-en.trie.h"
|
||||
#include "generated/hyph-es.trie.h"
|
||||
#include "generated/hyph-fi.trie.h"
|
||||
#include "generated/hyph-fr.trie.h"
|
||||
#include "generated/hyph-it.trie.h"
|
||||
#include "generated/hyph-pl.trie.h"
|
||||
@@ -26,8 +27,9 @@ LanguageHyphenator italianHyphenator(it_patterns, isLatinLetter, toLowerLatin);
|
||||
LanguageHyphenator swedishHyphenator(sv_patterns, isLatinLetter, toLowerLatin);
|
||||
LanguageHyphenator ukrainianHyphenator(uk_patterns, isCyrillicLetter, toLowerCyrillic);
|
||||
LanguageHyphenator polishHyphenator(pl_patterns, isLatinLetter, toLowerLatin);
|
||||
LanguageHyphenator finnishHyphenator(fi_patterns, isLatinLetter, toLowerLatin);
|
||||
|
||||
using EntryArray = std::array<LanguageEntry, 9>;
|
||||
using EntryArray = std::array<LanguageEntry, 10>;
|
||||
|
||||
const EntryArray& entries() {
|
||||
static const EntryArray kEntries = {{{"english", "en", &englishHyphenator},
|
||||
@@ -38,7 +40,8 @@ const EntryArray& entries() {
|
||||
{"italian", "it", &italianHyphenator},
|
||||
{"polish", "pl", &polishHyphenator},
|
||||
{"swedish", "sv", &swedishHyphenator},
|
||||
{"ukrainian", "uk", &ukrainianHyphenator}}};
|
||||
{"ukrainian", "uk", &ukrainianHyphenator},
|
||||
{"finnish", "fi", &finnishHyphenator}}};
|
||||
return kEntries;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
|
||||
#include "Epub/hyphenation/SerializedHyphenationTrie.h"
|
||||
|
||||
// Auto-generated by generate_hyphenation_trie.py. Do not edit manually.
|
||||
alignas(4) constexpr uint8_t fi_trie_data[] = {
|
||||
0x01, 0x01, 0x16, 0x0B, 0x0C, 0x17, 0x0C, 0x15, 0x0C, 0x0B, 0x16, 0x29, 0x2B, 0x20, 0x1F, 0x0C,
|
||||
0x33, 0x02, 0x0B, 0x02, 0x0B, 0x0C, 0x01, 0x0C, 0x34, 0x0B, 0x2A, 0x0B, 0x2A, 0x0B, 0x0C, 0x20,
|
||||
0x0B, 0x21, 0xA0, 0x00, 0x41, 0xA0, 0x02, 0x51, 0x21, 0x69, 0xFD, 0x21, 0x6C, 0xFD, 0xA1, 0x00,
|
||||
0x41, 0x62, 0xFD, 0xA0, 0x01, 0xA2, 0xA1, 0x00, 0x81, 0x6F, 0xFD, 0xA3, 0x00, 0x81, 0x69, 0x6F,
|
||||
0x75, 0xF8, 0xF8, 0xF8, 0x28, 0x61, 0x65, 0x69, 0x6F, 0x75, 0x79, 0x6C, 0x72, 0xDE, 0xDE, 0xEA,
|
||||
0xDE, 0xDE, 0xDE, 0xF2, 0xF7, 0x22, 0xA4, 0xB6, 0xCD, 0xCD, 0xA1, 0x00, 0x81, 0x61, 0xD9, 0x28,
|
||||
0x61, 0x65, 0x69, 0x6F, 0x75, 0x79, 0xC3, 0x72, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xF6, 0xFB,
|
||||
0xA2, 0x00, 0x81, 0x61, 0x65, 0xC3, 0xC3, 0x48, 0x61, 0x65, 0x69, 0x6F, 0x75, 0x79, 0x6C, 0x72,
|
||||
0xFF, 0xAB, 0xFF, 0xAB, 0xFF, 0xAB, 0xFF, 0xAB, 0xFF, 0xAB, 0xFF, 0xAB, 0xFF, 0xE3, 0xFF, 0xF9,
|
||||
0x49, 0x61, 0x65, 0x69, 0x6F, 0x75, 0x79, 0xC3, 0x6C, 0x72, 0xFF, 0x92, 0xFF, 0x92, 0xFF, 0x92,
|
||||
0xFF, 0x92, 0xFF, 0x92, 0xFF, 0x92, 0xFF, 0xC5, 0xFF, 0xA6, 0xFF, 0xCA, 0x47, 0x61, 0x65, 0x69,
|
||||
0x6F, 0x75, 0x79, 0xC3, 0xFF, 0x76, 0xFF, 0x76, 0xFF, 0x76, 0xFF, 0x76, 0xFF, 0x76, 0xFF, 0x76,
|
||||
0xFF, 0xA9, 0xA0, 0x00, 0xF1, 0x21, 0x73, 0xFD, 0xA1, 0x00, 0x41, 0x75, 0xFD, 0xA0, 0x00, 0x81,
|
||||
0x43, 0x61, 0x65, 0x69, 0xFF, 0x63, 0xFF, 0x63, 0xFF, 0x63, 0xC1, 0x01, 0xA2, 0x61, 0xFF, 0x59,
|
||||
0x4A, 0x61, 0x65, 0x69, 0x6F, 0x75, 0x79, 0xC3, 0x6C, 0x72, 0x76, 0xFF, 0x42, 0xFF, 0xE8, 0xFF,
|
||||
0x42, 0xFF, 0x42, 0xFF, 0x42, 0xFF, 0x42, 0xFF, 0x75, 0xFF, 0xED, 0xFF, 0xF0, 0xFF, 0xFA, 0xA1,
|
||||
0x00, 0x41, 0x73, 0xCE, 0x47, 0x61, 0x65, 0x69, 0x6F, 0x75, 0x79, 0xC3, 0xFF, 0xFB, 0xFF, 0x1E,
|
||||
0xFF, 0x1E, 0xFF, 0x1E, 0xFF, 0x1E, 0xFF, 0x1E, 0xFF, 0x51, 0xA0, 0x01, 0x52, 0x21, 0x6F, 0xFD,
|
||||
0x22, 0x74, 0x6E, 0xFD, 0xFD, 0xA0, 0x01, 0x73, 0x21, 0x6E, 0xFD, 0x22, 0x61, 0x6F, 0xFD, 0xFA,
|
||||
0x21, 0x61, 0xEA, 0x21, 0x6B, 0xFD, 0x21, 0x65, 0xF2, 0xA4, 0x00, 0x41, 0x6E, 0x6A, 0x69, 0x6C,
|
||||
0xE7, 0xF2, 0xFA, 0xFD, 0x21, 0x73, 0xE1, 0x21, 0x75, 0xFD, 0xA1, 0x00, 0x41, 0x64, 0xFD, 0x21,
|
||||
0x74, 0xD6, 0x21, 0x73, 0xFD, 0x22, 0x65, 0x69, 0xFA, 0xFD, 0x21, 0x61, 0xCB, 0x21, 0x6E, 0xBD,
|
||||
0x22, 0x74, 0x6F, 0xBD, 0xFD, 0x21, 0x69, 0xC0, 0x21, 0x61, 0xFD, 0xA4, 0x00, 0x41, 0x70, 0x73,
|
||||
0x74, 0x6D, 0xEA, 0xEF, 0xF5, 0xFD, 0x21, 0x69, 0xD9, 0xA1, 0x00, 0x41, 0x6C, 0xFD, 0x47, 0x61,
|
||||
0x65, 0x69, 0x6F, 0x75, 0x79, 0xC3, 0xFF, 0xBB, 0xFF, 0xCC, 0xFE, 0xA4, 0xFF, 0xED, 0xFE, 0xA4,
|
||||
0xFF, 0xFB, 0xFE, 0xD7, 0xA0, 0x01, 0x41, 0x21, 0x73, 0xFD, 0x21, 0x75, 0xFD, 0xA1, 0x00, 0x41,
|
||||
0x72, 0xFD, 0x49, 0x61, 0x65, 0x69, 0x6F, 0x75, 0x79, 0xC3, 0x6C, 0x72, 0xFE, 0x80, 0xFF, 0xFB,
|
||||
0xFE, 0x80, 0xFE, 0x80, 0xFE, 0x80, 0xFE, 0x80, 0xFE, 0xB3, 0xFF, 0x2B, 0xFE, 0x94, 0x21, 0x61,
|
||||
0xDC, 0x48, 0x61, 0x65, 0x69, 0x6F, 0x75, 0x79, 0xC3, 0x74, 0xFF, 0x3E, 0xFE, 0x61, 0xFE, 0x61,
|
||||
0xFE, 0x61, 0xFE, 0x61, 0xFE, 0x61, 0xFE, 0x94, 0xFF, 0xFD, 0x42, 0x69, 0x65, 0xFF, 0x80, 0xFF,
|
||||
0x40, 0x42, 0x6F, 0x65, 0xFF, 0x84, 0xFF, 0x47, 0x41, 0x75, 0xFF, 0x32, 0x21, 0x74, 0xFC, 0x42,
|
||||
0x61, 0x6F, 0xFF, 0xFD, 0xFF, 0x36, 0xA4, 0x00, 0x41, 0x73, 0x6C, 0x6A, 0x70, 0xE4, 0xEB, 0xF9,
|
||||
0xF2, 0x41, 0x79, 0xFF, 0x24, 0x21, 0x74, 0xFC, 0x21, 0x69, 0xFD, 0xA1, 0x00, 0x41, 0x73, 0xFD,
|
||||
0x42, 0x2E, 0x6E, 0xFF, 0x15, 0xFF, 0x15, 0x21, 0x61, 0xF9, 0x21, 0x65, 0xFD, 0xA1, 0x00, 0x41,
|
||||
0x64, 0xFD, 0x41, 0x65, 0xFE, 0xF8, 0x21, 0x6A, 0xFC, 0x42, 0x6B, 0x74, 0xFE, 0xFC, 0xFE, 0xFC,
|
||||
0x21, 0x73, 0xF9, 0x21, 0x69, 0xFD, 0xC3, 0x00, 0x41, 0x68, 0x70, 0x73, 0xFF, 0xF0, 0xFF, 0xFD,
|
||||
0xFF, 0x24, 0x41, 0x74, 0xFF, 0x23, 0xC2, 0x00, 0x41, 0x72, 0x68, 0xFF, 0x30, 0xFF, 0xFC, 0xA0,
|
||||
0x00, 0x52, 0x21, 0x72, 0xFD, 0x21, 0x69, 0xFA, 0x21, 0x6C, 0xFD, 0xA0, 0x00, 0x61, 0x21, 0x68,
|
||||
0xFD, 0x4A, 0x61, 0x65, 0x69, 0x6F, 0x75, 0x79, 0xC3, 0x74, 0x70, 0x63, 0xFF, 0x95, 0xFF, 0xAA,
|
||||
0xFF, 0xBC, 0xFF, 0xD5, 0xFD, 0xC1, 0xFF, 0xE5, 0xFD, 0xF4, 0xFF, 0xF1, 0xFF, 0xF7, 0xFF, 0xFD,
|
||||
0x48, 0x61, 0x65, 0x69, 0x6F, 0x75, 0x79, 0xC3, 0x73, 0xFD, 0xA2, 0xFD, 0xA2, 0xFD, 0xA2, 0xFD,
|
||||
0xA2, 0xFD, 0xA2, 0xFD, 0xA2, 0xFD, 0xD5, 0xFF, 0xDE, 0xA0, 0x00, 0x92, 0xA0, 0x00, 0xB2, 0xA0,
|
||||
0x01, 0x01, 0xC3, 0x00, 0x61, 0x69, 0x65, 0x79, 0xFE, 0x20, 0xFE, 0x20, 0xFF, 0xFD, 0x22, 0xA4,
|
||||
0xB6, 0xF4, 0xAD, 0x25, 0x79, 0x61, 0x6F, 0x75, 0xC3, 0xA8, 0xE6, 0xE6, 0xE9, 0xFB, 0x42, 0xB6,
|
||||
0xA4, 0xFF, 0x9D, 0xFF, 0x9D, 0x46, 0x79, 0x61, 0x6F, 0x75, 0xC3, 0x65, 0xFF, 0x96, 0xFF, 0xD4,
|
||||
0xFF, 0xD4, 0xFF, 0xD7, 0xFF, 0xF9, 0xFF, 0xD7, 0x22, 0xA4, 0xB6, 0xDB, 0xED, 0xA0, 0x00, 0x72,
|
||||
0xA0, 0x00, 0x71, 0x21, 0xA4, 0xFD, 0x21, 0xC3, 0xFD, 0x21, 0xA4, 0xFD, 0x21, 0x69, 0xF4, 0xA0,
|
||||
0x01, 0x22, 0x21, 0x70, 0xFD, 0x21, 0x6F, 0xFD, 0x21, 0x69, 0xFD, 0x26, 0x61, 0x6F, 0x75, 0xC3,
|
||||
0x65, 0x6C, 0xE2, 0xE2, 0xE2, 0xEE, 0xF1, 0xFD, 0x22, 0xA4, 0xB6, 0xD8, 0xD8, 0x21, 0x61, 0xD3,
|
||||
0xA0, 0x00, 0xB1, 0x24, 0x75, 0x69, 0x65, 0x6F, 0xCD, 0xCD, 0xFD, 0xFD, 0x24, 0x61, 0x65, 0x6F,
|
||||
0x75, 0xF4, 0xF4, 0xF4, 0xF4, 0x25, 0x79, 0xC3, 0x61, 0x75, 0x69, 0xBB, 0xE3, 0xE8, 0xEE, 0xF7,
|
||||
0xA0, 0x00, 0xD2, 0x22, 0xA4, 0xB6, 0xFD, 0xFD, 0x44, 0x61, 0x65, 0x6F, 0x69, 0xFF, 0x64, 0xFF,
|
||||
0x64, 0xFF, 0x64, 0xFF, 0x64, 0x42, 0x65, 0x61, 0xFF, 0x9B, 0xFF, 0xCB, 0x21, 0x65, 0xC4, 0x22,
|
||||
0x61, 0x75, 0xC1, 0xC1, 0xA0, 0x02, 0x32, 0x21, 0x73, 0xFD, 0x21, 0x6F, 0xFD, 0x49, 0x79, 0xC3,
|
||||
0x75, 0x61, 0x65, 0x69, 0x6F, 0x73, 0x6C, 0xFF, 0x80, 0xFF, 0xD6, 0xFF, 0xDB, 0xFF, 0xB0, 0xFF,
|
||||
0xE8, 0xFF, 0xEF, 0xFF, 0xF2, 0xFD, 0x70, 0xFF, 0xFD, 0x44, 0x69, 0x65, 0x6F, 0x75, 0xFF, 0x23,
|
||||
0xFF, 0x23, 0xFF, 0x23, 0xFF, 0x23, 0x43, 0x75, 0x61, 0x65, 0xFF, 0x5A, 0xFF, 0x8A, 0xFF, 0x8A,
|
||||
0x41, 0x76, 0xFF, 0x5F, 0x21, 0x61, 0xFC, 0xA0, 0x01, 0xC2, 0x21, 0x73, 0xFD, 0x21, 0x69, 0xFD,
|
||||
0x21, 0x65, 0xFD, 0x43, 0x69, 0x6F, 0x6B, 0xFF, 0xF1, 0xFD, 0xF7, 0xFF, 0xFD, 0xA0, 0x01, 0xA4,
|
||||
0x21, 0x73, 0xFD, 0x21, 0x61, 0xFD, 0x43, 0x6E, 0x74, 0x6B, 0xFC, 0x7D, 0xFC, 0x7D, 0xFF, 0xFD,
|
||||
0x41, 0x69, 0xFC, 0x73, 0x22, 0x61, 0x6F, 0xF2, 0xFC, 0x21, 0x69, 0xFB, 0x48, 0xC3, 0x61, 0x75,
|
||||
0x65, 0x6F, 0x69, 0x6C, 0x73, 0xFF, 0x3C, 0xFF, 0xAD, 0xFF, 0xBA, 0xFF, 0x20, 0xFF, 0x20, 0xFF,
|
||||
0x50, 0xFF, 0xD7, 0xFF, 0xFD, 0x44, 0x61, 0x69, 0x75, 0x79, 0xFE, 0xB7, 0xFE, 0xB7, 0xFE, 0xB7,
|
||||
0xFE, 0xB7, 0x42, 0x61, 0x69, 0xFE, 0xEE, 0xFE, 0xEE, 0x42, 0x75, 0x61, 0xFE, 0xE7, 0xFF, 0x17,
|
||||
0x42, 0xA4, 0xB6, 0xFE, 0xE6, 0xFF, 0x30, 0x24, 0x65, 0x61, 0x75, 0xC3, 0xDE, 0xEB, 0xF2, 0xF9,
|
||||
0x43, 0x61, 0x65, 0x6F, 0xFE, 0x8C, 0xFE, 0x8C, 0xFE, 0x8C, 0x42, 0x61, 0x75, 0xFE, 0xC6, 0xFE,
|
||||
0xC6, 0x44, 0x75, 0x61, 0x65, 0x6F, 0xFE, 0xBF, 0xFE, 0xEF, 0xFE, 0xEF, 0xFE, 0xEF, 0x41, 0xB6,
|
||||
0xFE, 0xB2, 0x21, 0xC3, 0xFC, 0x42, 0xA4, 0xB6, 0xFE, 0xB1, 0xFF, 0xFD, 0x43, 0x61, 0x6F, 0x79,
|
||||
0xFE, 0xD4, 0xFE, 0xD4, 0xFE, 0xD4, 0x42, 0x61, 0x65, 0xFE, 0x56, 0xFE, 0x56, 0x26, 0x69, 0x61,
|
||||
0x75, 0xC3, 0x65, 0x6F, 0xC3, 0xCD, 0xD4, 0xE8, 0xEF, 0xF9, 0xA0, 0x01, 0x11, 0x21, 0xA4, 0xFD,
|
||||
0xA0, 0x01, 0xE2, 0x21, 0x6E, 0xFD, 0x21, 0x69, 0xFD, 0x21, 0x64, 0xFD, 0xA0, 0x02, 0x03, 0x21,
|
||||
0x61, 0xFD, 0x21, 0x72, 0xFD, 0x21, 0x75, 0xFD, 0x21, 0x75, 0xFD, 0x23, 0xC3, 0x79, 0x73, 0xE2,
|
||||
0xEE, 0xFD, 0x41, 0x72, 0xFD, 0xD9, 0x42, 0x6C, 0x68, 0xFC, 0x47, 0xFF, 0xFC, 0xC1, 0x00, 0x81,
|
||||
0x69, 0xFB, 0xA6, 0x21, 0x76, 0xFA, 0x59, 0x62, 0x64, 0x66, 0x67, 0x68, 0x6A, 0x6B, 0x6C, 0x6D,
|
||||
0x6E, 0x70, 0x72, 0x73, 0x74, 0x76, 0xC3, 0x79, 0x6F, 0x75, 0x61, 0x65, 0x69, 0x2E, 0x63, 0x71,
|
||||
0xFB, 0xAE, 0xFB, 0xC9, 0xFB, 0xE1, 0xFB, 0xFA, 0xFC, 0x16, 0xFC, 0x16, 0xFC, 0x4A, 0xFC, 0x6E,
|
||||
0xFC, 0x16, 0xFC, 0xE8, 0xFD, 0x0C, 0xFD, 0x2B, 0xFD, 0xCB, 0xFD, 0xEA, 0xFC, 0x16, 0xFE, 0x42,
|
||||
0xFE, 0x65, 0xFE, 0x8F, 0xFE, 0xC7, 0xFF, 0x36, 0xFF, 0x71, 0xFF, 0xB7, 0xFF, 0xE5, 0xFF, 0xF0,
|
||||
0xFF, 0xFD,
|
||||
};
|
||||
|
||||
constexpr SerializedHyphenationPatterns fi_patterns = {
|
||||
0x496u,
|
||||
fi_trie_data,
|
||||
sizeof(fi_trie_data),
|
||||
};
|
||||
@@ -35,7 +35,7 @@ constexpr const char* BOLD_TAGS[] = {"b", "strong"};
|
||||
constexpr const char* ITALIC_TAGS[] = {"i", "em"};
|
||||
constexpr const char* UNDERLINE_TAGS[] = {"u", "ins"};
|
||||
constexpr const char* LINETHROUGH_TAGS[] = {"del", "s", "strike"};
|
||||
constexpr const char* IMAGE_TAGS[] = {"img"};
|
||||
constexpr const char* IMAGE_TAGS[] = {"img", "image"};
|
||||
constexpr const char* SKIP_TAGS[] = {"head"};
|
||||
|
||||
bool isWhitespace(const char c) { return c == ' ' || c == '\r' || c == '\n' || c == '\t'; }
|
||||
@@ -225,6 +225,7 @@ void ChapterHtmlSlimParser::flushPartWordBuffer() {
|
||||
currentTextBlock->addWord(partWordBuffer, fontStyle, false, nextWordContinues);
|
||||
partWordBufferIndex = 0;
|
||||
nextWordContinues = false;
|
||||
listItemBulletOnly = false;
|
||||
}
|
||||
|
||||
// start a new text block if needed
|
||||
@@ -254,6 +255,17 @@ void ChapterHtmlSlimParser::startNewTextBlock(const BlockStyle& blockStyle) {
|
||||
return;
|
||||
}
|
||||
|
||||
// <li> added a bullet as the first word, making the block non-empty. When a nested
|
||||
// block-level child (<p>, <div>, etc.) opens, reuse the block instead of flushing
|
||||
// the bullet to its own line. The bullet stays inline with the child's text.
|
||||
if (listItemBulletOnly) {
|
||||
const auto style = currentTextBlock->getBlockStyle();
|
||||
currentTextBlock->setBlockStyle(style.getCombinedBlockStyle(blockStyle, BlockStyle::CombineAxis::Vertical));
|
||||
listItemBulletOnly = false;
|
||||
flushPendingAnchor();
|
||||
return;
|
||||
}
|
||||
|
||||
makePages();
|
||||
}
|
||||
// If the pending anchor is a TOC chapter boundary, force a page break after the previous
|
||||
@@ -261,6 +273,7 @@ void ChapterHtmlSlimParser::startNewTextBlock(const BlockStyle& blockStyle) {
|
||||
flushPendingAnchor();
|
||||
currentTextBlock.reset(new ParsedText(extraParagraphSpacing, hyphenationEnabled, focusReadingEnabled, blockStyle));
|
||||
wordsExtractedInBlock = 0;
|
||||
listItemBulletOnly = false;
|
||||
}
|
||||
|
||||
void ChapterHtmlSlimParser::emitHorizontalRule(const BlockStyle& blockStyle) {
|
||||
@@ -495,11 +508,18 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char*
|
||||
for (int i = 0; atts[i]; i += 2) {
|
||||
if (strcmp(atts[i], "src") == 0) {
|
||||
src = atts[i + 1];
|
||||
} else if (src.empty() && (strcmp(atts[i], "href") == 0 || strcmp(atts[i], "xlink:href") == 0)) {
|
||||
src = atts[i + 1];
|
||||
} else if (strcmp(atts[i], "alt") == 0) {
|
||||
alt = atts[i + 1];
|
||||
}
|
||||
}
|
||||
|
||||
const size_t fragmentPos = src.find('#');
|
||||
if (fragmentPos != std::string::npos) {
|
||||
src.resize(fragmentPos);
|
||||
}
|
||||
|
||||
// imageRendering: 0=display, 1=placeholder (alt text only), 2=suppress entirely
|
||||
if (self->imageRendering == 2) {
|
||||
self->skipUntilDepth = self->depth;
|
||||
@@ -507,19 +527,6 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char*
|
||||
return;
|
||||
}
|
||||
|
||||
// Skip image if CSS display:none
|
||||
if (self->cssParser) {
|
||||
CssStyle imgDisplayStyle = self->cssParser->resolveStyle("img", classAttr);
|
||||
if (!styleAttr.empty()) {
|
||||
imgDisplayStyle.applyOver(CssParser::parseInlineStyle(styleAttr));
|
||||
}
|
||||
if (imgDisplayStyle.hasDisplay() && imgDisplayStyle.display == CssDisplay::None) {
|
||||
self->skipUntilDepth = self->depth;
|
||||
self->depth += 1;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (!src.empty() && self->imageRendering != 1) {
|
||||
LOG_DBG("EHP", "Found image: src=%s", src.c_str());
|
||||
|
||||
@@ -543,24 +550,28 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char*
|
||||
extractSuccess = self->epub->readItemContentsToStream(resolvedPath, cachedImageFile, 4096);
|
||||
cachedImageFile.flush();
|
||||
cachedImageFile.close();
|
||||
delay(50); // Give SD card time to sync
|
||||
}
|
||||
|
||||
if (extractSuccess) {
|
||||
// Get image dimensions
|
||||
// Get image dimensions, retrying to absorb SD-card sync latency on slow
|
||||
// cards. Replaces a blanket delay(50) that cost ~50ms on every image, and
|
||||
// closes the silent-drop bug where a single getDimensions failure was fatal.
|
||||
ImageDimensions dims = {0, 0};
|
||||
ImageToFramebufferDecoder* decoder = ImageDecoderFactory::getDecoder(cachedImagePath);
|
||||
if (decoder && decoder->getDimensions(cachedImagePath, dims)) {
|
||||
bool gotDimensions = false;
|
||||
for (int attempt = 0; attempt < 3 && !gotDimensions; attempt++) {
|
||||
if (attempt > 0) {
|
||||
delay(50); // Give a slow SD card time to finish syncing before retrying
|
||||
}
|
||||
gotDimensions = decoder && decoder->getDimensions(cachedImagePath, dims);
|
||||
}
|
||||
if (gotDimensions) {
|
||||
LOG_DBG("EHP", "Image dimensions: %dx%d", dims.width, dims.height);
|
||||
|
||||
int displayWidth = 0;
|
||||
int displayHeight = 0;
|
||||
const float emSize = static_cast<float>(self->renderer.getFontAscenderSize(self->fontId));
|
||||
CssStyle imgStyle = self->cssParser ? self->cssParser->resolveStyle("img", classAttr) : CssStyle{};
|
||||
// Merge inline style (e.g. style="height: 2em") so it overrides stylesheet rules
|
||||
if (!styleAttr.empty()) {
|
||||
imgStyle.applyOver(CssParser::parseInlineStyle(styleAttr));
|
||||
}
|
||||
const CssStyle& imgStyle = cssStyle;
|
||||
const bool hasCssHeight = imgStyle.hasImageHeight();
|
||||
const bool hasCssWidth = imgStyle.hasImageWidth();
|
||||
|
||||
@@ -882,6 +893,7 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char*
|
||||
|
||||
if (strcmp(name, "li") == 0) {
|
||||
self->currentTextBlock->addWord("\xe2\x80\xa2", EpdFontFamily::REGULAR);
|
||||
self->listItemBulletOnly = true;
|
||||
}
|
||||
}
|
||||
} else if (matches(name, UNDERLINE_TAGS, std::size(UNDERLINE_TAGS))) {
|
||||
@@ -1288,6 +1300,13 @@ void XMLCALL ChapterHtmlSlimParser::endElement(void* userData, const XML_Char* n
|
||||
}
|
||||
self->blockStyleStack.pop_back();
|
||||
}
|
||||
|
||||
// </li> closes: if the bullet never got inline text (empty <li> or <li> with only
|
||||
// block children that were flushed), clear the flag so the next sibling doesn't
|
||||
// merge into this block.
|
||||
if (strcmp(name, "li") == 0) {
|
||||
self->listItemBulletOnly = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -80,6 +80,7 @@ class ChapterHtmlSlimParser {
|
||||
int tableDepth = 0;
|
||||
int tableRowIndex = 0;
|
||||
int tableColIndex = 0;
|
||||
bool listItemBulletOnly = false; // true when currentTextBlock has only the <li> bullet
|
||||
|
||||
// Anchor-to-page mapping: tracks which page each HTML id attribute lands on
|
||||
int completedPageCount = 0;
|
||||
|
||||
@@ -137,8 +137,10 @@ void XMLCALL ContentOpfParser::startElement(void* userData, const XML_Char* name
|
||||
LOG_ERR("COF", "Couldn't open temp items file for reading. This is probably going to be a fatal error.");
|
||||
}
|
||||
|
||||
// Sort item index for binary search if we have enough items
|
||||
if (self->itemIndex.size() >= LARGE_SPINE_THRESHOLD) {
|
||||
// Sort the (unconditionally-built) item index so every idref lookup uses binary
|
||||
// search. Without this, small/medium manifests fell back to an O(spine × manifest)
|
||||
// linear rescan of .items.bin per itemref (up to ~200ms/item at large scale).
|
||||
if (!self->itemIndex.empty()) {
|
||||
std::sort(self->itemIndex.begin(), self->itemIndex.end(), [](const ItemIndexEntry& a, const ItemIndexEntry& b) {
|
||||
return a.idHash < b.idHash || (a.idHash == b.idHash && a.idLen < b.idLen);
|
||||
});
|
||||
@@ -284,9 +286,8 @@ void XMLCALL ContentOpfParser::startElement(void* userData, const XML_Char* name
|
||||
++it;
|
||||
}
|
||||
} else {
|
||||
// Slow path: linear scan (for small manifests, keeps original behavior)
|
||||
// TODO: This lookup is slow as need to scan through all items each time.
|
||||
// It can take up to 200ms per item when getting to 1500 items.
|
||||
// Fallback linear scan, only reached when the index is empty (no manifest
|
||||
// items). The fast binary-search path above is used for all real manifests.
|
||||
self->tempItemStore.seek(0);
|
||||
std::string itemId;
|
||||
while (self->tempItemStore.available()) {
|
||||
|
||||
@@ -32,7 +32,7 @@ class ContentOpfParser final : public Print {
|
||||
HalFile tempItemStore;
|
||||
std::string coverItemId;
|
||||
|
||||
// Index for fast idref→href lookup (used only for large EPUBs)
|
||||
// Index for fast idref→href lookup (binary search over .items.bin)
|
||||
struct ItemIndexEntry {
|
||||
uint32_t idHash; // FNV-1a hash of itemId
|
||||
uint16_t idLen; // length for collision reduction
|
||||
@@ -41,8 +41,6 @@ class ContentOpfParser final : public Print {
|
||||
std::deque<ItemIndexEntry> itemIndex;
|
||||
bool useItemIndex = false;
|
||||
|
||||
static constexpr uint16_t LARGE_SPINE_THRESHOLD = 400;
|
||||
|
||||
// FNV-1a hash function
|
||||
static uint32_t fnvHash(const std::string& s) {
|
||||
uint32_t hash = 2166136261u;
|
||||
|
||||
Reference in New Issue
Block a user