From 100724fd2c74a928efa8bbd8be4267e197c701e7 Mon Sep 17 00:00:00 2001 From: Arthur Tazhitdinov Date: Thu, 29 Jan 2026 02:19:46 +0500 Subject: [PATCH 1/4] fix: improve UTF-8 safe text truncation in GfxRenderer and update Epub/TxtReaderActivity to use new method --- lib/GfxRenderer/GfxRenderer.cpp | 39 ++++++++++++++++++-- src/activities/reader/EpubReaderActivity.cpp | 4 +- src/activities/reader/TxtReaderActivity.cpp | 4 +- 3 files changed, 39 insertions(+), 8 deletions(-) diff --git a/lib/GfxRenderer/GfxRenderer.cpp b/lib/GfxRenderer/GfxRenderer.cpp index fa1c61c6..88e69598 100644 --- a/lib/GfxRenderer/GfxRenderer.cpp +++ b/lib/GfxRenderer/GfxRenderer.cpp @@ -2,6 +2,18 @@ #include +namespace { +size_t utf8RemoveLastChar(std::string& str) { + if (str.empty()) return 0; + size_t pos = str.size() - 1; + while (pos > 0 && (static_cast(str[pos]) & 0xC0) == 0x80) { + --pos; + } + str.resize(pos); + return pos; +} +} // namespace + void GfxRenderer::insertFont(const int fontId, EpdFontFamily font) { fontMap.insert({fontId, font}); } void GfxRenderer::rotateCoordinates(const int x, const int y, int* rotatedX, int* rotatedY) const { @@ -415,13 +427,32 @@ void GfxRenderer::displayBuffer(const HalDisplay::RefreshMode refreshMode) const std::string GfxRenderer::truncatedText(const int fontId, const char* text, const int maxWidth, const EpdFontFamily::Style style) const { - std::string item = text; + std::string item = text ? text : ""; + if (maxWidth <= 0) { + return ""; + } + int itemWidth = getTextWidth(fontId, item.c_str(), style); - while (itemWidth > maxWidth && item.length() > 8) { - item.replace(item.length() - 5, 5, "..."); + if (itemWidth <= maxWidth) { + return item; + } + + const char* ellipsis = "..."; + const int ellipsisWidth = getTextWidth(fontId, ellipsis, style); + if (ellipsisWidth > maxWidth) { + return ""; + } + + while (!item.empty() && itemWidth + ellipsisWidth > maxWidth) { + utf8RemoveLastChar(item); itemWidth = getTextWidth(fontId, item.c_str(), style); } - return item; + + if (item.empty()) { + return ellipsis; + } + + return item + ellipsis; } // Note: Internal driver treats screen in command orientation; this library exposes a logical orientation diff --git a/src/activities/reader/EpubReaderActivity.cpp b/src/activities/reader/EpubReaderActivity.cpp index 58668c68..fb30119c 100644 --- a/src/activities/reader/EpubReaderActivity.cpp +++ b/src/activities/reader/EpubReaderActivity.cpp @@ -542,8 +542,8 @@ void EpubReaderActivity::renderStatusBar(const int orientedMarginRight, const in availableTitleSpace = rendererableScreenWidth - titleMarginLeft - titleMarginRight; titleMarginLeftAdjusted = titleMarginLeft; } - while (titleWidth > availableTitleSpace && title.length() > 11) { - title.replace(title.length() - 8, 8, "..."); + if (titleWidth > availableTitleSpace) { + title = renderer.truncatedText(SMALL_FONT_ID, title.c_str(), availableTitleSpace); titleWidth = renderer.getTextWidth(SMALL_FONT_ID, title.c_str()); } } diff --git a/src/activities/reader/TxtReaderActivity.cpp b/src/activities/reader/TxtReaderActivity.cpp index e9303de3..0d55707c 100644 --- a/src/activities/reader/TxtReaderActivity.cpp +++ b/src/activities/reader/TxtReaderActivity.cpp @@ -565,8 +565,8 @@ void TxtReaderActivity::renderStatusBar(const int orientedMarginRight, const int std::string title = txt->getTitle(); int titleWidth = renderer.getTextWidth(SMALL_FONT_ID, title.c_str()); - while (titleWidth > availableTextWidth && title.length() > 11) { - title.replace(title.length() - 8, 8, "..."); + if (titleWidth > availableTextWidth) { + title = renderer.truncatedText(SMALL_FONT_ID, title.c_str(), availableTextWidth); titleWidth = renderer.getTextWidth(SMALL_FONT_ID, title.c_str()); } From 8bb746aed4eff57b5a2bf71a398f035fd4453808 Mon Sep 17 00:00:00 2001 From: Arthur Tazhitdinov Date: Thu, 29 Jan 2026 02:32:50 +0500 Subject: [PATCH 2/4] fix: refactor UTF-8 safe string truncation methods and update usage in HomeActivity --- lib/GfxRenderer/GfxRenderer.cpp | 12 ------------ lib/Utf8/Utf8.cpp | 17 +++++++++++++++++ lib/Utf8/Utf8.h | 6 +++++- src/activities/home/HomeActivity.cpp | 12 +++++++----- src/util/StringUtils.cpp | 19 ------------------- src/util/StringUtils.h | 6 ------ 6 files changed, 29 insertions(+), 43 deletions(-) diff --git a/lib/GfxRenderer/GfxRenderer.cpp b/lib/GfxRenderer/GfxRenderer.cpp index 88e69598..f9f35efc 100644 --- a/lib/GfxRenderer/GfxRenderer.cpp +++ b/lib/GfxRenderer/GfxRenderer.cpp @@ -2,18 +2,6 @@ #include -namespace { -size_t utf8RemoveLastChar(std::string& str) { - if (str.empty()) return 0; - size_t pos = str.size() - 1; - while (pos > 0 && (static_cast(str[pos]) & 0xC0) == 0x80) { - --pos; - } - str.resize(pos); - return pos; -} -} // namespace - void GfxRenderer::insertFont(const int fontId, EpdFontFamily font) { fontMap.insert({fontId, font}); } void GfxRenderer::rotateCoordinates(const int x, const int y, int* rotatedX, int* rotatedY) const { diff --git a/lib/Utf8/Utf8.cpp b/lib/Utf8/Utf8.cpp index d5f7ebce..f77cce55 100644 --- a/lib/Utf8/Utf8.cpp +++ b/lib/Utf8/Utf8.cpp @@ -29,3 +29,20 @@ uint32_t utf8NextCodepoint(const unsigned char** string) { return cp; } + +size_t utf8RemoveLastChar(std::string& str) { + if (str.empty()) return 0; + size_t pos = str.size() - 1; + while (pos > 0 && (static_cast(str[pos]) & 0xC0) == 0x80) { + --pos; + } + str.resize(pos); + return pos; +} + +// Truncate string by removing N UTF-8 characters from the end +void utf8TruncateChars(std::string& str, const size_t numChars) { + for (size_t i = 0; i < numChars && !str.empty(); ++i) { + utf8RemoveLastChar(str); + } +} diff --git a/lib/Utf8/Utf8.h b/lib/Utf8/Utf8.h index 095c1584..23d63a4e 100644 --- a/lib/Utf8/Utf8.h +++ b/lib/Utf8/Utf8.h @@ -1,7 +1,11 @@ #pragma once #include - +#include #define REPLACEMENT_GLYPH 0xFFFD uint32_t utf8NextCodepoint(const unsigned char** string); +// Remove the last UTF-8 codepoint from a std::string and return the new size. +size_t utf8RemoveLastChar(std::string& str); +// Truncate string by removing N UTF-8 codepoints from the end. +void utf8TruncateChars(std::string& str, size_t numChars); diff --git a/src/activities/home/HomeActivity.cpp b/src/activities/home/HomeActivity.cpp index 58b29505..670f7cff 100644 --- a/src/activities/home/HomeActivity.cpp +++ b/src/activities/home/HomeActivity.cpp @@ -17,6 +17,8 @@ #include "fontIds.h" #include "util/StringUtils.h" +#include + void HomeActivity::taskTrampoline(void* param) { auto* self = static_cast(param); self->displayTaskLoop(); @@ -366,7 +368,7 @@ void HomeActivity::render() { while (!lines.back().empty() && renderer.getTextWidth(UI_12_FONT_ID, lines.back().c_str()) > maxLineWidth) { // Remove "..." first, then remove one UTF-8 char, then add "..." back lines.back().resize(lines.back().size() - 3); // Remove "..." - StringUtils::utf8RemoveLastChar(lines.back()); + utf8RemoveLastChar(lines.back()); lines.back().append("..."); } break; @@ -375,7 +377,7 @@ void HomeActivity::render() { int wordWidth = renderer.getTextWidth(UI_12_FONT_ID, i.c_str()); while (wordWidth > maxLineWidth && !i.empty()) { // Word itself is too long, trim it (UTF-8 safe) - StringUtils::utf8RemoveLastChar(i); + utf8RemoveLastChar(i); // Check if we have room for ellipsis std::string withEllipsis = i + "..."; wordWidth = renderer.getTextWidth(UI_12_FONT_ID, withEllipsis.c_str()); @@ -428,7 +430,7 @@ void HomeActivity::render() { if (!lastBookAuthor.empty()) { std::string trimmedAuthor = lastBookAuthor; while (renderer.getTextWidth(UI_10_FONT_ID, trimmedAuthor.c_str()) > maxLineWidth && !trimmedAuthor.empty()) { - StringUtils::utf8RemoveLastChar(trimmedAuthor); + utf8RemoveLastChar(trimmedAuthor); } if (renderer.getTextWidth(UI_10_FONT_ID, trimmedAuthor.c_str()) < renderer.getTextWidth(UI_10_FONT_ID, lastBookAuthor.c_str())) { @@ -462,14 +464,14 @@ void HomeActivity::render() { // Trim author if too long (UTF-8 safe) bool wasTrimmed = false; while (renderer.getTextWidth(UI_10_FONT_ID, trimmedAuthor.c_str()) > maxLineWidth && !trimmedAuthor.empty()) { - StringUtils::utf8RemoveLastChar(trimmedAuthor); + utf8RemoveLastChar(trimmedAuthor); wasTrimmed = true; } if (wasTrimmed && !trimmedAuthor.empty()) { // Make room for ellipsis while (renderer.getTextWidth(UI_10_FONT_ID, (trimmedAuthor + "...").c_str()) > maxLineWidth && !trimmedAuthor.empty()) { - StringUtils::utf8RemoveLastChar(trimmedAuthor); + utf8RemoveLastChar(trimmedAuthor); } trimmedAuthor.append("..."); } diff --git a/src/util/StringUtils.cpp b/src/util/StringUtils.cpp index 2426b687..8e2ce58e 100644 --- a/src/util/StringUtils.cpp +++ b/src/util/StringUtils.cpp @@ -61,23 +61,4 @@ bool checkFileExtension(const String& fileName, const char* extension) { return localFile.endsWith(localExtension); } -size_t utf8RemoveLastChar(std::string& str) { - if (str.empty()) return 0; - size_t pos = str.size() - 1; - // Walk back to find the start of the last UTF-8 character - // UTF-8 continuation bytes start with 10xxxxxx (0x80-0xBF) - while (pos > 0 && (static_cast(str[pos]) & 0xC0) == 0x80) { - --pos; - } - str.resize(pos); - return pos; -} - -// Truncate string by removing N UTF-8 characters from the end -void utf8TruncateChars(std::string& str, const size_t numChars) { - for (size_t i = 0; i < numChars && !str.empty(); ++i) { - utf8RemoveLastChar(str); - } -} - } // namespace StringUtils diff --git a/src/util/StringUtils.h b/src/util/StringUtils.h index 5c8332f0..4b93729b 100644 --- a/src/util/StringUtils.h +++ b/src/util/StringUtils.h @@ -19,10 +19,4 @@ std::string sanitizeFilename(const std::string& name, size_t maxLength = 100); bool checkFileExtension(const std::string& fileName, const char* extension); bool checkFileExtension(const String& fileName, const char* extension); -// UTF-8 safe string truncation - removes one character from the end -// Returns the new size after removing one UTF-8 character -size_t utf8RemoveLastChar(std::string& str); - -// Truncate string by removing N UTF-8 characters from the end -void utf8TruncateChars(std::string& str, size_t numChars); } // namespace StringUtils From 8f8eb869d345da9743a735fa100cab74a4676e8b Mon Sep 17 00:00:00 2001 From: Arthur Tazhitdinov Date: Thu, 29 Jan 2026 02:41:06 +0500 Subject: [PATCH 3/4] fix: optimize UTF-8 safe text truncation logic in GfxRenderer --- lib/GfxRenderer/GfxRenderer.cpp | 25 +++++-------------------- 1 file changed, 5 insertions(+), 20 deletions(-) diff --git a/lib/GfxRenderer/GfxRenderer.cpp b/lib/GfxRenderer/GfxRenderer.cpp index f9f35efc..2eab1a44 100644 --- a/lib/GfxRenderer/GfxRenderer.cpp +++ b/lib/GfxRenderer/GfxRenderer.cpp @@ -415,32 +415,17 @@ void GfxRenderer::displayBuffer(const HalDisplay::RefreshMode refreshMode) const std::string GfxRenderer::truncatedText(const int fontId, const char* text, const int maxWidth, const EpdFontFamily::Style style) const { - std::string item = text ? text : ""; - if (maxWidth <= 0) { - return ""; - } - - int itemWidth = getTextWidth(fontId, item.c_str(), style); - if (itemWidth <= maxWidth) { - return item; - } + if (!text || maxWidth <= 0) return ""; + std::string item = text; const char* ellipsis = "..."; - const int ellipsisWidth = getTextWidth(fontId, ellipsis, style); - if (ellipsisWidth > maxWidth) { - return ""; - } + int ellipsisWidth = getTextWidth(fontId, ellipsis, style); - while (!item.empty() && itemWidth + ellipsisWidth > maxWidth) { + while (!item.empty() && getTextWidth(fontId, (item + ellipsis).c_str(), style) > maxWidth) { utf8RemoveLastChar(item); - itemWidth = getTextWidth(fontId, item.c_str(), style); } - if (item.empty()) { - return ellipsis; - } - - return item + ellipsis; + return item.empty() ? ellipsis : item + ellipsis; } // Note: Internal driver treats screen in command orientation; this library exposes a logical orientation From 68ba5a7f5ea4955266b5a62ce7993fdd7a3e11c1 Mon Sep 17 00:00:00 2001 From: Arthur Tazhitdinov Date: Thu, 29 Jan 2026 02:45:23 +0500 Subject: [PATCH 4/4] format fix --- src/activities/home/HomeActivity.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/activities/home/HomeActivity.cpp b/src/activities/home/HomeActivity.cpp index 670f7cff..678af7cb 100644 --- a/src/activities/home/HomeActivity.cpp +++ b/src/activities/home/HomeActivity.cpp @@ -4,6 +4,7 @@ #include #include #include +#include #include #include @@ -17,8 +18,6 @@ #include "fontIds.h" #include "util/StringUtils.h" -#include - void HomeActivity::taskTrampoline(void* param) { auto* self = static_cast(param); self->displayTaskLoop();