diff --git a/lib/Epub/Epub/Section.cpp b/lib/Epub/Epub/Section.cpp index 9cb70027..18e0faef 100644 --- a/lib/Epub/Epub/Section.cpp +++ b/lib/Epub/Epub/Section.cpp @@ -8,9 +8,9 @@ #include "parsers/ChapterHtmlSlimParser.h" namespace { -constexpr uint8_t SECTION_FILE_VERSION = 11; +constexpr uint8_t SECTION_FILE_VERSION = 12; constexpr uint32_t HEADER_SIZE = sizeof(uint8_t) + sizeof(int) + sizeof(float) + sizeof(bool) + sizeof(uint8_t) + - sizeof(uint16_t) + sizeof(uint16_t) + sizeof(uint16_t) + sizeof(bool) + + sizeof(uint16_t) + sizeof(uint16_t) + sizeof(uint16_t) + sizeof(bool) + sizeof(bool) + sizeof(uint32_t); } // namespace @@ -33,7 +33,8 @@ uint32_t Section::onPageComplete(std::unique_ptr 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 uint16_t viewportHeight, const bool hyphenationEnabled, + const bool embeddedStyle) { if (!file) { Serial.printf("[%lu] [SCT] File not open for writing header\n", millis()); return; @@ -41,7 +42,7 @@ 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(uint32_t), + sizeof(embeddedStyle) + sizeof(uint32_t), "Header size mismatch"); serialization::writePod(file, SECTION_FILE_VERSION); serialization::writePod(file, fontId); @@ -51,13 +52,14 @@ void Section::writeSectionFileHeader(const int fontId, const float lineCompressi serialization::writePod(file, viewportWidth); serialization::writePod(file, viewportHeight); serialization::writePod(file, hyphenationEnabled); + serialization::writePod(file, embeddedStyle); serialization::writePod(file, pageCount); // Placeholder for page count (will be initially 0 when written) serialization::writePod(file, static_cast(0)); // Placeholder for LUT offset } 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 uint16_t viewportHeight, const bool hyphenationEnabled, const bool embeddedStyle) { if (!SdMan.openFileForRead("SCT", filePath, file)) { return false; } @@ -79,6 +81,7 @@ bool Section::loadSectionFile(const int fontId, const float lineCompression, con bool fileExtraParagraphSpacing; uint8_t fileParagraphAlignment; bool fileHyphenationEnabled; + bool fileEmbeddedStyle; serialization::readPod(file, fileFontId); serialization::readPod(file, fileLineCompression); serialization::readPod(file, fileExtraParagraphSpacing); @@ -86,11 +89,12 @@ bool Section::loadSectionFile(const int fontId, const float lineCompression, con serialization::readPod(file, fileViewportWidth); serialization::readPod(file, fileViewportHeight); serialization::readPod(file, fileHyphenationEnabled); + serialization::readPod(file, fileEmbeddedStyle); if (fontId != fileFontId || lineCompression != fileLineCompression || extraParagraphSpacing != fileExtraParagraphSpacing || paragraphAlignment != fileParagraphAlignment || viewportWidth != fileViewportWidth || viewportHeight != fileViewportHeight || - hyphenationEnabled != fileHyphenationEnabled) { + hyphenationEnabled != fileHyphenationEnabled || embeddedStyle != fileEmbeddedStyle) { file.close(); Serial.printf("[%lu] [SCT] Deserialization failed: Parameters do not match\n", millis()); clearCache(); @@ -122,7 +126,7 @@ bool Section::clearCache() const { 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 uint16_t viewportHeight, const bool hyphenationEnabled, const bool embeddedStyle, const std::function& popupFn) { const auto localPath = epub->getSpineItem(spineIndex).href; const auto tmpHtmlPath = epub->getCachePath() + "/.tmp_" + std::to_string(spineIndex) + ".html"; @@ -173,14 +177,14 @@ bool Section::createSectionFile(const int fontId, const float lineCompression, c return false; } writeSectionFileHeader(fontId, lineCompression, extraParagraphSpacing, paragraphAlignment, viewportWidth, - viewportHeight, hyphenationEnabled); + viewportHeight, hyphenationEnabled, embeddedStyle); std::vector lut = {}; ChapterHtmlSlimParser visitor( tmpHtmlPath, renderer, fontId, lineCompression, extraParagraphSpacing, paragraphAlignment, viewportWidth, viewportHeight, hyphenationEnabled, - [this, &lut](std::unique_ptr page) { lut.emplace_back(this->onPageComplete(std::move(page))); }, popupFn, - epub->getCssParser()); + [this, &lut](std::unique_ptr page) { lut.emplace_back(this->onPageComplete(std::move(page))); }, + embeddedStyle, popupFn, embeddedStyle ? epub->getCssParser() : nullptr); Hyphenator::setPreferredLanguage(epub->getLanguage()); success = visitor.parseAndBuildPages(); diff --git a/lib/Epub/Epub/Section.h b/lib/Epub/Epub/Section.h index 5fdf210a..42a6d993 100644 --- a/lib/Epub/Epub/Section.h +++ b/lib/Epub/Epub/Section.h @@ -15,7 +15,8 @@ class Section { FsFile file; void writeSectionFileHeader(int fontId, float lineCompression, bool extraParagraphSpacing, uint8_t paragraphAlignment, - uint16_t viewportWidth, uint16_t viewportHeight, bool hyphenationEnabled); + uint16_t viewportWidth, uint16_t viewportHeight, bool hyphenationEnabled, + bool embeddedStyle); uint32_t onPageComplete(std::unique_ptr page); public: @@ -29,10 +30,10 @@ class Section { filePath(epub->getCachePath() + "/sections/" + std::to_string(spineIndex) + ".bin") {} ~Section() = default; bool loadSectionFile(int fontId, float lineCompression, bool extraParagraphSpacing, uint8_t paragraphAlignment, - uint16_t viewportWidth, uint16_t viewportHeight, bool hyphenationEnabled); + uint16_t viewportWidth, uint16_t viewportHeight, bool hyphenationEnabled, bool embeddedStyle); bool clearCache() const; bool createSectionFile(int fontId, float lineCompression, bool extraParagraphSpacing, uint8_t paragraphAlignment, - uint16_t viewportWidth, uint16_t viewportHeight, bool hyphenationEnabled, + uint16_t viewportWidth, uint16_t viewportHeight, bool hyphenationEnabled, bool embeddedStyle, const std::function& popupFn = nullptr); std::unique_ptr loadPageFromSectionFile(); }; diff --git a/lib/Epub/Epub/blocks/BlockStyle.h b/lib/Epub/Epub/blocks/BlockStyle.h index 5c26a21d..63b054c9 100644 --- a/lib/Epub/Epub/blocks/BlockStyle.h +++ b/lib/Epub/Epub/blocks/BlockStyle.h @@ -80,8 +80,9 @@ struct BlockStyle { blockStyle.textIndent = cssStyle.textIndent.toPixelsInt16(emSize); blockStyle.textIndentDefined = cssStyle.hasTextIndent(); blockStyle.textAlignDefined = cssStyle.hasTextAlign(); - if (blockStyle.textAlignDefined) { - blockStyle.alignment = cssStyle.textAlign; + // User setting overrides CSS, unless "Book's Style" alignment setting is selected + if (paragraphAlignment == CssTextAlign::None) { + blockStyle.alignment = blockStyle.textAlignDefined ? cssStyle.textAlign : CssTextAlign::Justify; } else { blockStyle.alignment = paragraphAlignment; } diff --git a/lib/Epub/Epub/css/CssStyle.h b/lib/Epub/Epub/css/CssStyle.h index 7b83da3f..adbc19e2 100644 --- a/lib/Epub/Epub/css/CssStyle.h +++ b/lib/Epub/Epub/css/CssStyle.h @@ -3,7 +3,7 @@ #include // Matches order of PARAGRAPH_ALIGNMENT in CrossPointSettings -enum class CssTextAlign : uint8_t { Justify = 0, Left = 1, Center = 2, Right = 3 }; +enum class CssTextAlign : uint8_t { Justify = 0, Left = 1, Center = 2, Right = 3, None = 4 }; enum class CssUnit : uint8_t { Pixels = 0, Em = 1, Rem = 2, Points = 3 }; // Represents a CSS length value with its unit, allowing deferred resolution to pixels diff --git a/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp b/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp index ab93d9cb..cb5625c1 100644 --- a/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp +++ b/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp @@ -211,11 +211,17 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char* } const float emSize = static_cast(self->renderer.getLineHeight(self->fontId)) * self->lineCompression; - const auto userAlignment = static_cast(self->paragraphAlignment); + const auto userAlignmentBlockStyle = + BlockStyle::fromCssStyle(cssStyle, emSize, static_cast(self->paragraphAlignment)); if (matches(name, HEADER_TAGS, NUM_HEADER_TAGS)) { self->currentCssStyle = cssStyle; - self->startNewTextBlock(BlockStyle::fromCssStyle(cssStyle, emSize, userAlignment)); + auto headerBlockStyle = BlockStyle::fromCssStyle(cssStyle, emSize, CssTextAlign::Center); + headerBlockStyle.textAlignDefined = true; + if (self->embeddedStyle && cssStyle.hasTextAlign()) { + headerBlockStyle.alignment = cssStyle.textAlign; + } + self->startNewTextBlock(headerBlockStyle); self->boldUntilDepth = std::min(self->boldUntilDepth, self->depth); self->updateEffectiveInlineStyle(); } else if (matches(name, BLOCK_TAGS, NUM_BLOCK_TAGS)) { @@ -227,7 +233,7 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char* self->startNewTextBlock(self->currentTextBlock->getBlockStyle()); } else { self->currentCssStyle = cssStyle; - self->startNewTextBlock(BlockStyle::fromCssStyle(cssStyle, emSize, userAlignment)); + self->startNewTextBlock(userAlignmentBlockStyle); self->updateEffectiveInlineStyle(); if (strcmp(name, "li") == 0) { @@ -430,7 +436,11 @@ void XMLCALL ChapterHtmlSlimParser::endElement(void* userData, const XML_Char* n bool ChapterHtmlSlimParser::parseAndBuildPages() { auto paragraphAlignmentBlockStyle = BlockStyle(); paragraphAlignmentBlockStyle.textAlignDefined = true; - paragraphAlignmentBlockStyle.alignment = static_cast(this->paragraphAlignment); + // Resolve None sentinel to Justify for initial block (no CSS context yet) + const auto align = (this->paragraphAlignment == static_cast(CssTextAlign::None)) + ? CssTextAlign::Justify + : static_cast(this->paragraphAlignment); + paragraphAlignmentBlockStyle.alignment = align; startNewTextBlock(paragraphAlignmentBlockStyle); const XML_Parser parser = XML_ParserCreate(nullptr); diff --git a/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.h b/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.h index 92a9838a..261d8f4e 100644 --- a/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.h +++ b/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.h @@ -41,6 +41,7 @@ class ChapterHtmlSlimParser { uint16_t viewportHeight; bool hyphenationEnabled; const CssParser* cssParser; + bool embeddedStyle; // Style tracking (replaces depth-based approach) struct StyleStackEntry { @@ -70,7 +71,8 @@ class ChapterHtmlSlimParser { const uint8_t paragraphAlignment, const uint16_t viewportWidth, const uint16_t viewportHeight, const bool hyphenationEnabled, const std::function)>& completePageFn, - const std::function& popupFn = nullptr, const CssParser* cssParser = nullptr) + const bool embeddedStyle, const std::function& popupFn = nullptr, + const CssParser* cssParser = nullptr) : filepath(filepath), renderer(renderer), @@ -83,7 +85,8 @@ class ChapterHtmlSlimParser { hyphenationEnabled(hyphenationEnabled), completePageFn(completePageFn), popupFn(popupFn), - cssParser(cssParser) {} + cssParser(cssParser), + embeddedStyle(embeddedStyle) {} ~ChapterHtmlSlimParser() = default; bool parseAndBuildPages(); diff --git a/src/CrossPointSettings.cpp b/src/CrossPointSettings.cpp index da287046..28a357e9 100644 --- a/src/CrossPointSettings.cpp +++ b/src/CrossPointSettings.cpp @@ -22,7 +22,7 @@ void readAndValidate(FsFile& file, uint8_t& member, const uint8_t maxValue) { namespace { constexpr uint8_t SETTINGS_FILE_VERSION = 1; // Increment this when adding new persisted settings fields -constexpr uint8_t SETTINGS_COUNT = 29; +constexpr uint8_t SETTINGS_COUNT = 30; constexpr char SETTINGS_FILE[] = "/.crosspoint/settings.bin"; // Validate front button mapping to ensure each hardware button is unique. @@ -117,6 +117,7 @@ bool CrossPointSettings::saveToFile() const { serialization::writePod(outputFile, frontButtonLeft); serialization::writePod(outputFile, frontButtonRight); serialization::writePod(outputFile, fadingFix); + serialization::writePod(outputFile, embeddedStyle); // New fields added at end for backward compatibility outputFile.close(); @@ -220,6 +221,8 @@ bool CrossPointSettings::loadFromFile() { if (++settingsRead >= fileSettingsCount) break; serialization::readPod(inputFile, fadingFix); if (++settingsRead >= fileSettingsCount) break; + serialization::readPod(inputFile, embeddedStyle); + if (++settingsRead >= fileSettingsCount) break; // New fields added at end for backward compatibility } while (false); diff --git a/src/CrossPointSettings.h b/src/CrossPointSettings.h index 86700cad..1348519f 100644 --- a/src/CrossPointSettings.h +++ b/src/CrossPointSettings.h @@ -86,6 +86,7 @@ class CrossPointSettings { LEFT_ALIGN = 1, CENTER_ALIGN = 2, RIGHT_ALIGN = 3, + BOOK_STYLE = 4, PARAGRAPH_ALIGNMENT_COUNT }; @@ -168,6 +169,8 @@ class CrossPointSettings { uint8_t uiTheme = LYRA; // Sunlight fading compensation uint8_t fadingFix = 0; + // Use book's embedded CSS styles for EPUB rendering (1 = enabled, 0 = disabled) + uint8_t embeddedStyle = 1; ~CrossPointSettings() = default; diff --git a/src/activities/reader/EpubReaderActivity.cpp b/src/activities/reader/EpubReaderActivity.cpp index 72600c57..5428a00c 100644 --- a/src/activities/reader/EpubReaderActivity.cpp +++ b/src/activities/reader/EpubReaderActivity.cpp @@ -575,14 +575,14 @@ void EpubReaderActivity::renderScreen() { if (!section->loadSectionFile(SETTINGS.getReaderFontId(), SETTINGS.getReaderLineCompression(), SETTINGS.extraParagraphSpacing, SETTINGS.paragraphAlignment, viewportWidth, - viewportHeight, SETTINGS.hyphenationEnabled)) { + viewportHeight, SETTINGS.hyphenationEnabled, SETTINGS.embeddedStyle)) { Serial.printf("[%lu] [ERS] Cache not found, building...\n", millis()); const auto popupFn = [this]() { GUI.drawPopup(renderer, "Indexing..."); }; if (!section->createSectionFile(SETTINGS.getReaderFontId(), SETTINGS.getReaderLineCompression(), SETTINGS.extraParagraphSpacing, SETTINGS.paragraphAlignment, viewportWidth, - viewportHeight, SETTINGS.hyphenationEnabled, popupFn)) { + viewportHeight, SETTINGS.hyphenationEnabled, SETTINGS.embeddedStyle, popupFn)) { Serial.printf("[%lu] [ERS] Failed to persist page data to SD\n", millis()); section.reset(); return; diff --git a/src/activities/settings/SettingsActivity.cpp b/src/activities/settings/SettingsActivity.cpp index 893eb108..efe5b5ed 100644 --- a/src/activities/settings/SettingsActivity.cpp +++ b/src/activities/settings/SettingsActivity.cpp @@ -35,14 +35,15 @@ const SettingInfo displaySettings[displaySettingsCount] = { SettingInfo::Toggle("Sunlight Fading Fix", &CrossPointSettings::fadingFix), }; -constexpr int readerSettingsCount = 9; +constexpr int readerSettingsCount = 10; const SettingInfo readerSettings[readerSettingsCount] = { SettingInfo::Enum("Font Family", &CrossPointSettings::fontFamily, {"Bookerly", "Noto Sans", "Open Dyslexic"}), SettingInfo::Enum("Font Size", &CrossPointSettings::fontSize, {"Small", "Medium", "Large", "X Large"}), SettingInfo::Enum("Line Spacing", &CrossPointSettings::lineSpacing, {"Tight", "Normal", "Wide"}), SettingInfo::Value("Screen Margin", &CrossPointSettings::screenMargin, {5, 40, 5}), SettingInfo::Enum("Paragraph Alignment", &CrossPointSettings::paragraphAlignment, - {"Justify", "Left", "Center", "Right"}), + {"Justify", "Left", "Center", "Right", "Book's Style"}), + SettingInfo::Toggle("Embedded Style", &CrossPointSettings::embeddedStyle), SettingInfo::Toggle("Hyphenation", &CrossPointSettings::hyphenationEnabled), SettingInfo::Enum("Reading Orientation", &CrossPointSettings::orientation, {"Portrait", "Landscape CW", "Inverted", "Landscape CCW"}),