From 854b63fa8ac4a1a7a195daf1e246745975bbeb6c Mon Sep 17 00:00:00 2001 From: warren Date: Sat, 17 Jan 2026 20:31:34 +0800 Subject: [PATCH] Convert paragraph indent and spacing from boolean toggles to configurable ranges --- lib/Epub/Epub/ParsedText.cpp | 8 ++-- lib/Epub/Epub/ParsedText.h | 8 ++-- lib/Epub/Epub/Section.cpp | 47 +++++++++++-------- lib/Epub/Epub/Section.h | 12 ++--- .../Epub/parsers/ChapterHtmlSlimParser.cpp | 11 +++-- lib/Epub/Epub/parsers/ChapterHtmlSlimParser.h | 10 ++-- src/CrossPointSettings.cpp | 5 +- src/CrossPointSettings.h | 17 ++++++- src/activities/reader/EpubReaderActivity.cpp | 9 ++-- src/activities/settings/SettingsActivity.cpp | 7 ++- 10 files changed, 85 insertions(+), 49 deletions(-) diff --git a/lib/Epub/Epub/ParsedText.cpp b/lib/Epub/Epub/ParsedText.cpp index 3c37e31b..19395d5f 100644 --- a/lib/Epub/Epub/ParsedText.cpp +++ b/lib/Epub/Epub/ParsedText.cpp @@ -42,10 +42,12 @@ std::vector ParsedText::calculateWordWidths(const GfxRenderer& rendere std::vector wordWidths; wordWidths.reserve(totalWordCount); - // add em-space at the beginning of first word in paragraph to indent - if ((style == TextBlock::JUSTIFIED || style == TextBlock::LEFT_ALIGN) && !extraParagraphSpacing) { + // add em-space at the beginning of first word in paragraph to indent (independent of paragraph spacing) + if ((style == TextBlock::JUSTIFIED || style == TextBlock::LEFT_ALIGN) && indentParagraph > 0) { std::string& first_word = words.front(); - first_word.insert(0, "\xe2\x80\x83"); + for (uint8_t i = 0; i < indentParagraph; ++i) { + first_word.insert(0, "\xe2\x80\x83"); // em-space + } } auto wordsIt = words.begin(); diff --git a/lib/Epub/Epub/ParsedText.h b/lib/Epub/Epub/ParsedText.h index 4b851a94..e8e55f64 100644 --- a/lib/Epub/Epub/ParsedText.h +++ b/lib/Epub/Epub/ParsedText.h @@ -16,7 +16,8 @@ class ParsedText { std::list words; std::list wordStyles; TextBlock::Style style; - bool extraParagraphSpacing; + uint8_t extraParagraphSpacing; + uint8_t indentParagraph; std::vector computeLineBreaks(int pageWidth, int spaceWidth, const std::vector& wordWidths) const; void extractLine(size_t breakIndex, int pageWidth, int spaceWidth, const std::vector& wordWidths, @@ -25,8 +26,9 @@ class ParsedText { std::vector calculateWordWidths(const GfxRenderer& renderer, int fontId); public: - explicit ParsedText(const TextBlock::Style style, const bool extraParagraphSpacing) - : style(style), extraParagraphSpacing(extraParagraphSpacing) {} + explicit ParsedText(const TextBlock::Style style, const uint8_t extraParagraphSpacing, + const uint8_t indentParagraph = 1) + : style(style), extraParagraphSpacing(extraParagraphSpacing), indentParagraph(indentParagraph) {} ~ParsedText() = default; void addWord(std::string word, EpdFontFamily::Style fontStyle); diff --git a/lib/Epub/Epub/Section.cpp b/lib/Epub/Epub/Section.cpp index 18b81aae..b4736fdc 100644 --- a/lib/Epub/Epub/Section.cpp +++ b/lib/Epub/Epub/Section.cpp @@ -8,8 +8,9 @@ namespace { constexpr uint8_t SECTION_FILE_VERSION = 9; -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(uint32_t); +constexpr uint32_t HEADER_SIZE = sizeof(uint8_t) + sizeof(int) + sizeof(float) + sizeof(bool) + sizeof(bool) + + sizeof(uint8_t) + sizeof(uint16_t) + sizeof(uint16_t) + sizeof(uint16_t) + + sizeof(uint32_t); } // namespace uint32_t Section::onPageComplete(std::unique_ptr page) { @@ -29,21 +30,23 @@ uint32_t Section::onPageComplete(std::unique_ptr page) { return position; } -void Section::writeSectionFileHeader(const int fontId, const float lineCompression, const bool extraParagraphSpacing, - const uint8_t paragraphAlignment, const uint16_t viewportWidth, - const uint16_t viewportHeight) { +void Section::writeSectionFileHeader(const int fontId, const float lineCompression, const uint8_t extraParagraphSpacing, + const uint8_t indentParagraph, const uint8_t paragraphAlignment, + const uint16_t viewportWidth, const uint16_t viewportHeight) { if (!file) { Serial.printf("[%lu] [SCT] File not open for writing header\n", millis()); return; } static_assert(HEADER_SIZE == sizeof(SECTION_FILE_VERSION) + sizeof(fontId) + sizeof(lineCompression) + - sizeof(extraParagraphSpacing) + sizeof(paragraphAlignment) + sizeof(viewportWidth) + - sizeof(viewportHeight) + sizeof(pageCount) + sizeof(uint32_t), + sizeof(extraParagraphSpacing) + sizeof(indentParagraph) + + sizeof(paragraphAlignment) + sizeof(viewportWidth) + sizeof(viewportHeight) + + sizeof(pageCount) + sizeof(uint32_t), "Header size mismatch"); serialization::writePod(file, SECTION_FILE_VERSION); serialization::writePod(file, fontId); serialization::writePod(file, lineCompression); serialization::writePod(file, extraParagraphSpacing); + serialization::writePod(file, indentParagraph); serialization::writePod(file, paragraphAlignment); serialization::writePod(file, viewportWidth); serialization::writePod(file, viewportHeight); @@ -51,9 +54,9 @@ void Section::writeSectionFileHeader(const int fontId, const float lineCompressi 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) { +bool Section::loadSectionFile(const int fontId, const float lineCompression, const uint8_t extraParagraphSpacing, + const uint8_t indentParagraph, const uint8_t paragraphAlignment, + const uint16_t viewportWidth, const uint16_t viewportHeight) { if (!SdMan.openFileForRead("SCT", filePath, file)) { return false; } @@ -72,18 +75,21 @@ bool Section::loadSectionFile(const int fontId, const float lineCompression, con int fileFontId; uint16_t fileViewportWidth, fileViewportHeight; float fileLineCompression; - bool fileExtraParagraphSpacing; + uint8_t fileExtraParagraphSpacing; + uint8_t fileIndentParagraph; uint8_t fileParagraphAlignment; serialization::readPod(file, fileFontId); serialization::readPod(file, fileLineCompression); serialization::readPod(file, fileExtraParagraphSpacing); + serialization::readPod(file, fileIndentParagraph); serialization::readPod(file, fileParagraphAlignment); serialization::readPod(file, fileViewportWidth); serialization::readPod(file, fileViewportHeight); if (fontId != fileFontId || lineCompression != fileLineCompression || - extraParagraphSpacing != fileExtraParagraphSpacing || paragraphAlignment != fileParagraphAlignment || - viewportWidth != fileViewportWidth || viewportHeight != fileViewportHeight) { + extraParagraphSpacing != fileExtraParagraphSpacing || indentParagraph != fileIndentParagraph || + paragraphAlignment != fileParagraphAlignment || viewportWidth != fileViewportWidth || + viewportHeight != fileViewportHeight) { file.close(); Serial.printf("[%lu] [SCT] Deserialization failed: Parameters do not match\n", millis()); clearCache(); @@ -113,9 +119,10 @@ bool Section::clearCache() const { return true; } -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 std::function& progressSetupFn, +bool Section::createSectionFile(const int fontId, const float lineCompression, const uint8_t extraParagraphSpacing, + const uint8_t indentParagraph, const uint8_t paragraphAlignment, + const uint16_t viewportWidth, const uint16_t viewportHeight, + const std::function& progressSetupFn, const std::function& progressFn) { constexpr uint32_t MIN_SIZE_FOR_PROGRESS = 50 * 1024; // 50KB const auto localPath = epub->getSpineItem(spineIndex).href; @@ -171,13 +178,13 @@ bool Section::createSectionFile(const int fontId, const float lineCompression, c if (!SdMan.openFileForWrite("SCT", filePath, file)) { return false; } - writeSectionFileHeader(fontId, lineCompression, extraParagraphSpacing, paragraphAlignment, viewportWidth, - viewportHeight); + writeSectionFileHeader(fontId, lineCompression, extraParagraphSpacing, indentParagraph, paragraphAlignment, + viewportWidth, viewportHeight); std::vector lut = {}; ChapterHtmlSlimParser visitor( - tmpHtmlPath, renderer, fontId, lineCompression, extraParagraphSpacing, paragraphAlignment, viewportWidth, - viewportHeight, + tmpHtmlPath, renderer, fontId, lineCompression, extraParagraphSpacing, indentParagraph, paragraphAlignment, + viewportWidth, viewportHeight, [this, &lut](std::unique_ptr page) { lut.emplace_back(this->onPageComplete(std::move(page))); }, progressFn); success = visitor.parseAndBuildPages(); diff --git a/lib/Epub/Epub/Section.h b/lib/Epub/Epub/Section.h index bac95efd..21892e49 100644 --- a/lib/Epub/Epub/Section.h +++ b/lib/Epub/Epub/Section.h @@ -14,8 +14,8 @@ class Section { std::string filePath; FsFile file; - void writeSectionFileHeader(int fontId, float lineCompression, bool extraParagraphSpacing, uint8_t paragraphAlignment, - uint16_t viewportWidth, uint16_t viewportHeight); + void writeSectionFileHeader(int fontId, float lineCompression, uint8_t extraParagraphSpacing, uint8_t indentParagraph, + uint8_t paragraphAlignment, uint16_t viewportWidth, uint16_t viewportHeight); uint32_t onPageComplete(std::unique_ptr page); public: @@ -28,11 +28,11 @@ class Section { renderer(renderer), 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 loadSectionFile(int fontId, float lineCompression, uint8_t extraParagraphSpacing, uint8_t indentParagraph, + uint8_t paragraphAlignment, uint16_t viewportWidth, uint16_t viewportHeight); bool clearCache() const; - bool createSectionFile(int fontId, float lineCompression, bool extraParagraphSpacing, uint8_t paragraphAlignment, - uint16_t viewportWidth, uint16_t viewportHeight, + bool createSectionFile(int fontId, float lineCompression, uint8_t extraParagraphSpacing, uint8_t indentParagraph, + uint8_t paragraphAlignment, uint16_t viewportWidth, uint16_t viewportHeight, const std::function& progressSetupFn = nullptr, const std::function& progressFn = nullptr); std::unique_ptr loadPageFromSectionFile(); diff --git a/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp b/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp index acddd81d..8fdbc3a7 100644 --- a/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp +++ b/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp @@ -51,7 +51,7 @@ void ChapterHtmlSlimParser::startNewTextBlock(const TextBlock::Style style) { makePages(); } - currentTextBlock.reset(new ParsedText(style, extraParagraphSpacing)); + currentTextBlock.reset(new ParsedText(style, extraParagraphSpacing, indentParagraph)); } void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char* name, const XML_Char** atts) { @@ -387,8 +387,11 @@ void ChapterHtmlSlimParser::makePages() { currentTextBlock->layoutAndExtractLines( renderer, fontId, viewportWidth, [this](const std::shared_ptr& textBlock) { addLineToPage(textBlock); }); - // Extra paragraph spacing if enabled - if (extraParagraphSpacing) { - currentPageNextY += lineHeight / 2; + // Apply paragraph spacing: 0->0%, 1->30%, 2->50%, 3->80%, 4->100%, 5->120%, 6->140% + if (extraParagraphSpacing > 0) { + const float spacingMultipliers[] = {0.0f, 0.3f, 0.5f, 0.8f, 1.0f, 1.2f, 1.4f}; + const float spacingMultiplier = spacingMultipliers[extraParagraphSpacing]; + const int spacingAmount = static_cast(lineHeight * spacingMultiplier); + currentPageNextY += spacingAmount; } } diff --git a/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.h b/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.h index c559e157..accee555 100644 --- a/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.h +++ b/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.h @@ -32,7 +32,8 @@ class ChapterHtmlSlimParser { int16_t currentPageNextY = 0; int fontId; float lineCompression; - bool extraParagraphSpacing; + uint8_t extraParagraphSpacing; + uint8_t indentParagraph; uint8_t paragraphAlignment; uint16_t viewportWidth; uint16_t viewportHeight; @@ -46,9 +47,9 @@ class ChapterHtmlSlimParser { public: explicit ChapterHtmlSlimParser(const std::string& filepath, GfxRenderer& renderer, const int fontId, - const float lineCompression, const bool extraParagraphSpacing, - const uint8_t paragraphAlignment, const uint16_t viewportWidth, - const uint16_t viewportHeight, + const float lineCompression, const uint8_t extraParagraphSpacing, + const uint8_t indentParagraph, const uint8_t paragraphAlignment, + const uint16_t viewportWidth, const uint16_t viewportHeight, const std::function)>& completePageFn, const std::function& progressFn = nullptr) : filepath(filepath), @@ -56,6 +57,7 @@ class ChapterHtmlSlimParser { fontId(fontId), lineCompression(lineCompression), extraParagraphSpacing(extraParagraphSpacing), + indentParagraph(indentParagraph), paragraphAlignment(paragraphAlignment), viewportWidth(viewportWidth), viewportHeight(viewportHeight), diff --git a/src/CrossPointSettings.cpp b/src/CrossPointSettings.cpp index 17b5d053..ace4db7e 100644 --- a/src/CrossPointSettings.cpp +++ b/src/CrossPointSettings.cpp @@ -14,7 +14,7 @@ CrossPointSettings CrossPointSettings::instance; namespace { constexpr uint8_t SETTINGS_FILE_VERSION = 1; // Increment this when adding new persisted settings fields -constexpr uint8_t SETTINGS_COUNT = 18; +constexpr uint8_t SETTINGS_COUNT = 19; constexpr char SETTINGS_FILE[] = "/.crosspoint/settings.bin"; } // namespace @@ -48,6 +48,7 @@ bool CrossPointSettings::saveToFile() const { serialization::writePod(outputFile, textAntiAliasing); serialization::writePod(outputFile, hideBatteryPercentage); serialization::writePod(outputFile, longPressChapterSkip); + serialization::writePod(outputFile, indentParagraph); outputFile.close(); Serial.printf("[%lu] [CPS] Settings saved to file\n", millis()); @@ -116,6 +117,8 @@ bool CrossPointSettings::loadFromFile() { if (++settingsRead >= fileSettingsCount) break; serialization::readPod(inputFile, longPressChapterSkip); if (++settingsRead >= fileSettingsCount) break; + serialization::readPod(inputFile, indentParagraph); + if (++settingsRead >= fileSettingsCount) break; } while (false); inputFile.close(); diff --git a/src/CrossPointSettings.h b/src/CrossPointSettings.h index a5641aad..ee925dd5 100644 --- a/src/CrossPointSettings.h +++ b/src/CrossPointSettings.h @@ -58,14 +58,27 @@ class CrossPointSettings { // Hide battery percentage enum HIDE_BATTERY_PERCENTAGE { HIDE_NEVER = 0, HIDE_READER = 1, HIDE_ALWAYS = 2 }; + // Paragraph spacing options + enum PARAGRAPH_SPACING { + SPACING_NONE = 0, + SPACING_0_3X = 1, + SPACING_0_5X = 2, + SPACING_0_8X = 3, + SPACING_1_0X = 4, + SPACING_1_2X = 5, + SPACING_1_4X = 6 + }; + // Sleep screen settings uint8_t sleepScreen = DARK; // Sleep screen cover mode settings uint8_t sleepScreenCoverMode = FIT; // Status bar settings uint8_t statusBar = FULL; - // Text rendering settings - uint8_t extraParagraphSpacing = 1; + // Text rendering settings 0-3: number of em-spaces to indent paragraph + uint8_t indentParagraph = 1; + // 0-6: spacing (0, 0.3x, 0.5x, 0.8x, 1x, 1.2x, 1.4x) + uint8_t extraParagraphSpacing = SPACING_1_0X; uint8_t textAntiAliasing = 1; // Short power button click behaviour uint8_t shortPwrBtn = IGNORE; diff --git a/src/activities/reader/EpubReaderActivity.cpp b/src/activities/reader/EpubReaderActivity.cpp index 2eeba80f..87b4e0dd 100644 --- a/src/activities/reader/EpubReaderActivity.cpp +++ b/src/activities/reader/EpubReaderActivity.cpp @@ -267,8 +267,8 @@ void EpubReaderActivity::renderScreen() { const uint16_t viewportHeight = renderer.getScreenHeight() - orientedMarginTop - orientedMarginBottom; if (!section->loadSectionFile(SETTINGS.getReaderFontId(), SETTINGS.getReaderLineCompression(), - SETTINGS.extraParagraphSpacing, SETTINGS.paragraphAlignment, viewportWidth, - viewportHeight)) { + SETTINGS.extraParagraphSpacing, SETTINGS.indentParagraph, SETTINGS.paragraphAlignment, + viewportWidth, viewportHeight)) { Serial.printf("[%lu] [ERS] Cache not found, building...\n", millis()); // Progress bar dimensions @@ -312,8 +312,9 @@ void EpubReaderActivity::renderScreen() { }; if (!section->createSectionFile(SETTINGS.getReaderFontId(), SETTINGS.getReaderLineCompression(), - SETTINGS.extraParagraphSpacing, SETTINGS.paragraphAlignment, viewportWidth, - viewportHeight, progressSetup, progressCallback)) { + SETTINGS.extraParagraphSpacing, SETTINGS.indentParagraph, + SETTINGS.paragraphAlignment, viewportWidth, viewportHeight, progressSetup, + progressCallback)) { 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 efa0b9e1..5fabff4f 100644 --- a/src/activities/settings/SettingsActivity.cpp +++ b/src/activities/settings/SettingsActivity.cpp @@ -13,14 +13,17 @@ // Define the static settings list namespace { -constexpr int settingsCount = 20; +constexpr int settingsCount = 21; const SettingInfo settingsList[settingsCount] = { // Should match with SLEEP_SCREEN_MODE SettingInfo::Enum("Sleep Screen", &CrossPointSettings::sleepScreen, {"Dark", "Light", "Custom", "Cover", "None"}), SettingInfo::Enum("Sleep Screen Cover Mode", &CrossPointSettings::sleepScreenCoverMode, {"Fit", "Crop"}), SettingInfo::Enum("Status Bar", &CrossPointSettings::statusBar, {"None", "No Progress", "Full"}), SettingInfo::Enum("Hide Battery %", &CrossPointSettings::hideBatteryPercentage, {"Never", "In Reader", "Always"}), - SettingInfo::Toggle("Extra Paragraph Spacing", &CrossPointSettings::extraParagraphSpacing), + SettingInfo::Value("Paragraph Indent", &CrossPointSettings::indentParagraph, {0, 3, 1}), + SettingInfo::Enum("Paragraph Spacing", &CrossPointSettings::extraParagraphSpacing, + {"None", "0.3 LineHeight", "0.5 LineHeight", "0.8 LineHeight", "1.0 LineHeight", "1.2 LineHeight", + "1.4 LineHeight"}), SettingInfo::Toggle("Text Anti-Aliasing", &CrossPointSettings::textAntiAliasing), SettingInfo::Enum("Short Power Button Click", &CrossPointSettings::shortPwrBtn, {"Ignore", "Sleep", "Page Turn"}), SettingInfo::Enum("Reading Orientation", &CrossPointSettings::orientation,