diff --git a/lib/EpdFont/EpdFontFamily.h b/lib/EpdFont/EpdFontFamily.h index 51b6e68b..b0fc8d55 100644 --- a/lib/EpdFont/EpdFontFamily.h +++ b/lib/EpdFont/EpdFontFamily.h @@ -3,7 +3,20 @@ class EpdFontFamily { 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, const EpdFont* boldItalic = nullptr) diff --git a/lib/Epub/Epub/BookMetadataCache.cpp b/lib/Epub/Epub/BookMetadataCache.cpp index aa708ede..7e597a4b 100644 --- a/lib/Epub/Epub/BookMetadataCache.cpp +++ b/lib/Epub/Epub/BookMetadataCache.cpp @@ -11,7 +11,7 @@ #include "FsHelpers.h" namespace { -constexpr uint8_t BOOK_CACHE_VERSION = 8; +constexpr uint8_t BOOK_CACHE_VERSION = 9; constexpr char bookBinFile[] = "/book.bin"; constexpr char tmpSpineBinFile[] = "/spine.bin.tmp"; constexpr char tmpTocBinFile[] = "/toc.bin.tmp"; diff --git a/lib/Epub/Epub/blocks/TextBlock.cpp b/lib/Epub/Epub/blocks/TextBlock.cpp index c451a5f1..5e6b1a54 100644 --- a/lib/Epub/Epub/blocks/TextBlock.cpp +++ b/lib/Epub/Epub/blocks/TextBlock.cpp @@ -12,10 +12,21 @@ void TextBlock::render(const GfxRenderer& renderer, const int fontId, const int return; } + const int ascender = renderer.getFontAscenderSize(fontId); for (size_t i = 0; i < words.size(); i++) { const int wordX = wordXpos[i] + x; 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 bool hasEmSpacePrefix = w.size() >= 3 && static_cast(w[0]) == 0xE2 && diff --git a/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp b/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp index 97b4bd9a..2918e889 100644 --- a/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp +++ b/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp @@ -205,6 +205,8 @@ void ChapterHtmlSlimParser::updateEffectiveInlineStyle() { effectiveStrikethrough = currentCssStyle.hasTextDecoration() && (static_cast(currentCssStyle.textDecoration) & static_cast(CssTextDecoration::LineThrough)) != 0; + effectiveSup = false; + effectiveSub = false; // Apply inline style stack in order for (const auto& entry : inlineStyleStack) { @@ -220,6 +222,14 @@ void ChapterHtmlSlimParser::updateEffectiveInlineStyle() { if (entry.hasStrikethrough) { 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) { fontStyle = static_cast(fontStyle | EpdFontFamily::STRIKETHROUGH); } + if (effectiveSup) { + fontStyle = static_cast(fontStyle | EpdFontFamily::SUP); + } else if (effectiveSub) { + fontStyle = static_cast(fontStyle | EpdFontFamily::SUB); + } // flush the buffer — route to table cell text when inside a / partWordBuffer[partWordBufferIndex] = '\0'; @@ -1168,12 +1183,17 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char* if (!self->flushPartWordBuffer()) return; self->nextWordContinues = true; } - const char* prefix = (strcmp(name, "sup") == 0) ? "^" : "_"; - self->currentTextBlock->addWord(prefix, EpdFontFamily::REGULAR, false, true); - self->nextWordContinues = true; StyleStackEntry entry; 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->updateEffectiveInlineStyle(); } else if (strcmp(name, "span") == 0 || !isHeaderOrBlock(name)) { // Handle span and other inline elements for CSS styling if (cssStyle.hasFontWeight() || cssStyle.hasFontStyle() || cssStyle.hasTextDecoration()) { diff --git a/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.h b/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.h index 60f41e11..6c1f9eb9 100644 --- a/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.h +++ b/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.h @@ -66,6 +66,8 @@ class ChapterHtmlSlimParser final : public Print { bool hasItalic = false, italic = false; bool hasUnderline = false, underline = false; bool hasStrikethrough = false, strikethrough = false; + bool hasSup = false, sup = false; + bool hasSub = false, sub = false; }; std::vector inlineStyleStack; CssStyle currentCssStyle; @@ -73,6 +75,8 @@ class ChapterHtmlSlimParser final : public Print { bool effectiveItalic = false; bool effectiveUnderline = false; bool effectiveStrikethrough = false; + bool effectiveSup = false; + bool effectiveSub = false; // Buffered table model — populated while inside , emitted on
struct BufferedTableCell { std::unique_ptr text; diff --git a/lib/GfxRenderer/GfxRenderer.cpp b/lib/GfxRenderer/GfxRenderer.cpp index 194ad19c..feb84c80 100644 --- a/lib/GfxRenderer/GfxRenderer.cpp +++ b/lib/GfxRenderer/GfxRenderer.cpp @@ -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 // efficient as possible. 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; lastBaseTop = glyph->top; 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; - renderCharImpl(*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(*this, renderModeSnapshot, font, cp, lastBaseX, yPos, black, style); + } prevCp = cp; } } @@ -2017,6 +2091,9 @@ int GfxRenderer::getTextAdvanceX(const int fontId, const char* text, EpdFontFami continue; } prevAdvanceFP = glyph->advanceX; + if ((style & (EpdFontFamily::SUP | EpdFontFamily::SUB)) != 0) { + prevAdvanceFP = (prevAdvanceFP + 1) / 2; + } prevCp = cp; } widthPx += fp4::toPixel(prevAdvanceFP); // final glyph's advance