Drawing low resolution monochrome text with freetype-wasm
I recently got myself a label printer, which can only print pixels in monochrome (black or white, no grayscale). Separately, earlier this year I bought this gorgeous monospace font Berkeley Mono. It looks great on my terminal… Now I want to print labels using this same font… but the result looks like the first image below:
To get a better printout (second image), I need to find an alternative way to render text — one that can render small text as monochrome pixels well. That’s when I found The FreeType Auto-Hinter.
Now I need a way to use it to generate a monochrome bitmap image. As I’m a web developer, I want something that I can use in a web browser. That’s when I found freetype-wasm.
Lastly, the Local Font Access API lets web applications directly access locally-installed fonts like a desktop font rather than a web font.
Try it out!
First, we need access to your fonts:
Pick a font and size below:
Enter some text to render it:
The FreeType renderer
This function initializes the FreeType library and provides a way to load fonts and render text.
import FreeTypeInit from 'https://cdn.jsdelivr.net/npm/freetype-wasm@0/dist/freetype.js'
// Based on https://github.com/Ciantic/freetype-wasm/blob/master/example/example.js
async function createFreeTypeRenderer() {
const FreeType = await FreeTypeInit()
/**
* Loads a font from a URL or ArrayBuffer.
*/
const loadFont = async (url, { forceAutoHint = true } = {}) => {
let font
if (typeof url === 'string') {
const response = await fetch(url)
if (!response.ok) {
throw new Error('Failed to load font')
}
font = await response.arrayBuffer()
} else if (url instanceof ArrayBuffer) {
font = url
} else {
throw new Error('Invalid font source')
}
const [face] = FreeType.LoadFontFromBytes(new Uint8Array(font))
/**
* Creates a renderer for a specific font size.
*/
const withSize = (size, handleNewGlyph = (code, char, glyph) => {}) => {
const cache = new Map()
async function updateCache(str) {
FreeType.SetFont(face.family_name, face.style_name)
FreeType.SetCharmap(FreeType.FT_ENCODING_UNICODE)
FreeType.SetPixelSize(0, size)
// Get char codes without bitmaps
const codes = []
for (const char of new Set(str)) {
const point = char.codePointAt(0)
if (!cache.has(char) && point !== undefined) {
codes.push(point)
}
}
// Populate missing bitmaps
const newGlyphs = FreeType.LoadGlyphs(
codes,
FreeType.FT_LOAD_RENDER |
FreeType.FT_LOAD_MONOCHROME |
FreeType.FT_LOAD_TARGET_MONO |
(forceAutoHint ? FreeType.FT_LOAD_FORCE_AUTOHINT : 0)
)
for (const [code, glyph] of newGlyphs) {
const char = String.fromCodePoint(code)
await handleNewGlyph(code, char, glyph)
cache.set(char, {
glyph,
bitmap: glyph.bitmap.imagedata
? await createImageBitmap(glyph.bitmap.imagedata)
: null,
})
}
}
/**
* @param {CanvasRenderingContext2D} ctx
* @param {string} str
* @param {number} offsetx
* @param {number} offsety
*/
const draw = async (
ctx,
str,
offsetx,
offsety,
{ letterSpacing = 0 } = {}
) => {
await updateCache(str)
let prev = null
for (const char of str) {
const { glyph, bitmap } = cache.get(char) || {}
if (glyph) {
// Kerning
if (prev) {
const kerning = FreeType.GetKerning(
prev.glyph_index,
glyph.glyph_index,
0
)
offsetx += kerning.x >> 6
}
if (bitmap) {
ctx.drawImage(
bitmap,
offsetx + glyph.bitmap_left,
offsety - glyph.bitmap_top
)
}
offsetx += glyph.advance.x >> 6
offsetx += letterSpacing
prev = glyph
}
}
}
const measure = async (str, { letterSpacing = 0 } = {}) => {
await updateCache(str)
let width = 0
let prev = null
for (const char of str) {
const { glyph } = cache.get(char) || {}
if (glyph) {
if (prev) {
const kerning = FreeType.GetKerning(
prev.glyph_index,
glyph.glyph_index,
0
)
width += kerning.x >> 6
width += letterSpacing
}
width += glyph.advance.x >> 6
prev = glyph
}
}
return width
}
const getGlyph = async (char) => {
await updateCache(char)
return cache.get(char)
}
return { draw, measure, getGlyph }
}
return { withSize }
}
return { loadFont }
}
Example usage
const renderer = await createFreeTypeRenderer()
const berkeleyMono = await window
.queryLocalFonts({ postscriptNames: ['BerkeleyMono-Bold'] })
.then((fonts) => fonts[0].blob())
.then((blob) => blob.arrayBuffer())
.then((buffer) => renderer.loadFont(buffer))
await berkeleyMono.withSize(19).draw(ctx, 'Hello, world!', 0, 19)