A couple of fixes

This commit is contained in:
jpirnay
2026-04-30 12:08:30 +02:00
parent 555894edcb
commit 867bd34798
11 changed files with 118 additions and 34 deletions
+66
View File
@@ -149,6 +149,9 @@ void ParsedText::layoutAndExtractLines(
// Apply fixed transforms before any per-line layout work.
applyParagraphIndent();
if (bionicReadingEnabled) {
applyBionicReadingTransform();
}
// Ensure SD card font glyph metrics are loaded before measuring word widths.
// For flash-based fonts isSdCardFont() returns false and this block is skipped
@@ -506,6 +509,69 @@ void ParsedText::applyParagraphIndent() {
}
}
void ParsedText::applyBionicReadingTransform() {
if (words.empty()) {
return;
}
std::vector<std::string> transformedWords;
std::vector<EpdFontFamily::Style> transformedStyles;
std::vector<bool> transformedContinues;
transformedWords.reserve(words.size() * 2);
transformedStyles.reserve(wordStyles.size() * 2);
transformedContinues.reserve(wordContinues.size() * 2);
for (size_t i = 0; i < words.size(); ++i) {
const std::string& word = words[i];
const auto originalStyle = wordStyles[i];
const bool attachToPrevious = wordContinues[i];
const unsigned char* ptr = reinterpret_cast<const unsigned char*>(word.c_str());
int codepointCount = 0;
while (utf8NextCodepoint(&ptr)) {
codepointCount++;
}
if (codepointCount <= 3) {
transformedWords.push_back(word);
transformedStyles.push_back(originalStyle);
transformedContinues.push_back(attachToPrevious);
continue;
}
const int boldPrefixCount = std::max(1, (codepointCount + 1) / 2);
ptr = reinterpret_cast<const unsigned char*>(word.c_str());
const unsigned char* prefixEnd = ptr;
for (int j = 0; j < boldPrefixCount && *prefixEnd; ++j) {
utf8NextCodepoint(&prefixEnd);
}
const size_t prefixByteCount =
static_cast<size_t>(prefixEnd - reinterpret_cast<const unsigned char*>(word.c_str()));
if (prefixByteCount >= word.size()) {
transformedWords.push_back(word);
transformedStyles.push_back(originalStyle);
transformedContinues.push_back(attachToPrevious);
continue;
}
const std::string prefix(word.data(), prefixByteCount);
const std::string suffix(word.data() + prefixByteCount, word.size() - prefixByteCount);
const auto boldStyle = static_cast<EpdFontFamily::Style>(originalStyle | EpdFontFamily::BOLD);
transformedWords.push_back(prefix);
transformedStyles.push_back(boldStyle);
transformedContinues.push_back(attachToPrevious);
transformedWords.push_back(suffix);
transformedStyles.push_back(originalStyle);
transformedContinues.push_back(true);
}
words = std::move(transformedWords);
wordStyles = std::move(transformedStyles);
wordContinues = std::move(transformedContinues);
}
// Builds break indices while opportunistically splitting the word that would overflow the current line.
std::vector<size_t> ParsedText::computeHyphenatedLineBreaks(const GfxRenderer& renderer, const int fontId,
const int pageWidth, std::vector<uint16_t>& wordWidths,
+15 -9
View File
@@ -12,7 +12,7 @@
#include "parsers/ChapterHtmlSlimParser.h"
namespace {
constexpr uint8_t SECTION_FILE_VERSION = 23;
constexpr uint8_t SECTION_FILE_VERSION = 24;
constexpr uint32_t HEADER_SIZE = sizeof(uint8_t) + // SECTION_FILE_VERSION
sizeof(int) + // fontId
sizeof(float) + // lineCompression
@@ -23,6 +23,7 @@ constexpr uint32_t HEADER_SIZE = sizeof(uint8_t) + // SECTION_FILE_VERSION
sizeof(uint16_t) + // pageCount (stored as 16-bit in header)
sizeof(bool) + // hyphenationEnabled
sizeof(bool) + // embeddedStyle
sizeof(bool) + // bionicReadingEnabled
sizeof(uint8_t) + // imageRendering
sizeof(uint32_t) + // page LUT offset
sizeof(uint32_t) + // anchor map offset
@@ -55,7 +56,8 @@ uint32_t Section::onPageComplete(std::unique_ptr<Page> page) {
void Section::writeSectionFileHeader(const int fontId, const float lineCompression, const bool extraParagraphSpacing,
const uint8_t paragraphAlignment, const uint16_t viewportWidth,
const uint16_t viewportHeight, const bool hyphenationEnabled,
const bool embeddedStyle, const uint8_t imageRendering) {
const bool embeddedStyle, const bool bionicReadingEnabled,
const uint8_t imageRendering) {
if (!file) {
LOG_DBG("SCT", "File not open for writing header");
return;
@@ -63,8 +65,8 @@ void Section::writeSectionFileHeader(const int fontId, const float lineCompressi
static_assert(HEADER_SIZE == sizeof(SECTION_FILE_VERSION) + sizeof(fontId) + sizeof(lineCompression) +
sizeof(extraParagraphSpacing) + sizeof(paragraphAlignment) + sizeof(viewportWidth) +
sizeof(viewportHeight) + sizeof(pageCount) + sizeof(hyphenationEnabled) +
sizeof(embeddedStyle) + sizeof(imageRendering) + sizeof(uint32_t) +
sizeof(uint32_t) + sizeof(uint32_t),
sizeof(embeddedStyle) + sizeof(bionicReadingEnabled) + sizeof(imageRendering) +
sizeof(uint32_t) + sizeof(uint32_t) + sizeof(uint32_t),
"Header size mismatch");
serialization::writePod(file, SECTION_FILE_VERSION);
serialization::writePod(file, fontId);
@@ -75,6 +77,7 @@ void Section::writeSectionFileHeader(const int fontId, const float lineCompressi
serialization::writePod(file, viewportHeight);
serialization::writePod(file, hyphenationEnabled);
serialization::writePod(file, embeddedStyle);
serialization::writePod(file, bionicReadingEnabled);
serialization::writePod(file, imageRendering);
serialization::writePod(file, pageCount); // Placeholder for page count (will be initially 0, patched later)
serialization::writePod(file, static_cast<uint32_t>(0)); // Placeholder for LUT offset (patched later)
@@ -85,7 +88,7 @@ void Section::writeSectionFileHeader(const int fontId, const float lineCompressi
bool Section::loadSectionFile(const int fontId, const float lineCompression, const bool extraParagraphSpacing,
const uint8_t paragraphAlignment, const uint16_t viewportWidth,
const uint16_t viewportHeight, const bool hyphenationEnabled, const bool embeddedStyle,
const uint8_t imageRendering) {
const bool bionicReadingEnabled, const uint8_t imageRendering) {
if (!Storage.openFileForRead("SCT", filePath, file)) {
return false;
}
@@ -107,6 +110,7 @@ bool Section::loadSectionFile(const int fontId, const float lineCompression, con
uint8_t fileParagraphAlignment;
bool fileHyphenationEnabled;
bool fileEmbeddedStyle;
bool fileBionicReadingEnabled;
uint8_t fileImageRendering;
serialization::readPod(file, fileFontId);
serialization::readPod(file, fileLineCompression);
@@ -116,13 +120,14 @@ bool Section::loadSectionFile(const int fontId, const float lineCompression, con
serialization::readPod(file, fileViewportHeight);
serialization::readPod(file, fileHyphenationEnabled);
serialization::readPod(file, fileEmbeddedStyle);
serialization::readPod(file, fileBionicReadingEnabled);
serialization::readPod(file, fileImageRendering);
if (fontId != fileFontId || lineCompression != fileLineCompression ||
extraParagraphSpacing != fileExtraParagraphSpacing || paragraphAlignment != fileParagraphAlignment ||
viewportWidth != fileViewportWidth || viewportHeight != fileViewportHeight ||
hyphenationEnabled != fileHyphenationEnabled || embeddedStyle != fileEmbeddedStyle ||
imageRendering != fileImageRendering) {
bionicReadingEnabled != fileBionicReadingEnabled || imageRendering != fileImageRendering) {
LOG_ERR("SCT", "Deserialization failed: Parameters do not match");
clearCache(); // closes file before removal
return false;
@@ -188,7 +193,8 @@ bool Section::clearCache() {
bool Section::createSectionFile(const int fontId, const float lineCompression, const bool extraParagraphSpacing,
const uint8_t paragraphAlignment, const uint16_t viewportWidth,
const uint16_t viewportHeight, const bool hyphenationEnabled, const bool embeddedStyle,
const uint8_t imageRendering, const std::function<void(int)>& progressFn) {
const bool bionicReadingEnabled, const uint8_t imageRendering,
const std::function<void(int)>& progressFn) {
const uint32_t phaseTotalStart = millis();
const auto localPath = epub->getSpineItem(spineIndex).href;
@@ -210,7 +216,7 @@ bool Section::createSectionFile(const int fontId, const float lineCompression, c
return false;
}
writeSectionFileHeader(fontId, lineCompression, extraParagraphSpacing, paragraphAlignment, viewportWidth,
viewportHeight, hyphenationEnabled, embeddedStyle, imageRendering);
viewportHeight, hyphenationEnabled, embeddedStyle, bionicReadingEnabled, imageRendering);
std::vector<uint32_t> lut = {};
// Derive the content base directory and image cache path prefix for the parser
@@ -243,7 +249,7 @@ bool Section::createSectionFile(const int fontId, const float lineCompression, c
ChapterHtmlSlimParser visitor(
epub, renderer, fontId, lineCompression, extraParagraphSpacing, paragraphAlignment, viewportWidth, viewportHeight,
hyphenationEnabled,
hyphenationEnabled, bionicReadingEnabled,
[this, &lut](std::unique_ptr<Page> page) { lut.emplace_back(this->onPageComplete(std::move(page))); },
embeddedStyle, contentBase, imageBasePath, imageRendering, std::move(tocAnchors), progressFn, cssParser);
Hyphenator::setPreferredLanguage(epub->getLanguage());
+4 -3
View File
@@ -20,7 +20,7 @@ class Section {
void writeSectionFileHeader(int fontId, float lineCompression, bool extraParagraphSpacing, uint8_t paragraphAlignment,
uint16_t viewportWidth, uint16_t viewportHeight, bool hyphenationEnabled,
bool embeddedStyle, uint8_t imageRendering);
bool embeddedStyle, bool bionicReadingEnabled, uint8_t imageRendering);
uint32_t onPageComplete(std::unique_ptr<Page> page);
struct TocBoundary {
@@ -50,11 +50,12 @@ class Section {
~Section() = default;
bool loadSectionFile(int fontId, float lineCompression, bool extraParagraphSpacing, uint8_t paragraphAlignment,
uint16_t viewportWidth, uint16_t viewportHeight, bool hyphenationEnabled, bool embeddedStyle,
uint8_t imageRendering);
bool bionicReadingEnabled, uint8_t imageRendering);
bool clearCache();
bool createSectionFile(int fontId, float lineCompression, bool extraParagraphSpacing, uint8_t paragraphAlignment,
uint16_t viewportWidth, uint16_t viewportHeight, bool hyphenationEnabled, bool embeddedStyle,
uint8_t imageRendering, const std::function<void(int)>& progressFn = nullptr);
bool bionicReadingEnabled, uint8_t imageRendering,
const std::function<void(int)>& progressFn = nullptr);
std::unique_ptr<Page> loadPageFromSectionFile();
// Given a page in this section, return the TOC index for that page.
@@ -255,7 +255,7 @@ void ChapterHtmlSlimParser::startNewTextBlock(const BlockStyle& blockStyle) {
anchorData.push_back({std::move(pendingAnchorId), static_cast<uint16_t>(completedPageCount)});
pendingAnchorId.clear();
}
currentTextBlock.reset(new ParsedText(extraParagraphSpacing, hyphenationEnabled, blockStyle));
currentTextBlock.reset(new ParsedText(extraParagraphSpacing, hyphenationEnabled, blockStyle, bionicReadingEnabled));
wordsExtractedInBlock = 0;
}
+10 -10
View File
@@ -128,6 +128,7 @@ class ChapterHtmlSlimParser final : public Print {
int currentFootnoteLinkTextLen = 0;
std::vector<std::pair<int, FootnoteEntry>> pendingFootnotes; // <wordIndex, entry>
int wordsExtractedInBlock = 0;
bool bionicReadingEnabled = false;
// Per-chapter caches: resolveStyle and parseInlineStyle are called for every HTML element;
// caching by (tag|classAttr) and styleAttr avoids repeated string operations and hash lookups.
@@ -149,16 +150,14 @@ class ChapterHtmlSlimParser final : public Print {
static void XMLCALL endElement(void* userData, const XML_Char* name);
public:
explicit ChapterHtmlSlimParser(std::shared_ptr<Epub> epub, GfxRenderer& renderer, const int fontId,
const float lineCompression, const bool extraParagraphSpacing,
const uint8_t paragraphAlignment, const uint16_t viewportWidth,
const uint16_t viewportHeight, const bool hyphenationEnabled,
const std::function<void(std::unique_ptr<Page>)>& completePageFn,
const bool embeddedStyle, const std::string& contentBase,
const std::string& imageBasePath, const uint8_t imageRendering = 0,
std::vector<std::string> tocAnchors = {},
const std::function<void(int)>& progressFn = nullptr,
const CssParser* cssParser = nullptr)
explicit ChapterHtmlSlimParser(
std::shared_ptr<Epub> epub, GfxRenderer& renderer, const int fontId, const float lineCompression,
const bool extraParagraphSpacing, const uint8_t paragraphAlignment, const uint16_t viewportWidth,
const uint16_t viewportHeight, const bool hyphenationEnabled, const bool bionicReadingEnabled,
const std::function<void(std::unique_ptr<Page>)>& completePageFn, const bool embeddedStyle,
const std::string& contentBase, const std::string& imageBasePath, const uint8_t imageRendering = 0,
std::vector<std::string> tocAnchors = {}, const std::function<void(int)>& progressFn = nullptr,
const CssParser* cssParser = nullptr)
: epub(epub),
renderer(renderer),
@@ -169,6 +168,7 @@ class ChapterHtmlSlimParser final : public Print {
viewportWidth(viewportWidth),
viewportHeight(viewportHeight),
hyphenationEnabled(hyphenationEnabled),
bionicReadingEnabled(bionicReadingEnabled),
completePageFn(completePageFn),
progressFn(progressFn),
cssParser(cssParser),
+2
View File
@@ -249,6 +249,8 @@ class CrossPointSettings {
uint8_t imageDithering = IMAGE_DITHER_BAYER;
// Enable synthetic TOC fallback for malformed/sparse TOC books (1 = enabled, 0 = disabled)
uint8_t syntheticTocFallback = 1;
// Default bionic reading in EPUB pages when no per-book override is set (1 = enabled, 0 = disabled)
uint8_t bionicReading = 0;
// Automatically push reading progress to the KOReader sync server when leaving the reader
// (1 = enabled, 0 = disabled). The push only fires when credentials are configured and the
// reader session advanced at least 3 pages, and is skipped when remote progress is already ahead.
+1 -1
View File
@@ -456,7 +456,7 @@ bool JsonSettingsIO::loadRecentBooks(RecentBooksStore& store, const char* json)
book.fontFamilyOverride =
clampInt8(obj["fontFamilyOverride"] | -1, -1, CrossPointSettings::FONT_FAMILY_COUNT - 1, -1);
book.fontSizeOverride = clampInt8(obj["fontSizeOverride"] | -1, -1, CrossPointSettings::FONT_SIZE_COUNT - 1, -1);
book.bionicReadingOverride = obj["bionicReadingOverride"] | false;
book.bionicReadingOverride = clampInt8(obj["bionicReadingOverride"] | -1, -1, 1, -1);
store.recentBooks.push_back(book);
}
+1 -1
View File
@@ -26,7 +26,7 @@ void RecentBooksStore::addBook(const std::string& path, const std::string& title
int8_t imageRenderingOverride = -1;
int8_t fontFamilyOverride = -1;
int8_t fontSizeOverride = -1;
bool bionicReadingOverride = false;
int8_t bionicReadingOverride = -1;
// Remove existing entry if present
auto it =
+2 -1
View File
@@ -17,7 +17,8 @@ struct RecentBook {
int8_t fontFamilyOverride = -1;
// -1 = use global setting, otherwise CrossPointSettings::FONT_SIZE value.
int8_t fontSizeOverride = -1;
bool bionicReadingOverride = false;
// -1 = use global default, otherwise explicit per-book override (0 = off, 1 = on).
int8_t bionicReadingOverride = -1;
bool operator==(const RecentBook& other) const { return path == other.path; }
};
+2
View File
@@ -117,6 +117,8 @@ inline const std::vector<SettingInfo> list = {
StrId::STR_CAT_READER),
SettingInfo::Toggle(StrId::STR_HYPHENATION, &CrossPointSettings::hyphenationEnabled, "hyphenationEnabled",
StrId::STR_CAT_READER),
SettingInfo::Toggle(StrId::STR_BIONIC_READING, &CrossPointSettings::bionicReading, "bionicReading",
StrId::STR_CAT_READER),
SettingInfo::Enum(StrId::STR_IMAGES, &CrossPointSettings::imageRendering,
{StrId::STR_IMAGES_DISPLAY, StrId::STR_IMAGES_PLACEHOLDER, StrId::STR_IMAGES_SUPPRESS},
"imageRendering", StrId::STR_CAT_READER),
+14 -8
View File
@@ -191,7 +191,9 @@ void EpubReaderActivity::onEnter() {
bookImageRenderingOverride = currentBook.imageRenderingOverride;
bookFontFamilyOverride = currentBook.fontFamilyOverride;
bookFontSizeOverride = currentBook.fontSizeOverride;
bookBionicReadingOverride = currentBook.bionicReadingOverride;
bookBionicReadingOverride = (currentBook.bionicReadingOverride >= 0)
? static_cast<bool>(currentBook.bionicReadingOverride)
: static_cast<bool>(SETTINGS.bionicReading);
logReaderMemSnapshot("onEnter_after_recent_books");
// Trigger first update
@@ -1254,7 +1256,8 @@ void EpubReaderActivity::render(RenderLock&& lock) {
if (!section->loadSectionFile(getEffectiveReaderFontId(), getEffectiveReaderLineCompression(),
SETTINGS.extraParagraphSpacing, SETTINGS.paragraphAlignment, viewportWidth,
viewportHeight, SETTINGS.hyphenationEnabled, embeddedStyle, imageRendering)) {
viewportHeight, SETTINGS.hyphenationEnabled, embeddedStyle, bookBionicReadingOverride,
imageRendering)) {
LOG_DBG("ERS", "Cache not found, building...");
lastRenderStats.cacheRebuilt = true;
@@ -1275,8 +1278,8 @@ void EpubReaderActivity::render(RenderLock&& lock) {
renderer.clearSdCardFontAccumulation();
if (!section->createSectionFile(getEffectiveReaderFontId(), getEffectiveReaderLineCompression(),
SETTINGS.extraParagraphSpacing, SETTINGS.paragraphAlignment, viewportWidth,
viewportHeight, SETTINGS.hyphenationEnabled, embeddedStyle, imageRendering,
progressFn)) {
viewportHeight, SETTINGS.hyphenationEnabled, embeddedStyle,
bookBionicReadingOverride, imageRendering, progressFn)) {
LOG_ERR("ERS", "Failed to persist page data to SD");
section.reset();
return;
@@ -1430,7 +1433,8 @@ void EpubReaderActivity::silentIndexNextChapterIfNeeded(const uint16_t viewportW
Section nextSection(epub, nextSpineIndex, renderer);
if (nextSection.loadSectionFile(getEffectiveReaderFontId(), getEffectiveReaderLineCompression(),
SETTINGS.extraParagraphSpacing, SETTINGS.paragraphAlignment, viewportWidth,
viewportHeight, SETTINGS.hyphenationEnabled, embeddedStyle, imageRendering)) {
viewportHeight, SETTINGS.hyphenationEnabled, embeddedStyle, bookBionicReadingOverride,
imageRendering)) {
return;
}
@@ -1439,7 +1443,8 @@ void EpubReaderActivity::silentIndexNextChapterIfNeeded(const uint16_t viewportW
renderer.clearSdCardFontAccumulation();
if (!nextSection.createSectionFile(getEffectiveReaderFontId(), getEffectiveReaderLineCompression(),
SETTINGS.extraParagraphSpacing, SETTINGS.paragraphAlignment, viewportWidth,
viewportHeight, SETTINGS.hyphenationEnabled, embeddedStyle, imageRendering)) {
viewportHeight, SETTINGS.hyphenationEnabled, embeddedStyle,
bookBionicReadingOverride, imageRendering)) {
LOG_ERR("ERS", "Failed silent indexing for chapter: %d", nextSpineIndex);
}
}
@@ -1822,12 +1827,13 @@ bool EpubReaderActivity::drawCurrentPageToBuffer(const std::string& filePath, Gf
if (!section->loadSectionFile(getEffectiveFontId(effectiveFontFamily, effectiveFontSize), effectiveLineCompression,
SETTINGS.extraParagraphSpacing, SETTINGS.paragraphAlignment, viewportWidth,
viewportHeight, SETTINGS.hyphenationEnabled, SETTINGS.embeddedStyle,
SETTINGS.imageRendering)) {
static_cast<bool>(SETTINGS.bionicReading), SETTINGS.imageRendering)) {
LOG_DBG("SLP", "EPUB: section cache not found for spine %d, rebuilding", spineIndex);
if (!section->createSectionFile(getEffectiveFontId(effectiveFontFamily, effectiveFontSize),
effectiveLineCompression, SETTINGS.extraParagraphSpacing,
SETTINGS.paragraphAlignment, viewportWidth, viewportHeight,
SETTINGS.hyphenationEnabled, SETTINGS.embeddedStyle, SETTINGS.imageRendering)) {
SETTINGS.hyphenationEnabled, SETTINGS.embeddedStyle,
static_cast<bool>(SETTINGS.bionicReading), SETTINGS.imageRendering)) {
LOG_ERR("SLP", "EPUB: failed to rebuild section cache for spine %d", spineIndex);
return false;
}