feat: <sup> and <sub> support (#2131)

## Summary

* **What is the goal of this PR?** Support for `<sup>` and `<sub>` tags.

## Additional Context

This isn't my work, but @jpirnay 's (missing you here, man!). I migrated
his work from
https://github.com/jpirnay/crosspoint-reader/commit/bcd8c32cf26447ccc792cfedd2fdbfce4fee5210
with some micro-optimizations.

Screenshots: 

[Subscript-and-Superscript-Tests_ch2_p1_10pct_55632.bmp](https://github.com/user-attachments/files/28196936/Subscript-and-Superscript-Tests_ch2_p1_10pct_55632.bmp)

[Subscript-and-Superscript-Tests_ch3_p1_21pct_77473.bmp](https://github.com/user-attachments/files/28196937/Subscript-and-Superscript-Tests_ch3_p1_21pct_77473.bmp)


---

### 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? _**< YES >**_

---------

Co-authored-by: jpirnay <jens@pirnay.com>
Co-authored-by: Julia <julia@uxj.io>
This commit is contained in:
Uri Tauber
2026-05-26 12:38:40 -04:00
committed by GitHub
co-authored by jpirnay Julia
parent 34e923d722
commit c5e861d71c
11 changed files with 443 additions and 18 deletions
+94 -3
View File
@@ -131,7 +131,81 @@ enum class TextRotation { None, Rotated90CW };
// Shared glyph rendering logic for normal and rotated text.
// Coordinate mapping and cursor advance direction are selected at compile time via the template parameter.
template <TextRotation rotation>
// Render a glyph at 50% scale. Used for SUP/SUB style bits.
//
// Each destination pixel represents a 2x2 source block. Drawing when that block
// contains ink preserves thin strokes that nearest-neighbor sampling can skip.
//
// The advance width is also halved in drawText() so layout reserves exactly the right
// horizontal space for the scaled glyph.
static void renderCharScaled(const GfxRenderer& renderer, GfxRenderer::RenderMode renderMode,
const EpdFontFamily& fontFamily, const uint32_t cp, int cursorX, int cursorY,
const bool pixelState, const EpdFontFamily::Style style) {
const EpdGlyph* glyph = fontFamily.getGlyph(cp, style);
if (!glyph) return;
const EpdFontData* fontData = fontFamily.getData(style);
const uint8_t* bitmap = renderer.getGlyphBitmap(fontData, glyph);
if (!bitmap) return;
const int srcW = glyph->width;
const int srcH = glyph->height;
const int dstW = (srcW + 1) / 2; // ceil so odd-width glyphs aren't clipped
const int dstH = (srcH + 1) / 2;
// Scale the glyph bearing by the same factor so the scaled glyph sits at the correct
// pixel offset from the (already-shifted) cursor position.
const int baseX = cursorX + glyph->left / 2;
const int baseY = cursorY - glyph->top / 2;
if (fontData->is2Bit) {
// 2-bit packed format: 4 pixels per byte, MSB first, 2 bits per pixel.
// raw value: 0=white, 1=light-gray, 2=dark-gray, 3=black.
for (int dstY = 0; dstY < dstH; dstY++) {
const int srcY = dstY * 2;
for (int dstX = 0; dstX < dstW; dstX++) {
const int srcX = dstX * 2;
uint8_t coverage = 0;
uint8_t maxRaw = 0;
for (int sampleY = 0; sampleY < 2 && srcY + sampleY < srcH; sampleY++) {
for (int sampleX = 0; sampleX < 2 && srcX + sampleX < srcW; sampleX++) {
const int pos = (srcY + sampleY) * srcW + srcX + sampleX;
const uint8_t byte = bitmap[pos >> 2];
const uint8_t raw = (byte >> ((3 - (pos & 3)) * 2)) & 0x3;
coverage += raw;
if (raw > maxRaw) maxRaw = raw;
}
}
if (maxRaw >= 2 || coverage >= 2) {
renderer.drawPixel(baseX + dstX, baseY + dstY, pixelState);
}
}
}
} else {
// 1-bit packed format: 8 pixels per byte, MSB first.
for (int dstY = 0; dstY < dstH; dstY++) {
const int srcY = dstY * 2;
for (int dstX = 0; dstX < dstW; dstX++) {
const int srcX = dstX * 2;
bool hasInk = false;
for (int sampleY = 0; sampleY < 2 && srcY + sampleY < srcH; sampleY++) {
for (int sampleX = 0; sampleX < 2 && srcX + sampleX < srcW; sampleX++) {
const int pos = (srcY + sampleY) * srcW + srcX + sampleX;
const uint8_t byte = bitmap[pos >> 3];
const uint8_t bit = 7 - (pos & 7);
if ((byte >> bit) & 1) {
hasInk = true;
}
}
}
if (hasInk) {
renderer.drawPixel(baseX + dstX, baseY + dstY, pixelState);
}
}
}
}
}
template <TextRotation rotation = TextRotation::None>
static void renderCharImpl(const GfxRenderer& renderer, GfxRenderer::RenderMode renderMode,
const EpdFontFamily& fontFamily, const uint32_t cp, int cursorX, int cursorY,
const bool pixelState, const EpdFontFamily::Style style) {
@@ -352,7 +426,19 @@ void GfxRenderer::drawText(const int fontId, const int x, const int y, const cha
lastBaseTop = glyph ? glyph->top : 0;
prevAdvanceFP = glyph ? glyph->advanceX : 0; // 12.4 fixed-point
renderCharImpl<TextRotation::None>(*this, renderMode, font, cp, lastBaseX, yPos, black, style);
const bool isSupSub = (style & (EpdFontFamily::SUP | EpdFontFamily::SUB)) != 0;
if (isSupSub) {
// Halve the advance so the cursor advances by the same amount the scaled glyph
// actually occupies, keeping spacing correct without needing a separate smaller font.
prevAdvanceFP = (prevAdvanceFP + 1) / 2;
}
if (isSupSub) {
// yPos already carries the vertical offset applied by TextBlock::render().
renderCharScaled(*this, renderMode, font, cp, lastBaseX, yPos, black, style);
} else {
renderCharImpl<TextRotation::None>(*this, renderMode, font, cp, lastBaseX, yPos, black, style);
}
prevCp = cp;
}
}
@@ -1298,9 +1384,11 @@ int GfxRenderer::getTextAdvanceX(const int fontId, const char* text, EpdFontFami
auto sdIt = sdCardFonts_.find(fontId);
if (sdIt != sdCardFonts_.end() && sdIt->second->hasAdvanceTable()) {
int32_t widthFP = 0;
const bool isSupSub = (style & (EpdFontFamily::SUP | EpdFontFamily::SUB)) != 0;
const uint8_t styleIdx = resolveSdCardStyle(*sdIt->second, style);
while (uint32_t cp = utf8NextCodepoint(reinterpret_cast<const uint8_t**>(&text))) {
widthFP += sdIt->second->getAdvance(cp, styleIdx);
int32_t advFP = sdIt->second->getAdvance(cp, styleIdx);
widthFP += isSupSub ? (advFP + 1) / 2 : advFP;
}
return fp4::toPixel(widthFP);
}
@@ -1331,6 +1419,9 @@ int GfxRenderer::getTextAdvanceX(const int fontId, const char* text, EpdFontFami
const EpdGlyph* glyph = font.getGlyph(cp, style);
prevAdvanceFP = glyph ? glyph->advanceX : 0;
if ((style & (EpdFontFamily::SUP | EpdFontFamily::SUB)) != 0) {
prevAdvanceFP = (prevAdvanceFP + 1) / 2;
}
prevCp = cp;
}
widthPx += fp4::toPixel(prevAdvanceFP); // final glyph's advance