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:
co-authored by
jpirnay
Julia
parent
34e923d722
commit
c5e861d71c
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user