Merge pull request #222 from jpirnay/feat-supsub
feat: Proper support for sup / sub
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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<uint8_t>(w[0]) == 0xE2 &&
|
||||
|
||||
@@ -205,6 +205,8 @@ void ChapterHtmlSlimParser::updateEffectiveInlineStyle() {
|
||||
effectiveStrikethrough =
|
||||
currentCssStyle.hasTextDecoration() && (static_cast<uint8_t>(currentCssStyle.textDecoration) &
|
||||
static_cast<uint8_t>(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<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>
|
||||
partWordBuffer[partWordBufferIndex] = '\0';
|
||||
@@ -1185,6 +1200,22 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char*
|
||||
}
|
||||
self->inlineStyleStack.push_back(entry);
|
||||
self->updateEffectiveInlineStyle();
|
||||
} else if (strcmp(name, "sup") == 0 || strcmp(name, "sub") == 0) {
|
||||
if (self->partWordBufferIndex > 0) {
|
||||
if (!self->flushPartWordBuffer()) return;
|
||||
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()) {
|
||||
|
||||
@@ -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<StyleStackEntry> 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 <table>, emitted on </table>
|
||||
struct BufferedTableCell {
|
||||
std::unique_ptr<ParsedText> text;
|
||||
|
||||
@@ -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;
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -1015,6 +1015,7 @@ def main():
|
||||
<li>pre element: leading/trailing blank lines</li>
|
||||
<li>pre with inline code element</li>
|
||||
<li>horizontal rules between paragraphs</li>
|
||||
<li>superscript and subscript rendering</li>
|
||||
</ul>
|
||||
""",
|
||||
),
|
||||
@@ -1085,6 +1086,42 @@ greet("World");</code></pre>
|
||||
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.</p>
|
||||
<hr/>
|
||||
<p>End of horizontal rule tests.</p>
|
||||
""",
|
||||
),
|
||||
[],
|
||||
),
|
||||
(
|
||||
"5. Superscript and Subscript",
|
||||
make_chapter(
|
||||
"Superscript and Subscript",
|
||||
"""
|
||||
<h2>Basic superscript</h2>
|
||||
<p>E = mc<sup>2</sup> is Einstein's mass-energy equivalence.</p>
|
||||
<p>The area of a circle is πr<sup>2</sup>.</p>
|
||||
<p>2<sup>10</sup> = 1024.</p>
|
||||
<p>x<sup>n</sup> + y<sup>n</sup> = z<sup>n</sup></p>
|
||||
|
||||
<h2>Basic subscript</h2>
|
||||
<p>Water is H<sub>2</sub>O.</p>
|
||||
<p>Carbon dioxide is CO<sub>2</sub>.</p>
|
||||
<p>The sequence a<sub>1</sub>, a<sub>2</sub>, a<sub>3</sub>, ..., a<sub>n</sub>.</p>
|
||||
<p>Glucose: C<sub>6</sub>H<sub>12</sub>O<sub>6</sub>.</p>
|
||||
|
||||
<h2>Mixed sup and sub</h2>
|
||||
<p>The pH of water is 7, meaning [H<sub>3</sub>O<sup>+</sup>] = 10<sup>-7</sup> mol/L.</p>
|
||||
<p>Footnote reference<sup>1</sup> and another<sup>2</sup> in the same sentence.</p>
|
||||
|
||||
<h2>Ordinals</h2>
|
||||
<p>On the 1<sup>st</sup> of January, the 2<sup>nd</sup> quarter begins on the 3<sup>rd</sup> month.</p>
|
||||
|
||||
<h2>Nested with bold and italic</h2>
|
||||
<p>Speed of light: c = 2.998 × 10<sup>8</sup> m/s.</p>
|
||||
<p>Avogadro: 6.022 × 10<sup>23</sup> mol<sup>-1</sup>.</p>
|
||||
<p>Bold superscript: x<sup><b>2</b></sup> and italic subscript: H<sub><i>n</i></sub>.</p>
|
||||
|
||||
<h2>Long runs</h2>
|
||||
<p>This word<sup>has a rather long superscript attached to it</sup> continuing normally.</p>
|
||||
<p>This word<sub>has a rather long subscript attached to it</sub> continuing normally.</p>
|
||||
""",
|
||||
),
|
||||
[],
|
||||
|
||||
Reference in New Issue
Block a user