A fork of https://github.com/crosspoint-reader/crosspoint-reader
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: Add Settings for toggling CSS on or off (#717)

Closes #712

## Summary

**What is the goal of this PR?**

- To add new settings for toggling on/off embedded CSS styles in the
reader. This gives more control and customization to the user over how
the ereader experience looks.

**What changes are included?**

- Added new "Embedded Style" option to the Reader settings
- Added new "Book's Style" option for "Paragraph Alignment"
- User's selected "Paragraph Alignment" will take precedence and
override the embedded CSS `text-align` property, _unless_ the user has
"Book's Style" set as their "Paragraph Alignment"

## Additional Context

![IMG_6336](https://github.com/user-attachments/assets/dff619ef-986d-465e-b352-73a76baae334)


https://github.com/user-attachments/assets/9e404b13-c7e0-41c7-9406-4715f389166a


Addresses feedback from the community about the new CSS feature:
https://github.com/crosspoint-reader/crosspoint-reader/pull/700

---

### AI Usage

While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.

Did you use AI tools to help write this code? _**YES**_, Claude Code

authored by

Jake Kenneally and committed by
GitHub
f89ce514 d8e813a7

+53 -27
+14 -10
lib/Epub/Epub/Section.cpp
··· 8 8 #include "parsers/ChapterHtmlSlimParser.h" 9 9 10 10 namespace { 11 - constexpr uint8_t SECTION_FILE_VERSION = 11; 11 + constexpr uint8_t SECTION_FILE_VERSION = 12; 12 12 constexpr uint32_t HEADER_SIZE = sizeof(uint8_t) + sizeof(int) + sizeof(float) + sizeof(bool) + sizeof(uint8_t) + 13 - sizeof(uint16_t) + sizeof(uint16_t) + sizeof(uint16_t) + sizeof(bool) + 13 + sizeof(uint16_t) + sizeof(uint16_t) + sizeof(uint16_t) + sizeof(bool) + sizeof(bool) + 14 14 sizeof(uint32_t); 15 15 } // namespace 16 16 ··· 33 33 34 34 void Section::writeSectionFileHeader(const int fontId, const float lineCompression, const bool extraParagraphSpacing, 35 35 const uint8_t paragraphAlignment, const uint16_t viewportWidth, 36 - const uint16_t viewportHeight, const bool hyphenationEnabled) { 36 + const uint16_t viewportHeight, const bool hyphenationEnabled, 37 + const bool embeddedStyle) { 37 38 if (!file) { 38 39 Serial.printf("[%lu] [SCT] File not open for writing header\n", millis()); 39 40 return; ··· 41 42 static_assert(HEADER_SIZE == sizeof(SECTION_FILE_VERSION) + sizeof(fontId) + sizeof(lineCompression) + 42 43 sizeof(extraParagraphSpacing) + sizeof(paragraphAlignment) + sizeof(viewportWidth) + 43 44 sizeof(viewportHeight) + sizeof(pageCount) + sizeof(hyphenationEnabled) + 44 - sizeof(uint32_t), 45 + sizeof(embeddedStyle) + sizeof(uint32_t), 45 46 "Header size mismatch"); 46 47 serialization::writePod(file, SECTION_FILE_VERSION); 47 48 serialization::writePod(file, fontId); ··· 51 52 serialization::writePod(file, viewportWidth); 52 53 serialization::writePod(file, viewportHeight); 53 54 serialization::writePod(file, hyphenationEnabled); 55 + serialization::writePod(file, embeddedStyle); 54 56 serialization::writePod(file, pageCount); // Placeholder for page count (will be initially 0 when written) 55 57 serialization::writePod(file, static_cast<uint32_t>(0)); // Placeholder for LUT offset 56 58 } 57 59 58 60 bool Section::loadSectionFile(const int fontId, const float lineCompression, const bool extraParagraphSpacing, 59 61 const uint8_t paragraphAlignment, const uint16_t viewportWidth, 60 - const uint16_t viewportHeight, const bool hyphenationEnabled) { 62 + const uint16_t viewportHeight, const bool hyphenationEnabled, const bool embeddedStyle) { 61 63 if (!SdMan.openFileForRead("SCT", filePath, file)) { 62 64 return false; 63 65 } ··· 79 81 bool fileExtraParagraphSpacing; 80 82 uint8_t fileParagraphAlignment; 81 83 bool fileHyphenationEnabled; 84 + bool fileEmbeddedStyle; 82 85 serialization::readPod(file, fileFontId); 83 86 serialization::readPod(file, fileLineCompression); 84 87 serialization::readPod(file, fileExtraParagraphSpacing); ··· 86 89 serialization::readPod(file, fileViewportWidth); 87 90 serialization::readPod(file, fileViewportHeight); 88 91 serialization::readPod(file, fileHyphenationEnabled); 92 + serialization::readPod(file, fileEmbeddedStyle); 89 93 90 94 if (fontId != fileFontId || lineCompression != fileLineCompression || 91 95 extraParagraphSpacing != fileExtraParagraphSpacing || paragraphAlignment != fileParagraphAlignment || 92 96 viewportWidth != fileViewportWidth || viewportHeight != fileViewportHeight || 93 - hyphenationEnabled != fileHyphenationEnabled) { 97 + hyphenationEnabled != fileHyphenationEnabled || embeddedStyle != fileEmbeddedStyle) { 94 98 file.close(); 95 99 Serial.printf("[%lu] [SCT] Deserialization failed: Parameters do not match\n", millis()); 96 100 clearCache(); ··· 122 126 123 127 bool Section::createSectionFile(const int fontId, const float lineCompression, const bool extraParagraphSpacing, 124 128 const uint8_t paragraphAlignment, const uint16_t viewportWidth, 125 - const uint16_t viewportHeight, const bool hyphenationEnabled, 129 + const uint16_t viewportHeight, const bool hyphenationEnabled, const bool embeddedStyle, 126 130 const std::function<void()>& popupFn) { 127 131 const auto localPath = epub->getSpineItem(spineIndex).href; 128 132 const auto tmpHtmlPath = epub->getCachePath() + "/.tmp_" + std::to_string(spineIndex) + ".html"; ··· 173 177 return false; 174 178 } 175 179 writeSectionFileHeader(fontId, lineCompression, extraParagraphSpacing, paragraphAlignment, viewportWidth, 176 - viewportHeight, hyphenationEnabled); 180 + viewportHeight, hyphenationEnabled, embeddedStyle); 177 181 std::vector<uint32_t> lut = {}; 178 182 179 183 ChapterHtmlSlimParser visitor( 180 184 tmpHtmlPath, renderer, fontId, lineCompression, extraParagraphSpacing, paragraphAlignment, viewportWidth, 181 185 viewportHeight, hyphenationEnabled, 182 - [this, &lut](std::unique_ptr<Page> page) { lut.emplace_back(this->onPageComplete(std::move(page))); }, popupFn, 183 - epub->getCssParser()); 186 + [this, &lut](std::unique_ptr<Page> page) { lut.emplace_back(this->onPageComplete(std::move(page))); }, 187 + embeddedStyle, popupFn, embeddedStyle ? epub->getCssParser() : nullptr); 184 188 Hyphenator::setPreferredLanguage(epub->getLanguage()); 185 189 success = visitor.parseAndBuildPages(); 186 190
+4 -3
lib/Epub/Epub/Section.h
··· 15 15 FsFile file; 16 16 17 17 void writeSectionFileHeader(int fontId, float lineCompression, bool extraParagraphSpacing, uint8_t paragraphAlignment, 18 - uint16_t viewportWidth, uint16_t viewportHeight, bool hyphenationEnabled); 18 + uint16_t viewportWidth, uint16_t viewportHeight, bool hyphenationEnabled, 19 + bool embeddedStyle); 19 20 uint32_t onPageComplete(std::unique_ptr<Page> page); 20 21 21 22 public: ··· 29 30 filePath(epub->getCachePath() + "/sections/" + std::to_string(spineIndex) + ".bin") {} 30 31 ~Section() = default; 31 32 bool loadSectionFile(int fontId, float lineCompression, bool extraParagraphSpacing, uint8_t paragraphAlignment, 32 - uint16_t viewportWidth, uint16_t viewportHeight, bool hyphenationEnabled); 33 + uint16_t viewportWidth, uint16_t viewportHeight, bool hyphenationEnabled, bool embeddedStyle); 33 34 bool clearCache() const; 34 35 bool createSectionFile(int fontId, float lineCompression, bool extraParagraphSpacing, uint8_t paragraphAlignment, 35 - uint16_t viewportWidth, uint16_t viewportHeight, bool hyphenationEnabled, 36 + uint16_t viewportWidth, uint16_t viewportHeight, bool hyphenationEnabled, bool embeddedStyle, 36 37 const std::function<void()>& popupFn = nullptr); 37 38 std::unique_ptr<Page> loadPageFromSectionFile(); 38 39 };
+3 -2
lib/Epub/Epub/blocks/BlockStyle.h
··· 80 80 blockStyle.textIndent = cssStyle.textIndent.toPixelsInt16(emSize); 81 81 blockStyle.textIndentDefined = cssStyle.hasTextIndent(); 82 82 blockStyle.textAlignDefined = cssStyle.hasTextAlign(); 83 - if (blockStyle.textAlignDefined) { 84 - blockStyle.alignment = cssStyle.textAlign; 83 + // User setting overrides CSS, unless "Book's Style" alignment setting is selected 84 + if (paragraphAlignment == CssTextAlign::None) { 85 + blockStyle.alignment = blockStyle.textAlignDefined ? cssStyle.textAlign : CssTextAlign::Justify; 85 86 } else { 86 87 blockStyle.alignment = paragraphAlignment; 87 88 }
+1 -1
lib/Epub/Epub/css/CssStyle.h
··· 3 3 #include <cstdint> 4 4 5 5 // Matches order of PARAGRAPH_ALIGNMENT in CrossPointSettings 6 - enum class CssTextAlign : uint8_t { Justify = 0, Left = 1, Center = 2, Right = 3 }; 6 + enum class CssTextAlign : uint8_t { Justify = 0, Left = 1, Center = 2, Right = 3, None = 4 }; 7 7 enum class CssUnit : uint8_t { Pixels = 0, Em = 1, Rem = 2, Points = 3 }; 8 8 9 9 // Represents a CSS length value with its unit, allowing deferred resolution to pixels
+14 -4
lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp
··· 211 211 } 212 212 213 213 const float emSize = static_cast<float>(self->renderer.getLineHeight(self->fontId)) * self->lineCompression; 214 - const auto userAlignment = static_cast<CssTextAlign>(self->paragraphAlignment); 214 + const auto userAlignmentBlockStyle = 215 + BlockStyle::fromCssStyle(cssStyle, emSize, static_cast<CssTextAlign>(self->paragraphAlignment)); 215 216 216 217 if (matches(name, HEADER_TAGS, NUM_HEADER_TAGS)) { 217 218 self->currentCssStyle = cssStyle; 218 - self->startNewTextBlock(BlockStyle::fromCssStyle(cssStyle, emSize, userAlignment)); 219 + auto headerBlockStyle = BlockStyle::fromCssStyle(cssStyle, emSize, CssTextAlign::Center); 220 + headerBlockStyle.textAlignDefined = true; 221 + if (self->embeddedStyle && cssStyle.hasTextAlign()) { 222 + headerBlockStyle.alignment = cssStyle.textAlign; 223 + } 224 + self->startNewTextBlock(headerBlockStyle); 219 225 self->boldUntilDepth = std::min(self->boldUntilDepth, self->depth); 220 226 self->updateEffectiveInlineStyle(); 221 227 } else if (matches(name, BLOCK_TAGS, NUM_BLOCK_TAGS)) { ··· 227 233 self->startNewTextBlock(self->currentTextBlock->getBlockStyle()); 228 234 } else { 229 235 self->currentCssStyle = cssStyle; 230 - self->startNewTextBlock(BlockStyle::fromCssStyle(cssStyle, emSize, userAlignment)); 236 + self->startNewTextBlock(userAlignmentBlockStyle); 231 237 self->updateEffectiveInlineStyle(); 232 238 233 239 if (strcmp(name, "li") == 0) { ··· 430 436 bool ChapterHtmlSlimParser::parseAndBuildPages() { 431 437 auto paragraphAlignmentBlockStyle = BlockStyle(); 432 438 paragraphAlignmentBlockStyle.textAlignDefined = true; 433 - paragraphAlignmentBlockStyle.alignment = static_cast<CssTextAlign>(this->paragraphAlignment); 439 + // Resolve None sentinel to Justify for initial block (no CSS context yet) 440 + const auto align = (this->paragraphAlignment == static_cast<uint8_t>(CssTextAlign::None)) 441 + ? CssTextAlign::Justify 442 + : static_cast<CssTextAlign>(this->paragraphAlignment); 443 + paragraphAlignmentBlockStyle.alignment = align; 434 444 startNewTextBlock(paragraphAlignmentBlockStyle); 435 445 436 446 const XML_Parser parser = XML_ParserCreate(nullptr);
+5 -2
lib/Epub/Epub/parsers/ChapterHtmlSlimParser.h
··· 41 41 uint16_t viewportHeight; 42 42 bool hyphenationEnabled; 43 43 const CssParser* cssParser; 44 + bool embeddedStyle; 44 45 45 46 // Style tracking (replaces depth-based approach) 46 47 struct StyleStackEntry { ··· 70 71 const uint8_t paragraphAlignment, const uint16_t viewportWidth, 71 72 const uint16_t viewportHeight, const bool hyphenationEnabled, 72 73 const std::function<void(std::unique_ptr<Page>)>& completePageFn, 73 - const std::function<void()>& popupFn = nullptr, const CssParser* cssParser = nullptr) 74 + const bool embeddedStyle, const std::function<void()>& popupFn = nullptr, 75 + const CssParser* cssParser = nullptr) 74 76 75 77 : filepath(filepath), 76 78 renderer(renderer), ··· 83 85 hyphenationEnabled(hyphenationEnabled), 84 86 completePageFn(completePageFn), 85 87 popupFn(popupFn), 86 - cssParser(cssParser) {} 88 + cssParser(cssParser), 89 + embeddedStyle(embeddedStyle) {} 87 90 88 91 ~ChapterHtmlSlimParser() = default; 89 92 bool parseAndBuildPages();
+4 -1
src/CrossPointSettings.cpp
··· 22 22 namespace { 23 23 constexpr uint8_t SETTINGS_FILE_VERSION = 1; 24 24 // Increment this when adding new persisted settings fields 25 - constexpr uint8_t SETTINGS_COUNT = 29; 25 + constexpr uint8_t SETTINGS_COUNT = 30; 26 26 constexpr char SETTINGS_FILE[] = "/.crosspoint/settings.bin"; 27 27 28 28 // Validate front button mapping to ensure each hardware button is unique. ··· 117 117 serialization::writePod(outputFile, frontButtonLeft); 118 118 serialization::writePod(outputFile, frontButtonRight); 119 119 serialization::writePod(outputFile, fadingFix); 120 + serialization::writePod(outputFile, embeddedStyle); 120 121 // New fields added at end for backward compatibility 121 122 outputFile.close(); 122 123 ··· 219 220 frontButtonMappingRead = true; 220 221 if (++settingsRead >= fileSettingsCount) break; 221 222 serialization::readPod(inputFile, fadingFix); 223 + if (++settingsRead >= fileSettingsCount) break; 224 + serialization::readPod(inputFile, embeddedStyle); 222 225 if (++settingsRead >= fileSettingsCount) break; 223 226 // New fields added at end for backward compatibility 224 227 } while (false);
+3
src/CrossPointSettings.h
··· 86 86 LEFT_ALIGN = 1, 87 87 CENTER_ALIGN = 2, 88 88 RIGHT_ALIGN = 3, 89 + BOOK_STYLE = 4, 89 90 PARAGRAPH_ALIGNMENT_COUNT 90 91 }; 91 92 ··· 168 169 uint8_t uiTheme = LYRA; 169 170 // Sunlight fading compensation 170 171 uint8_t fadingFix = 0; 172 + // Use book's embedded CSS styles for EPUB rendering (1 = enabled, 0 = disabled) 173 + uint8_t embeddedStyle = 1; 171 174 172 175 ~CrossPointSettings() = default; 173 176
+2 -2
src/activities/reader/EpubReaderActivity.cpp
··· 575 575 576 576 if (!section->loadSectionFile(SETTINGS.getReaderFontId(), SETTINGS.getReaderLineCompression(), 577 577 SETTINGS.extraParagraphSpacing, SETTINGS.paragraphAlignment, viewportWidth, 578 - viewportHeight, SETTINGS.hyphenationEnabled)) { 578 + viewportHeight, SETTINGS.hyphenationEnabled, SETTINGS.embeddedStyle)) { 579 579 Serial.printf("[%lu] [ERS] Cache not found, building...\n", millis()); 580 580 581 581 const auto popupFn = [this]() { GUI.drawPopup(renderer, "Indexing..."); }; 582 582 583 583 if (!section->createSectionFile(SETTINGS.getReaderFontId(), SETTINGS.getReaderLineCompression(), 584 584 SETTINGS.extraParagraphSpacing, SETTINGS.paragraphAlignment, viewportWidth, 585 - viewportHeight, SETTINGS.hyphenationEnabled, popupFn)) { 585 + viewportHeight, SETTINGS.hyphenationEnabled, SETTINGS.embeddedStyle, popupFn)) { 586 586 Serial.printf("[%lu] [ERS] Failed to persist page data to SD\n", millis()); 587 587 section.reset(); 588 588 return;
+3 -2
src/activities/settings/SettingsActivity.cpp
··· 35 35 SettingInfo::Toggle("Sunlight Fading Fix", &CrossPointSettings::fadingFix), 36 36 }; 37 37 38 - constexpr int readerSettingsCount = 9; 38 + constexpr int readerSettingsCount = 10; 39 39 const SettingInfo readerSettings[readerSettingsCount] = { 40 40 SettingInfo::Enum("Font Family", &CrossPointSettings::fontFamily, {"Bookerly", "Noto Sans", "Open Dyslexic"}), 41 41 SettingInfo::Enum("Font Size", &CrossPointSettings::fontSize, {"Small", "Medium", "Large", "X Large"}), 42 42 SettingInfo::Enum("Line Spacing", &CrossPointSettings::lineSpacing, {"Tight", "Normal", "Wide"}), 43 43 SettingInfo::Value("Screen Margin", &CrossPointSettings::screenMargin, {5, 40, 5}), 44 44 SettingInfo::Enum("Paragraph Alignment", &CrossPointSettings::paragraphAlignment, 45 - {"Justify", "Left", "Center", "Right"}), 45 + {"Justify", "Left", "Center", "Right", "Book's Style"}), 46 + SettingInfo::Toggle("Embedded Style", &CrossPointSettings::embeddedStyle), 46 47 SettingInfo::Toggle("Hyphenation", &CrossPointSettings::hyphenationEnabled), 47 48 SettingInfo::Enum("Reading Orientation", &CrossPointSettings::orientation, 48 49 {"Portrait", "Landscape CW", "Inverted", "Landscape CCW"}),