Proper support of superscript/subscript

This commit is contained in:
jpirnay
2026-05-15 20:47:01 +02:00
parent ae704153d9
commit 95d33a67f0
6 changed files with 132 additions and 7 deletions
+14 -1
View File
@@ -3,7 +3,20 @@
class EpdFontFamily { class EpdFontFamily {
public: public:
enum Style : uint8_t { REGULAR = 0, BOLD = 1, ITALIC = 2, BOLD_ITALIC = 3, UNDERLINE = 4, STRIKETHROUGH = 8 }; // Bitmask of text style flags carried per-word through layout and serialized in page cache.
// Bits 0-1 select the font variant (BOLD/ITALIC); bits 2-5 are decoration/positioning overlays
// applied at render time without changing the underlying font. getFont() ignores all bits
// above bit 1 so decorations compose freely with bold/italic (e.g. BOLD | UNDERLINE | SUP).
enum Style : uint8_t {
REGULAR = 0,
BOLD = 1,
ITALIC = 2,
BOLD_ITALIC = 3,
UNDERLINE = 4, // drawn as a line below baseline by TextBlock::render()
STRIKETHROUGH = 8, // drawn as a line through midline by TextBlock::render()
SUP = 16, // superscript: glyph scaled 50%, raised ~40% of ascender
SUB = 32, // subscript: glyph scaled 50%, lowered ~25% of ascender
};
explicit EpdFontFamily(const EpdFont* regular, const EpdFont* bold = nullptr, const EpdFont* italic = nullptr, explicit EpdFontFamily(const EpdFont* regular, const EpdFont* bold = nullptr, const EpdFont* italic = nullptr,
const EpdFont* boldItalic = nullptr) const EpdFont* boldItalic = nullptr)
+1 -1
View File
@@ -11,7 +11,7 @@
#include "FsHelpers.h" #include "FsHelpers.h"
namespace { namespace {
constexpr uint8_t BOOK_CACHE_VERSION = 8; constexpr uint8_t BOOK_CACHE_VERSION = 9;
constexpr char bookBinFile[] = "/book.bin"; constexpr char bookBinFile[] = "/book.bin";
constexpr char tmpSpineBinFile[] = "/spine.bin.tmp"; constexpr char tmpSpineBinFile[] = "/spine.bin.tmp";
constexpr char tmpTocBinFile[] = "/toc.bin.tmp"; constexpr char tmpTocBinFile[] = "/toc.bin.tmp";
+12 -1
View File
@@ -12,10 +12,21 @@ void TextBlock::render(const GfxRenderer& renderer, const int fontId, const int
return; return;
} }
const int ascender = renderer.getFontAscenderSize(fontId);
for (size_t i = 0; i < words.size(); i++) { for (size_t i = 0; i < words.size(); i++) {
const int wordX = wordXpos[i] + x; const int wordX = wordXpos[i] + x;
const EpdFontFamily::Style currentStyle = wordStyles[i]; const EpdFontFamily::Style currentStyle = wordStyles[i];
renderer.drawText(fontId, wordX, y, words[i].c_str(), true, currentStyle); // SUP/SUB shift the baseline passed to drawText; the glyph is also scaled 50% inside
// drawText, so these offsets are chosen relative to the full-size ascender:
// SUP: raise by 40% of ascender — sits clearly above the cap-height
// SUB: lower by 25% of ascender — descends below baseline without clashing with ascenders below
int wordY = y;
if ((currentStyle & EpdFontFamily::SUP) != 0) {
wordY -= ascender * 2 / 5;
} else if ((currentStyle & EpdFontFamily::SUB) != 0) {
wordY += ascender / 4;
}
renderer.drawText(fontId, wordX, wordY, words[i].c_str(), true, currentStyle);
const std::string& w = words[i]; const std::string& w = words[i];
const bool hasEmSpacePrefix = w.size() >= 3 && static_cast<uint8_t>(w[0]) == 0xE2 && const bool hasEmSpacePrefix = w.size() >= 3 && static_cast<uint8_t>(w[0]) == 0xE2 &&
@@ -205,6 +205,8 @@ void ChapterHtmlSlimParser::updateEffectiveInlineStyle() {
effectiveStrikethrough = effectiveStrikethrough =
currentCssStyle.hasTextDecoration() && (static_cast<uint8_t>(currentCssStyle.textDecoration) & currentCssStyle.hasTextDecoration() && (static_cast<uint8_t>(currentCssStyle.textDecoration) &
static_cast<uint8_t>(CssTextDecoration::LineThrough)) != 0; static_cast<uint8_t>(CssTextDecoration::LineThrough)) != 0;
effectiveSup = false;
effectiveSub = false;
// Apply inline style stack in order // Apply inline style stack in order
for (const auto& entry : inlineStyleStack) { for (const auto& entry : inlineStyleStack) {
@@ -220,6 +222,14 @@ void ChapterHtmlSlimParser::updateEffectiveInlineStyle() {
if (entry.hasStrikethrough) { if (entry.hasStrikethrough) {
effectiveStrikethrough = entry.strikethrough; effectiveStrikethrough = entry.strikethrough;
} }
if (entry.hasSup) {
effectiveSup = entry.sup;
if (entry.sup) effectiveSub = false;
}
if (entry.hasSub) {
effectiveSub = entry.sub;
if (entry.sub) effectiveSup = false;
}
} }
} }
@@ -280,6 +290,11 @@ bool ChapterHtmlSlimParser::flushPartWordBuffer() {
if (isStrikethrough) { if (isStrikethrough) {
fontStyle = static_cast<EpdFontFamily::Style>(fontStyle | EpdFontFamily::STRIKETHROUGH); fontStyle = static_cast<EpdFontFamily::Style>(fontStyle | EpdFontFamily::STRIKETHROUGH);
} }
if (effectiveSup) {
fontStyle = static_cast<EpdFontFamily::Style>(fontStyle | EpdFontFamily::SUP);
} else if (effectiveSub) {
fontStyle = static_cast<EpdFontFamily::Style>(fontStyle | EpdFontFamily::SUB);
}
// flush the buffer — route to table cell text when inside a <td>/<th> // flush the buffer — route to table cell text when inside a <td>/<th>
partWordBuffer[partWordBufferIndex] = '\0'; partWordBuffer[partWordBufferIndex] = '\0';
@@ -1168,12 +1183,17 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char*
if (!self->flushPartWordBuffer()) return; if (!self->flushPartWordBuffer()) return;
self->nextWordContinues = true; self->nextWordContinues = true;
} }
const char* prefix = (strcmp(name, "sup") == 0) ? "^" : "_";
self->currentTextBlock->addWord(prefix, EpdFontFamily::REGULAR, false, true);
self->nextWordContinues = true;
StyleStackEntry entry; StyleStackEntry entry;
entry.depth = self->depth; entry.depth = self->depth;
if (strcmp(name, "sup") == 0) {
entry.hasSup = true;
entry.sup = true;
} else {
entry.hasSub = true;
entry.sub = true;
}
self->inlineStyleStack.push_back(entry); self->inlineStyleStack.push_back(entry);
self->updateEffectiveInlineStyle();
} else if (strcmp(name, "span") == 0 || !isHeaderOrBlock(name)) { } else if (strcmp(name, "span") == 0 || !isHeaderOrBlock(name)) {
// Handle span and other inline elements for CSS styling // Handle span and other inline elements for CSS styling
if (cssStyle.hasFontWeight() || cssStyle.hasFontStyle() || cssStyle.hasTextDecoration()) { if (cssStyle.hasFontWeight() || cssStyle.hasFontStyle() || cssStyle.hasTextDecoration()) {
@@ -66,6 +66,8 @@ class ChapterHtmlSlimParser final : public Print {
bool hasItalic = false, italic = false; bool hasItalic = false, italic = false;
bool hasUnderline = false, underline = false; bool hasUnderline = false, underline = false;
bool hasStrikethrough = false, strikethrough = false; bool hasStrikethrough = false, strikethrough = false;
bool hasSup = false, sup = false;
bool hasSub = false, sub = false;
}; };
std::vector<StyleStackEntry> inlineStyleStack; std::vector<StyleStackEntry> inlineStyleStack;
CssStyle currentCssStyle; CssStyle currentCssStyle;
@@ -73,6 +75,8 @@ class ChapterHtmlSlimParser final : public Print {
bool effectiveItalic = false; bool effectiveItalic = false;
bool effectiveUnderline = false; bool effectiveUnderline = false;
bool effectiveStrikethrough = false; bool effectiveStrikethrough = false;
bool effectiveSup = false;
bool effectiveSub = false;
// Buffered table model — populated while inside <table>, emitted on </table> // Buffered table model — populated while inside <table>, emitted on </table>
struct BufferedTableCell { struct BufferedTableCell {
std::unique_ptr<ParsedText> text; std::unique_ptr<ParsedText> text;
+78 -1
View File
@@ -842,6 +842,68 @@ static void renderCharImpl(const GfxRenderer& renderer, GfxRenderer::RenderMode
} }
} }
// Render a glyph at 50% scale via nearest-neighbor sampling. Used for SUP/SUB style bits.
//
// Nearest-neighbor is chosen deliberately: at 50% every source pixel maps cleanly to one
// destination pixel (srcX = dstX*2, srcY = dstY*2), so there is no blending and no new
// gray levels are introduced — important for 1-bit BW rendering.
//
// For 2-bit (anti-aliased) fonts only raw values >= 2 (dark-gray and black) are drawn.
// Dropping the light-gray level keeps small glyphs crisp rather than muddy.
//
// 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;
const int pos = srcY * srcW + srcX;
const uint8_t byte = bitmap[pos >> 2];
const uint8_t raw = (byte >> ((3 - (pos & 3)) * 2)) & 0x3;
if (raw >= 2) { // threshold: skip light-gray, draw dark-gray and black
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;
const int pos = srcY * srcW + srcX;
const uint8_t byte = bitmap[pos >> 3];
const uint8_t bit = 7 - (pos & 7);
if ((byte >> bit) & 1) {
renderer.drawPixel(baseX + dstX, baseY + dstY, pixelState);
}
}
}
}
}
// IMPORTANT: This function is in critical rendering path and is called for every pixel. Please keep it as simple and // IMPORTANT: This function is in critical rendering path and is called for every pixel. Please keep it as simple and
// efficient as possible. // efficient as possible.
void GfxRenderer::drawPixel(const int x, const int y, const bool state) const { void GfxRenderer::drawPixel(const int x, const int y, const bool state) const {
@@ -960,9 +1022,21 @@ void GfxRenderer::drawText(const int fontId, const int x, const int y, const cha
lastBaseWidth = glyph->width; lastBaseWidth = glyph->width;
lastBaseTop = glyph->top; lastBaseTop = glyph->top;
lastBaseAdvanceFP = glyph->advanceX; lastBaseAdvanceFP = glyph->advanceX;
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.
lastBaseAdvanceFP = (lastBaseAdvanceFP + 1) / 2;
}
prevAdvanceFP = lastBaseAdvanceFP; prevAdvanceFP = lastBaseAdvanceFP;
renderCharImpl<TextRotation::None>(*this, renderModeSnapshot, font, cp, lastBaseX, yPos, black, style); if (isSupSub) {
// yPos already carries the vertical offset applied by TextBlock::render().
renderCharScaled(*this, renderModeSnapshot, font, cp, lastBaseX, yPos, black, style);
} else {
renderCharImpl<TextRotation::None>(*this, renderModeSnapshot, font, cp, lastBaseX, yPos, black, style);
}
prevCp = cp; prevCp = cp;
} }
} }
@@ -2017,6 +2091,9 @@ int GfxRenderer::getTextAdvanceX(const int fontId, const char* text, EpdFontFami
continue; continue;
} }
prevAdvanceFP = glyph->advanceX; prevAdvanceFP = glyph->advanceX;
if ((style & (EpdFontFamily::SUP | EpdFontFamily::SUB)) != 0) {
prevAdvanceFP = (prevAdvanceFP + 1) / 2;
}
prevCp = cp; prevCp = cp;
} }
widthPx += fp4::toPixel(prevAdvanceFP); // final glyph's advance widthPx += fp4::toPixel(prevAdvanceFP); // final glyph's advance