feat: add RTL support in epub and txt readers (#1700)

Co-authored-by: Zach Nelson <zach@zdnelson.com>
This commit is contained in:
Uri Tauber
2026-05-29 03:25:17 -04:00
committed by GitHub
co-authored by Zach Nelson
parent cc2079a578
commit f5bc554ae7
27 changed files with 1621 additions and 119 deletions
+69 -14
View File
@@ -1,5 +1,6 @@
#include "GfxRenderer.h"
#include <BidiUtils.h>
#include <FontDecompressor.h>
#include <HalGPIO.h>
#include <Logging.h>
@@ -21,6 +22,10 @@ uint8_t resolveSdCardStyle(const SdCardFont& font, const EpdFontFamily::Style st
}
} // namespace
namespace {
const char* resolveVisualText(const char* text, std::string& visualBuffer, BidiUtils::BidiBaseDir baseDir);
} // namespace
const uint8_t* GfxRenderer::getGlyphBitmap(const EpdFontData* fontData, const EpdGlyph* glyph) const {
if (fontData->groups != nullptr) {
auto* fd = fontCacheManager_ ? fontCacheManager_->getDecompressor() : nullptr;
@@ -352,26 +357,42 @@ void GfxRenderer::drawPixel(const int x, const int y, const bool state) const {
}
}
int GfxRenderer::getTextWidth(const int fontId, const char* text, const EpdFontFamily::Style style) const {
int GfxRenderer::getTextWidth(const int fontId, const char* text, const EpdFontFamily::Style style,
const BidiUtils::BidiBaseDir baseDir) const {
if (text == nullptr || *text == '\0') {
return 0;
}
const auto fontIt = fontMap.find(fontId);
if (fontIt == fontMap.end()) {
LOG_ERR("GFX", "Font %d not found", fontId);
return 0;
}
std::string visual;
const char* renderedText = resolveVisualText(text, visual, baseDir);
int w = 0, h = 0;
fontIt->second.getTextDimensions(text, &w, &h, style);
fontIt->second.getTextDimensions(renderedText, &w, &h, style);
return w;
}
void GfxRenderer::drawCenteredText(const int fontId, const int y, const char* text, const bool black,
const EpdFontFamily::Style style) const {
const int x = (getScreenWidth() - getTextWidth(fontId, text, style)) / 2;
drawText(fontId, x, y, text, black, style);
const EpdFontFamily::Style style, const BidiUtils::BidiBaseDir baseDir) const {
const int x = (getScreenWidth() - getTextWidth(fontId, text, style, baseDir)) / 2;
drawText(fontId, x, y, text, black, style, baseDir);
}
void GfxRenderer::drawText(const int fontId, const int x, const int y, const char* text, const bool black,
const EpdFontFamily::Style style) const {
const EpdFontFamily::Style style, const BidiUtils::BidiBaseDir baseDir) const {
// cannot draw a NULL / empty string
if (text == nullptr || *text == '\0') {
return;
}
std::string visual;
const char* renderedText = resolveVisualText(text, visual, baseDir);
const int yPos = y + getFontAscenderSize(fontId);
int lastBaseX = x;
int lastBaseLeft = 0;
@@ -379,13 +400,8 @@ void GfxRenderer::drawText(const int fontId, const int x, const int y, const cha
int lastBaseTop = 0;
int32_t prevAdvanceFP = 0; // 12.4 fixed-point: prev glyph's advance + next kern for snap
// cannot draw a NULL / empty string
if (text == nullptr || *text == '\0') {
return;
}
if (fontCacheManager_ && fontCacheManager_->isScanning()) {
fontCacheManager_->recordText(text, fontId, style);
fontCacheManager_->recordText(renderedText, fontId, style);
return;
}
@@ -396,9 +412,16 @@ void GfxRenderer::drawText(const int fontId, const int x, const int y, const cha
}
const auto& font = fontIt->second;
const char* textCursor = renderedText;
uint32_t cp;
uint32_t prevCp = 0;
while ((cp = utf8NextCodepoint(reinterpret_cast<const uint8_t**>(&text)))) {
while ((cp = utf8NextCodepoint(reinterpret_cast<const uint8_t**>(&textCursor)))) {
// Skip Hebrew Niqqud (vowel marks)
// Temporary: avoid adding Niqqud to built-in fonts. Remove when custom fonts are supported.
if (cp >= 0x0591 && cp <= 0x05C7) {
continue;
}
if (utf8IsCombiningMark(cp)) {
const EpdGlyph* combiningGlyph = font.getGlyph(cp, style);
if (!combiningGlyph) continue;
@@ -409,7 +432,7 @@ void GfxRenderer::drawText(const int fontId, const int x, const int y, const cha
continue;
}
cp = font.applyLigatures(cp, text, style);
cp = font.applyLigatures(cp, textCursor, style);
// Differential rounding: snap (previous advance + current kern) as one unit so
// identical character pairs always produce the same pixel step regardless of
@@ -443,6 +466,32 @@ void GfxRenderer::drawText(const int fontId, const int x, const int y, const cha
}
}
namespace {
const char* resolveVisualText(const char* text, std::string& visualBuffer, const BidiUtils::BidiBaseDir baseDir) {
if (!text || *text == '\0') return text;
if (baseDir != BidiUtils::BidiBaseDir::RTL) {
// Byte-level scan: skip BiDi when no RTL script lead bytes are present.
// Hebrew UTF-8 lead bytes: 0xD6-0xD7; Arabic/Syriac: 0xD8-0xDB.
// This covers all RTL content without false negatives and avoids triggering
// the full UAX#9 algorithm for Latin-extended, em-dashes, accented text, etc.
bool hasRtlBytes = false;
for (const unsigned char* q = reinterpret_cast<const unsigned char*>(text); *q; ++q) {
if (*q >= 0xD6 && *q <= 0xDB) {
hasRtlBytes = true;
break;
}
}
if (!hasRtlBytes) return text;
}
if (BidiUtils::applyBidiVisual(text, visualBuffer, static_cast<int>(baseDir)) && !visualBuffer.empty()) {
return visualBuffer.c_str();
}
return text;
}
} // namespace
void GfxRenderer::drawLine(int x1, int y1, int x2, int y2, const bool state) const {
if (fontCacheManager_ && fontCacheManager_->isScanning()) return;
if (x1 == x2) {
@@ -1481,6 +1530,12 @@ void GfxRenderer::drawTextRotated90CW(const int fontId, const int x, const int y
uint32_t cp;
uint32_t prevCp = 0;
while ((cp = utf8NextCodepoint(reinterpret_cast<const uint8_t**>(&text)))) {
// Skip Hebrew Niqqud (vowel marks)
// Temporary: avoid adding Niqqud to built-in fonts. Remove when custom fonts are supported.
if (cp >= 0x0591 && cp <= 0x05C7) {
continue;
}
if (utf8IsCombiningMark(cp)) {
const EpdGlyph* combiningGlyph = font.getGlyph(cp, style);
if (!combiningGlyph) continue;