From 4bb8a869e7a09f64d99f1b91a5fb1989af1aecb0 Mon Sep 17 00:00:00 2001 From: Arthur Tazhitdinov Date: Sun, 1 Feb 2026 16:23:48 +0500 Subject: [PATCH] fix: truncating chapter titles using UTF-8 safe function (#599) ## Summary * Truncating chapter titles using utf8 safe functions (Cyrillic titles were split mid codepoint) * refactoring of lib/Utf8 --- ### 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? _**< PARTIALLY >**_ --- lib/GfxRenderer/GfxRenderer.cpp | 18 +++++++++++++----- lib/Utf8/Utf8.cpp | 17 +++++++++++++++++ lib/Utf8/Utf8.h | 6 +++++- src/activities/home/HomeActivity.cpp | 11 ++++++----- src/activities/reader/EpubReaderActivity.cpp | 4 ++-- src/activities/reader/TxtReaderActivity.cpp | 4 ++-- src/util/StringUtils.cpp | 19 ------------------- src/util/StringUtils.h | 6 ------ 8 files changed, 45 insertions(+), 40 deletions(-) diff --git a/lib/GfxRenderer/GfxRenderer.cpp b/lib/GfxRenderer/GfxRenderer.cpp index fa1c61c6..b5aa7710 100644 --- a/lib/GfxRenderer/GfxRenderer.cpp +++ b/lib/GfxRenderer/GfxRenderer.cpp @@ -415,13 +415,21 @@ 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 { + if (!text || maxWidth <= 0) return ""; + std::string item = text; - int itemWidth = getTextWidth(fontId, item.c_str(), style); - while (itemWidth > maxWidth && item.length() > 8) { - item.replace(item.length() - 5, 5, "..."); - itemWidth = getTextWidth(fontId, item.c_str(), style); + const char* ellipsis = "..."; + int textWidth = getTextWidth(fontId, item.c_str(), style); + if (textWidth <= maxWidth) { + // Text fits, return as is + return item; } - return item; + + while (!item.empty() && getTextWidth(fontId, (item + ellipsis).c_str(), style) >= maxWidth) { + utf8RemoveLastChar(item); + } + + return item.empty() ? ellipsis : item + ellipsis; } // Note: Internal driver treats screen in command orientation; this library exposes a logical orientation 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..678af7cb 100644 --- a/src/activities/home/HomeActivity.cpp +++ b/src/activities/home/HomeActivity.cpp @@ -4,6 +4,7 @@ #include #include #include +#include #include #include @@ -366,7 +367,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 +376,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 +429,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 +463,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/activities/reader/EpubReaderActivity.cpp b/src/activities/reader/EpubReaderActivity.cpp index 71d606cb..fa85212e 100644 --- a/src/activities/reader/EpubReaderActivity.cpp +++ b/src/activities/reader/EpubReaderActivity.cpp @@ -584,8 +584,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 86e38e7c..ef730e1a 100644 --- a/src/activities/reader/TxtReaderActivity.cpp +++ b/src/activities/reader/TxtReaderActivity.cpp @@ -546,8 +546,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()); } 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