From 440bf9e7334e90b38d38c40413ae90a2b407fe59 Mon Sep 17 00:00:00 2001 From: Jonas Diemer Date: Sun, 1 Feb 2026 13:16:53 +0100 Subject: [PATCH 1/3] Added option to increase word spacing. --- lib/Epub/Epub/ParsedText.cpp | 5 ++-- lib/Epub/Epub/ParsedText.h | 10 +++++--- lib/Epub/Epub/Section.cpp | 23 +++++++++++-------- lib/Epub/Epub/Section.h | 7 +++--- .../Epub/parsers/ChapterHtmlSlimParser.cpp | 2 +- lib/Epub/Epub/parsers/ChapterHtmlSlimParser.h | 3 +++ src/CrossPointSettings.cpp | 5 +++- src/CrossPointSettings.h | 1 + src/activities/reader/EpubReaderActivity.cpp | 4 ++-- .../settings/CategorySettingsActivity.cpp | 2 +- src/activities/settings/SettingsActivity.cpp | 3 ++- 11 files changed, 42 insertions(+), 23 deletions(-) diff --git a/lib/Epub/Epub/ParsedText.cpp b/lib/Epub/Epub/ParsedText.cpp index 81d688ec..0bbc23b2 100644 --- a/lib/Epub/Epub/ParsedText.cpp +++ b/lib/Epub/Epub/ParsedText.cpp @@ -68,7 +68,8 @@ void ParsedText::layoutAndExtractLines(const GfxRenderer& renderer, const int fo applyParagraphIndent(); const int pageWidth = viewportWidth; - const int spaceWidth = renderer.getSpaceWidth(fontId); + const int spaceWidth = static_cast(wordSpacing) / 100.0 * renderer.getSpaceWidth(fontId); + Serial.printf("[%lu] [PaT] wordSpacing: %d spaceWidth: %d\n", millis(), wordSpacing, spaceWidth); auto wordWidths = calculateWordWidths(renderer, fontId); std::vector lineBreakIndices; if (hyphenationEnabled) { @@ -385,4 +386,4 @@ void ParsedText::extractLine(const size_t breakIndex, const int pageWidth, const } processLine(std::make_shared(std::move(lineWords), std::move(lineXPos), std::move(lineWordStyles), style)); -} \ No newline at end of file +} diff --git a/lib/Epub/Epub/ParsedText.h b/lib/Epub/Epub/ParsedText.h index e72db7ef..0c2408d1 100644 --- a/lib/Epub/Epub/ParsedText.h +++ b/lib/Epub/Epub/ParsedText.h @@ -18,6 +18,7 @@ class ParsedText { TextBlock::Style style; bool extraParagraphSpacing; bool hyphenationEnabled; + uint8_t wordSpacing; void applyParagraphIndent(); std::vector computeLineBreaks(const GfxRenderer& renderer, int fontId, int pageWidth, int spaceWidth, @@ -33,8 +34,11 @@ class ParsedText { public: explicit ParsedText(const TextBlock::Style style, const bool extraParagraphSpacing, - const bool hyphenationEnabled = false) - : style(style), extraParagraphSpacing(extraParagraphSpacing), hyphenationEnabled(hyphenationEnabled) {} + const bool hyphenationEnabled = false, const uint8_t wordSpacing = 100) + : style(style), + extraParagraphSpacing(extraParagraphSpacing), + hyphenationEnabled(hyphenationEnabled), + wordSpacing(wordSpacing) {} ~ParsedText() = default; void addWord(std::string word, EpdFontFamily::Style fontStyle); @@ -45,4 +49,4 @@ class ParsedText { void layoutAndExtractLines(const GfxRenderer& renderer, int fontId, uint16_t viewportWidth, const std::function)>& processLine, bool includeLastLine = true); -}; \ No newline at end of file +}; diff --git a/lib/Epub/Epub/Section.cpp b/lib/Epub/Epub/Section.cpp index cf67108b..7939275a 100644 --- a/lib/Epub/Epub/Section.cpp +++ b/lib/Epub/Epub/Section.cpp @@ -8,10 +8,10 @@ #include "parsers/ChapterHtmlSlimParser.h" namespace { -constexpr uint8_t SECTION_FILE_VERSION = 10; +constexpr uint8_t SECTION_FILE_VERSION = 11; 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(uint32_t); + sizeof(uint32_t) + sizeof(uint8_t); } // namespace uint32_t Section::onPageComplete(std::unique_ptr page) { @@ -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 uint8_t wordSpacing) { 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(uint32_t) + sizeof(wordSpacing), "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, wordSpacing); 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, uint8_t wordSpacing) { if (!SdMan.openFileForRead("SCT", filePath, file)) { return false; } @@ -79,6 +81,8 @@ bool Section::loadSectionFile(const int fontId, const float lineCompression, con bool fileExtraParagraphSpacing; uint8_t fileParagraphAlignment; bool fileHyphenationEnabled; + uint8_t fileWordSpacing; + serialization::readPod(file, fileFontId); serialization::readPod(file, fileLineCompression); serialization::readPod(file, fileExtraParagraphSpacing); @@ -86,11 +90,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, fileWordSpacing); if (fontId != fileFontId || lineCompression != fileLineCompression || extraParagraphSpacing != fileExtraParagraphSpacing || paragraphAlignment != fileParagraphAlignment || viewportWidth != fileViewportWidth || viewportHeight != fileViewportHeight || - hyphenationEnabled != fileHyphenationEnabled) { + hyphenationEnabled != fileHyphenationEnabled || wordSpacing != fileWordSpacing) { file.close(); Serial.printf("[%lu] [SCT] Deserialization failed: Parameters do not match\n", millis()); clearCache(); @@ -122,7 +127,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 uint8_t wordSpacing, const std::function& popupFn) { const auto localPath = epub->getSpineItem(spineIndex).href; const auto tmpHtmlPath = epub->getCachePath() + "/.tmp_" + std::to_string(spineIndex) + ".html"; @@ -173,12 +178,12 @@ bool Section::createSectionFile(const int fontId, const float lineCompression, c return false; } writeSectionFileHeader(fontId, lineCompression, extraParagraphSpacing, paragraphAlignment, viewportWidth, - viewportHeight, hyphenationEnabled); + viewportHeight, hyphenationEnabled, wordSpacing); std::vector lut = {}; ChapterHtmlSlimParser visitor( tmpHtmlPath, renderer, fontId, lineCompression, extraParagraphSpacing, paragraphAlignment, viewportWidth, - viewportHeight, hyphenationEnabled, + viewportHeight, hyphenationEnabled, wordSpacing, [this, &lut](std::unique_ptr page) { lut.emplace_back(this->onPageComplete(std::move(page))); }, popupFn); Hyphenator::setPreferredLanguage(epub->getLanguage()); success = visitor.parseAndBuildPages(); diff --git a/lib/Epub/Epub/Section.h b/lib/Epub/Epub/Section.h index 5fdf210a..d9492e9a 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, + uint8_t wordSpacing); 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, uint8_t wordSpacing); 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, uint8_t wordSpacing, const std::function& popupFn = nullptr); std::unique_ptr loadPageFromSectionFile(); }; diff --git a/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp b/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp index ac1f537f..6e46d866 100644 --- a/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp +++ b/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp @@ -68,7 +68,7 @@ void ChapterHtmlSlimParser::startNewTextBlock(const TextBlock::Style style) { makePages(); } - currentTextBlock.reset(new ParsedText(style, extraParagraphSpacing, hyphenationEnabled)); + currentTextBlock.reset(new ParsedText(style, extraParagraphSpacing, hyphenationEnabled, wordSpacing)); } void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char* name, const XML_Char** atts) { diff --git a/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.h b/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.h index 38202e6e..7f16ec3f 100644 --- a/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.h +++ b/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.h @@ -37,6 +37,7 @@ class ChapterHtmlSlimParser { uint16_t viewportWidth; uint16_t viewportHeight; bool hyphenationEnabled; + uint8_t wordSpacing; void startNewTextBlock(TextBlock::Style style); void flushPartWordBuffer(); @@ -51,12 +52,14 @@ class ChapterHtmlSlimParser { const float lineCompression, const bool extraParagraphSpacing, const uint8_t paragraphAlignment, const uint16_t viewportWidth, const uint16_t viewportHeight, const bool hyphenationEnabled, + const uint8_t wordSpacing, const std::function)>& completePageFn, const std::function& popupFn = nullptr) : filepath(filepath), renderer(renderer), fontId(fontId), lineCompression(lineCompression), + wordSpacing(wordSpacing), extraParagraphSpacing(extraParagraphSpacing), paragraphAlignment(paragraphAlignment), viewportWidth(viewportWidth), diff --git a/src/CrossPointSettings.cpp b/src/CrossPointSettings.cpp index 232c7c57..14272fe1 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 = 23; +constexpr uint8_t SETTINGS_COUNT = 24; constexpr char SETTINGS_FILE[] = "/.crosspoint/settings.bin"; } // namespace @@ -60,6 +60,7 @@ bool CrossPointSettings::saveToFile() const { serialization::writeString(outputFile, std::string(opdsUsername)); serialization::writeString(outputFile, std::string(opdsPassword)); serialization::writePod(outputFile, sleepScreenCoverFilter); + serialization::writePod(outputFile, wordSpacing); // New fields added at end for backward compatibility outputFile.close(); @@ -148,6 +149,8 @@ bool CrossPointSettings::loadFromFile() { if (++settingsRead >= fileSettingsCount) break; readAndValidate(inputFile, sleepScreenCoverFilter, SLEEP_SCREEN_COVER_FILTER_COUNT); if (++settingsRead >= fileSettingsCount) break; + readAndValidate(inputFile, wordSpacing, 255); + 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 c450d348..779a3bba 100644 --- a/src/CrossPointSettings.h +++ b/src/CrossPointSettings.h @@ -107,6 +107,7 @@ class CrossPointSettings { uint8_t statusBar = FULL; // Text rendering settings uint8_t extraParagraphSpacing = 1; + uint8_t wordSpacing = 100; 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 17d81632..84fc7402 100644 --- a/src/activities/reader/EpubReaderActivity.cpp +++ b/src/activities/reader/EpubReaderActivity.cpp @@ -366,14 +366,14 @@ void EpubReaderActivity::renderScreen() { if (!section->loadSectionFile(SETTINGS.getReaderFontId(), SETTINGS.getReaderLineCompression(), SETTINGS.extraParagraphSpacing, SETTINGS.paragraphAlignment, viewportWidth, - viewportHeight, SETTINGS.hyphenationEnabled)) { + viewportHeight, SETTINGS.hyphenationEnabled, SETTINGS.wordSpacing)) { Serial.printf("[%lu] [ERS] Cache not found, building...\n", millis()); const auto popupFn = [this]() { ScreenComponents::drawPopup(renderer, "Indexing..."); }; if (!section->createSectionFile(SETTINGS.getReaderFontId(), SETTINGS.getReaderLineCompression(), SETTINGS.extraParagraphSpacing, SETTINGS.paragraphAlignment, viewportWidth, - viewportHeight, SETTINGS.hyphenationEnabled, popupFn)) { + viewportHeight, SETTINGS.hyphenationEnabled, SETTINGS.wordSpacing, popupFn)) { Serial.printf("[%lu] [ERS] Failed to persist page data to SD\n", millis()); section.reset(); return; diff --git a/src/activities/settings/CategorySettingsActivity.cpp b/src/activities/settings/CategorySettingsActivity.cpp index 7fd5ef5f..9db80c3d 100644 --- a/src/activities/settings/CategorySettingsActivity.cpp +++ b/src/activities/settings/CategorySettingsActivity.cpp @@ -88,7 +88,7 @@ void CategorySettingsActivity::toggleCurrentSetting() { const uint8_t currentValue = SETTINGS.*(setting.valuePtr); SETTINGS.*(setting.valuePtr) = (currentValue + 1) % static_cast(setting.enumValues.size()); } else if (setting.type == SettingType::VALUE && setting.valuePtr != nullptr) { - const int8_t currentValue = SETTINGS.*(setting.valuePtr); + const uint8_t currentValue = SETTINGS.*(setting.valuePtr); if (currentValue + setting.valueRange.step > setting.valueRange.max) { SETTINGS.*(setting.valuePtr) = setting.valueRange.min; } else { diff --git a/src/activities/settings/SettingsActivity.cpp b/src/activities/settings/SettingsActivity.cpp index 7316db05..44b896bd 100644 --- a/src/activities/settings/SettingsActivity.cpp +++ b/src/activities/settings/SettingsActivity.cpp @@ -24,11 +24,12 @@ const SettingInfo displaySettings[displaySettingsCount] = { SettingInfo::Enum("Refresh Frequency", &CrossPointSettings::refreshFrequency, {"1 page", "5 pages", "10 pages", "15 pages", "30 pages"})}; -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("Word Spacing %", &CrossPointSettings::wordSpacing, {100, 250, 25}), SettingInfo::Value("Screen Margin", &CrossPointSettings::screenMargin, {5, 40, 5}), SettingInfo::Enum("Paragraph Alignment", &CrossPointSettings::paragraphAlignment, {"Justify", "Left", "Center", "Right"}), From 1a7fed2a695da8c9792a6ce04ee5995246860e0f Mon Sep 17 00:00:00 2001 From: Jonas Diemer Date: Sun, 1 Feb 2026 13:20:33 +0100 Subject: [PATCH 2/3] Allow also 75%. --- src/activities/settings/SettingsActivity.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/activities/settings/SettingsActivity.cpp b/src/activities/settings/SettingsActivity.cpp index 44b896bd..15da593e 100644 --- a/src/activities/settings/SettingsActivity.cpp +++ b/src/activities/settings/SettingsActivity.cpp @@ -29,7 +29,7 @@ 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("Word Spacing %", &CrossPointSettings::wordSpacing, {100, 250, 25}), + SettingInfo::Value("Word Spacing %", &CrossPointSettings::wordSpacing, {75, 250, 25}), SettingInfo::Value("Screen Margin", &CrossPointSettings::screenMargin, {5, 40, 5}), SettingInfo::Enum("Paragraph Alignment", &CrossPointSettings::paragraphAlignment, {"Justify", "Left", "Center", "Right"}), From 2f57565b9dbf8c6f359003bf23ca7b70d5829e22 Mon Sep 17 00:00:00 2001 From: Jonas Diemer Date: Sun, 1 Feb 2026 13:49:52 +0100 Subject: [PATCH 3/3] Round correctly and remove debug print. --- lib/Epub/Epub/ParsedText.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/Epub/Epub/ParsedText.cpp b/lib/Epub/Epub/ParsedText.cpp index 0bbc23b2..01f7aafc 100644 --- a/lib/Epub/Epub/ParsedText.cpp +++ b/lib/Epub/Epub/ParsedText.cpp @@ -68,8 +68,7 @@ void ParsedText::layoutAndExtractLines(const GfxRenderer& renderer, const int fo applyParagraphIndent(); const int pageWidth = viewportWidth; - const int spaceWidth = static_cast(wordSpacing) / 100.0 * renderer.getSpaceWidth(fontId); - Serial.printf("[%lu] [PaT] wordSpacing: %d spaceWidth: %d\n", millis(), wordSpacing, spaceWidth); + const int spaceWidth = std::round(static_cast(wordSpacing) / 100.0 * renderer.getSpaceWidth(fontId)); auto wordWidths = calculateWordWidths(renderer, fontId); std::vector lineBreakIndices; if (hyphenationEnabled) {