Text in gray
The last chapter turned a chapter of a book into a document: blocks of styled runs, each with a font family, a style and a size. Before those runs can be cut into lines, every piece of text has to be measured: how wide is "Bonjour" in Liberation Serif at 44 pixels? And once the lines are made, every letter has to be drawn, in gray, into the page.
On most platforms, a library does both: FreeType, or libinkview on a PocketBook, which wraps
it. Inkshelf has no C code, so it does this itself, in internal/graphics/fonts, with pure-Go
libraries. It does it often: every line of every page goes through it, on a slow processor. That
is why this package is tuned more than most.
Font files, glyphs and sizes
A TrueType or OpenType file describes each character's GlyphGlyph: The drawn shape of a character in a font. Usually one per character, but a ligature is one glyph for several, and an Arabic letter has one per position in its word. as an outline: curves that can be drawn at any size. With it come numbers: how far the pen moves after each glyph (its advance), and adjustments between pairs of letters, such as "AV", which KerningKerning: A small adjustment of the space between two given letters, such as A and V, stored in the font. tucks together.
Inkshelf keeps two things apart:
- A
Fileis a font file parsed once, byfonts.Parse. A font embedded in a book is aFile, used at several sizes. - A
Fontis aFileat one size, ready to measure and draw, made byFile.Open(size).
Sizes are in pixels. The font is opened at 72 DPI, where a point is a pixel, so a 44 px font
has an emem: A length relative to the font size: 1.5em is one and a half times the text size. Inkshelf stores lengths in hundredths of an em. of 44 pixels: the box its letters are designed in. Book text is 44
px by default (defaultTextSize), 32 to 64 with A− and A+. The interface uses three fonts,
opened once by widgets.OpenFonts: 32, 40 and 44 bold, in InkPad 3 pixels, scaled by
widgets.Px to the screen the app runs on.
Where do the fonts come from? The reader's firmware ships a few in /ebrmain/fonts, which any
app may use. fonts.Open(family, style, size) tries Liberation first (the fonts libinkview
used), then DejaVu, and if none opens (on your computer, say), Go's own fonts, compiled into
the program. Go has no serif font and no Arabic or Hebrew letters, which matters in a moment.
The dot and the baseline
Text is drawn by moving a pen, the dot, from left to right. For each character: add the kerning with the previous one, stamp the glyph at the dot, move the dot by the advance.
Letters sit on the BaselineBaseline: The line letters sit on. Inkshelf places text by the top of its line; the baseline is the font's ascent below it., but Inkshelf places a line of text by its top-left corner, as libinkview did: the baseline is the font's ascent below the top. That way a screen's code never has to know about baselines, only about lines.
dotX, dotY := fixed.I(topLeft.X), topLeft.Y+font.ascent
for _, character := range text {
if previous >= 0 {
dotX += font.kern(previous, character)
}
rasterized := font.glyph(character) // from the cache
if rasterized.mask != nil { // nil for a space
blend(dst, rasterized.mask, image.Pt(dotX.Round(), dotY).Add(rasterized.offset), gray.Y)
}
dotX += rasterized.advance + fixed.I(spacing)
previous = character
}
fixed.I and Round are the arithmetic of fixed.Int26_6, the numbers font libraries count
in: integers in 64ths of a pixel, so that sub-pixel positions need no floating point.
Rasterize once, blend many times
To draw an outline, a rasterizer measures, for each pixel, how much of it the outline covers: 0 for none, 255 for all. The result is an Alpha maskAlpha mask: A small image saying how much of each pixel a glyph covers, 0 to 255. Drawing text blends the ink into the page through it.. Drawing the letter is then blending: each pixel of the page moves from its gray toward the ink's gray, in proportion to the mask.
There is a catch. Where the letter lands matters. An "o" whose left edge falls on a pixel boundary gives one mask; the same "o" a quarter of a pixel to the right gives another, with other pixels half covered. If the dot could land anywhere, every "o" would need its own mask.
So Inkshelf opens every font with full HintingHinting: Snapping a glyph's outline to the pixel grid. With full hinting, advances are whole pixels, so a glyph can be rasterized once and reused.: the font's outlines are snapped to the pixel grid, and its advances and kerning become whole pixels. The dot always lands on a whole pixel, every "o" looks the same, and one mask per character is exact. Hinting also makes small text crisper, which on e-ink is welcome.
The blend is a straight line between the two grays, in integers, rounded:
if alpha != 0 {
pixels[index] = uint8((uint32(pixels[index])*(255-uint32(alpha)) + uint32(gray)*uint32(alpha) + 127) / 255)
}
There is no gamma curve here. The GammaGamma: A curve that brightens or darkens the mid-grays while keeping black and white. Inkshelf uses 1.8 to darken the grays of page images for e-ink; text gets none. of page images is for scanned pictures; antialiased text is blended linearly.
A Font keeps what it has computed, in maps: the rasterized glyph of each character
(glyphs), its advance (advances), the kerning of each pair (kerns), and, for shaped text
below, masks by glyph index (masks) and whole shaped runs (shaped). These caches make a
Font unsafe to share between goroutinesGoroutine: Go’s lightweight thread. Inkshelf starts them only from the main thread, with state.Go.: each one that draws
opens its own.
When the font does not have the letter
No font has every character. A French novel quotes a line of Greek; a title holds an arrow; a book embeds a font with only the Latin letters its publisher needed. A character the font lacks would be drawn as its .notdef.notdef: The glyph a font draws for a character it does not have: usually an empty box, nicknamed "tofu". glyph, an empty box.
So layout uses a Font fallbackFont fallback: Drawing the characters a font lacks with another font that has them: DejaVu Sans, then Go's font.: layout.Faces asks the font first whether it
Covers the whole text, one check that almost always says yes. When it does not,
forEachCoveredPart walks the text and gives each missing character to the first font of a
list that has it (fallbacksOf): the reader's own typeface, then DejaVu Sans (the firmware's
font with the most characters, from fonts.OpenFallbacks), then Go's. Neighbouring characters
with the same font make one part, measured and drawn with that font, lowered so that all the
parts share one baseline. A character no font has stays in the first font, as a box.
4 parts, each drawn with its own font and lowered to share the first font's baseline.
Shaping
One glyph per character is enough for Latin. It is not for every script. Arabic letters take a different form at the start, middle or end of a word and join their neighbours; Hebrew stacks vowel points; the scripts of India reorder their vowels. The font itself holds the rules, in its tables. Applying them is ShapingShaping: Turning a run of characters into the right glyphs at the right places, by the font's own rules: Arabic letters join, Hebrew points stack.: turning a run of characters into the right glyphs at the right places.
Font.Shape does it with go-text's HarfbuzzShaper, a pure-Go port of HarfBuzz, the shaping
engine of Chrome and Firefox. It returns Glyph values: a glyph index in the font (no
longer a character: a ligature is one glyph for two letters), an advance and an offset.
DrawGlyphs then stamps them as DrawSpaced stamps characters, with masks cached by glyph
index.
Shaping costs much more than looking up characters, so layout only shapes what needs it:
document.NeedsShaping lists the scripts, and a fragment is shaped only if it is right to left
or holds one of their characters. Latin text never pays.
Going deeper
- Two parsers for one file.
golang.org/x/imagereads outlines and rasterizes; go-text reads the shaping tables. Each parses the font its own way, so aFilekeeps its bytes and go-text parses them on first use (File.textFont, once, withsync.Once). If go-text cannot read the font,unshapedgives one glyph per character, unjoined. - The shaped cache forgets everything at once. Past
maxShaped(4096) runs,Shapeclears the map: simpler than keeping the most used, and a page is shaped again at most. - Why not
imagefont.Drawer? The standard drawer rasterizes every glyph on every call, andimage/drawhas no fast path for*image.Gray.blendwrites the gray bytes directly. - A Hot pathHot path: Code that runs for every pixel of every page turn. Tuned against the ARM assembly, and changed only with benchmarks..
blendruns for every pixel of every glyph of every page. Changedraw.goonly withBenchmarkDrawStringbefore and after, and the inlining unchanged (Making a change). - Small capitals are faked: lowercase letters are drawn as capitals of a font at 75 %
(
smallCapsScale), lowered to the same baseline. - A book's own fonts. EPUB fonts come as TrueType, WOFF (zlib-compressed tables) or WOFF2
(Brotli). Publishers often obfuscate them: the first 1040 bytes (IDPF) or 1024 bytes (Adobe)
are XORed with a key derived from the book's identifier. This is not DRM, only a deterrent.
container.Book.Fontloads each font once:webfont.Deobfuscatewith each identifier of the book, thenwebfont.RecoverAdobe(which guesses the key from the bytes every font header holds), thenwebfont.ToSFNTandfonts.Parse.@font-facerules map them to families (document.Fonts), andFaces.openpicks them by family and style.
Where it lives in the code
internal/graphics/fonts/file.go · Parse, File.OpenA font file parsed once; a Font at one size, fully hinted, 72 DPI.internal/graphics/fonts/open.go · Open, OpenFallbacksThe reader’s fonts in /ebrmain/fonts, then Go’s; the fallback list.internal/graphics/fonts/draw.go · DrawSpaced, glyph, blendHOT PATH: glyphs rasterized once, blended into an *image.Gray.internal/graphics/fonts/font.go · Font, Measure, CoversMeasuring, and the caches that make a Font single-goroutine.internal/graphics/fonts/shape.go · Shape, DrawGlyphsShaping with go-text’s HarfBuzz port.internal/typesetting/layout/faces.go · Faces, fallbacksOf, forEachCoveredPartWhich font draws which part of a run.internal/web/webfont/webfont.go · ToSFNT, Deobfuscate, RecoverAdobeA book’s WOFF and WOFF2 fonts, and their obfuscation.internal/app/widgets/fonts.go · OpenFontsThe interface’s three fonts.
go doc ./internal/graphics/fonts prints the package's own introduction. The full story:
From EPUB to page: fonts.
Next: with every fragment measurable, a paragraph can be cut into lines: Breaking lines.