text-shaper

Pure TypeScript text shaping engine with OpenType layout, TrueType hinting, and FreeType-style rasterization. Works in browsers and Bun/Node.js with zero dependencies.
Performance
text-shaper outperforms harfbuzzjs (WebAssembly) and opentype.js across all benchmarks:
| Path Extraction | 16x faster | 10x faster |
| Text to SVG | 1.2-1.5x faster | 4-6x faster |
| Latin Shaping | 1.5x faster | 22x faster |
| Arabic Shaping | 1.2x faster | 86x faster |
| Hebrew Shaping | 1.6x faster | 33x faster |
| Hindi Shaping | 3.6x faster | 11x faster |
| Myanmar Shaping | 10.5x faster | 17x faster |
| CJK Shaping | 1.3-1.5x faster | 11-13x faster |
Features
- OpenType Layout: Full GSUB (substitution) and GPOS (positioning) support
- Complex Scripts: Arabic, Indic, USE (Universal Shaping Engine) shapers
- Variable Fonts: fvar, gvar, avar, HVAR, VVAR, MVAR tables
- AAT Support: morx, kerx, trak tables for Apple fonts
- Color Fonts: SVG, sbix, CBDT/CBLC, COLR/CPAL tables
- BiDi: UAX #9 bidirectional text algorithm
- Rasterization: FreeType-style grayscale, LCD subpixel, and monochrome rendering
- TrueType Hinting: Full bytecode interpreter (150+ opcodes)
- Texture Atlas: GPU-ready glyph atlas generation with shelf packing
- SDF/MSDF: Signed distance field rendering for scalable text
- Zero Dependencies: Pure TypeScript, works in browser and Node.js
Installation
npm install text-shaper
bun add text-shaper
Usage
Basic Shaping
import { Font, shape, UnicodeBuffer } from "text-shaper";
const fontData = await fetch("path/to/font.ttf").then(r => r.arrayBuffer());
const font = Font.load(fontData);
const buffer = new UnicodeBuffer();
buffer.addStr("Hello, World!");
const glyphBuffer = shape(font, buffer);
for (let i = 0; i < glyphBuffer.length; i++) {
const info = glyphBuffer.info[i];
const pos = glyphBuffer.pos[i];
console.log(`Glyph ${info.glyphId}: advance=${pos.xAdvance}`);
}
High-Performance Shaping
For best performance, reuse buffers with shapeInto:
import { Font, shapeInto, UnicodeBuffer, GlyphBuffer } from "text-shaper";
const font = Font.load(fontData);
const uBuffer = new UnicodeBuffer();
const gBuffer = GlyphBuffer.withCapacity(128);
for (const text of texts) {
uBuffer.clear();
uBuffer.addStr(text);
gBuffer.reset();
shapeInto(font, uBuffer, gBuffer);
}
TTC Collections
import { Font } from "text-shaper";
const buffer = await fetch("fonts.ttc").then(r => r.arrayBuffer());
const collection = Font.collection(buffer);
if (collection) {
console.log(collection.count);
const names = collection.names();
const font = collection.get(0);
}
With Features
import { Font, shape, UnicodeBuffer, feature } from "text-shaper";
const glyphBuffer = shape(font, buffer, {
features: [
feature("smcp"),
feature("liga"),
feature("kern"),
],
});
import { smallCaps, standardLigatures, kerning, combineFeatures } from "text-shaper";
const glyphBuffer = shape(font, buffer, {
features: combineFeatures(smallCaps(), standardLigatures(), kerning()),
});
Variable Fonts
import { Font, shape, UnicodeBuffer, tag } from "text-shaper";
const glyphBuffer = shape(font, buffer, {
variations: [
{ tag: tag("wght"), value: 700 },
{ tag: tag("wdth"), value: 75 },
],
});
Rendering to SVG
import {
Font, shape, UnicodeBuffer,
glyphBufferToShapedGlyphs, shapedTextToSVG
} from "text-shaper";
const buffer = new UnicodeBuffer();
buffer.addStr("Hello");
const glyphBuffer = shape(font, buffer);
const shapedGlyphs = glyphBufferToShapedGlyphs(glyphBuffer);
const svg = shapedTextToSVG(font, shapedGlyphs, { fontSize: 48 });
Rasterization
import { Font, rasterizeGlyph, buildAtlas, PixelMode } from "text-shaper";
const bitmap = rasterizeGlyph(font, glyphId, 48, {
pixelMode: PixelMode.Gray,
hinting: true,
});
const atlas = buildAtlas(font, glyphIds, {
fontSize: 32,
padding: 1,
pixelMode: PixelMode.Gray,
hinting: true,
});
SDF/MSDF Rendering
import {
Font, getGlyphPath, renderSdf, renderMsdf, buildMsdfAtlas
} from "text-shaper";
const path = getGlyphPath(font, glyphId);
if (path) {
const sdf = renderSdf(path, {
width: 64,
height: 64,
scale: 1,
spread: 8,
});
}
const msdfAtlas = buildMsdfAtlas(font, glyphIds, {
fontSize: 32,
spread: 4,
});
Fluent API
Two composition styles for glyph manipulation and rendering:
Builder Pattern (Method Chaining)
import { Font, glyph, char, glyphVar, combine, PixelMode } from "text-shaper";
const rgba = glyph(font, glyphId)
?.scale(2)
.rotateDeg(15)
.rasterizeAuto({ padding: 2 })
.blur(5)
.toRGBA();
const svg = char(font, "A")
?.scale(3)
.italic(12)
.toSVG({ width: 100, height: 100 });
const bitmap = glyphVar(font, glyphId, [700, 100])
?.embolden(50)
.rasterize({ pixelMode: PixelMode.Gray, scale: 2 })
.toBitmap();
const h = glyph(font, hGlyphId)?.translate(0, 0);
const i = glyph(font, iGlyphId)?.translate(100, 0);
if (h && i) {
const combined = combine(h, i).scale(2).rasterizeAuto().toRGBA();
}
PathBuilder Methods
.scale(sx, sy?)
.translate(dx, dy)
.rotate(radians)
.rotateDeg(degrees)
.shear(shearX, shearY)
.italic(degrees)
.matrix(m)
.perspective(m)
.resetTransform()
.apply()
.embolden(strength)
.condense(factor)
.oblique(slant)
.stroke(width, cap?, join?)
.strokeAsymmetric(opts)
.rasterize(options)
.rasterizeAuto(options?)
.toSdf(options)
.toMsdf(options)
.toSVG(options?)
.toSVGElement(options?)
.toCanvas(ctx, options?)
.toPath()
.clone()
BitmapBuilder Methods
.blur(radius)
.boxBlur(radius)
.fastBlur(radius)
.cascadeBlur(rx, ry?)
.adaptiveBlur(rx, ry?)
.embolden(xStrength, yStrength?)
.shift(dx, dy)
.resize(width, height)
.resizeBilinear(w, h)
.pad(left, top, right, bottom)
.convert(pixelMode)
.toRGBA()
.toGray()
.toBitmap()
.toRasterizedGlyph()
.clone()
Pipe Pattern (Functional)
import {
pipe, glyph,
$scale, $rotate, $embolden,
$rasterize, $blur, $toRGBA
} from "text-shaper";
const rgba = pipe(
glyph(font, glyphId),
$scale(2),
$rotate(Math.PI / 12),
$embolden(30),
$rasterize({ pixelMode: PixelMode.Gray }),
$blur(3),
$toRGBA()
);
Line Breaking & Justification
import { breakIntoLines, justify, JustifyMode } from "text-shaper";
const lines = breakIntoLines(glyphBuffer, font, maxWidth);
const justified = justify(line, targetWidth, {
mode: JustifyMode.Distribute,
});
Text Segmentation
import { countGraphemes, splitGraphemes, splitWords } from "text-shaper";
const count = countGraphemes("👨👩👧👦Hello");
const graphemes = splitGraphemes("नमस्ते");
const words = splitWords("Hello World");
Canvas Rendering
import {
Font, shape, UnicodeBuffer,
glyphBufferToShapedGlyphs, renderShapedText
} from "text-shaper";
const canvas = document.getElementById("canvas") as HTMLCanvasElement;
const ctx = canvas.getContext("2d")!;
const buffer = new UnicodeBuffer();
buffer.addStr("Hello Canvas!");
const glyphBuffer = shape(font, buffer);
const shapedGlyphs = glyphBufferToShapedGlyphs(glyphBuffer);
renderShapedText(ctx, font, shapedGlyphs, {
x: 50,
y: 100,
fontSize: 48,
});
BiDi & RTL Text
import {
Font, shape, UnicodeBuffer,
processBidi, reorderGlyphs, detectDirection
} from "text-shaper";
const buffer = new UnicodeBuffer();
buffer.addStr("Hello שלום World");
const glyphBuffer = shape(font, buffer);
const bidiResult = processBidi("مرحبا Hello");
console.log(bidiResult.direction);
console.log(bidiResult.levels);
const dir = detectDirection("שלום");
Color Fonts
import {
Font,
hasColorGlyph, getColorPaint, getColorLayers,
hasSvgGlyph, getSvgDocument,
hasColorBitmap, getBitmapGlyph,
} from "text-shaper";
const glyphId = font.glyphId("😀".codePointAt(0)!);
if (hasColorGlyph(font, glyphId)) {
const paint = getColorPaint(font, glyphId);
}
if (hasSvgGlyph(font, glyphId)) {
const svgDoc = getSvgDocument(font, glyphId);
}
if (hasColorBitmap(font, glyphId)) {
const bitmap = getBitmapGlyph(font, glyphId, 128);
}
Texture Atlas (WebGL/GPU)
import {
Font, buildAtlas, buildStringAtlas, buildMsdfAtlas,
atlasToRGBA, getGlyphUV, PixelMode
} from "text-shaper";
const atlas = buildAtlas(font, glyphIds, {
fontSize: 32,
padding: 2,
pixelMode: PixelMode.Gray,
});
const textAtlas = buildStringAtlas(font, "Hello World!", {
fontSize: 48,
padding: 1,
});
const rgba = atlasToRGBA(atlas);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, atlas.width, atlas.height,
0, gl.RGBA, gl.UNSIGNED_BYTE, rgba);
const uv = getGlyphUV(atlas, glyphId);
const msdfAtlas = buildMsdfAtlas(font, glyphIds, {
fontSize: 32,
spread: 4,
});
Browser Usage
const fontData = await fetch("/fonts/MyFont.ttf").then(r => r.arrayBuffer());
const font = Font.load(fontData);
const file = input.files[0];
const buffer = await file.arrayBuffer();
const font = Font.load(buffer);
API Reference
Core Classes
Font | Load and parse OpenType/TrueType fonts |
Face | Font face with variation coordinates applied |
UnicodeBuffer | Input buffer for text to shape |
GlyphBuffer | Output buffer containing shaped glyphs |
Shaping Functions
shape(font, buffer, options?) | Shape text, returns new GlyphBuffer |
shapeInto(font, buffer, glyphBuffer, options?) | Shape into existing buffer (faster) |
createShapePlan(font, options) | Create reusable shape plan |
getOrCreateShapePlan(font, options) | Get cached or create shape plan |
Rendering Functions
getGlyphPath(font, glyphId) | Get glyph outline as path commands |
shapedTextToSVG(font, shapedGlyphs, options) | Render shaped text to SVG string |
renderShapedText(ctx, font, shapedGlyphs, options) | Render to Canvas 2D context |
glyphBufferToShapedGlyphs(buffer) | Convert GlyphBuffer to ShapedGlyph[] |
rasterizeGlyph(font, glyphId, size, options) | Rasterize glyph to bitmap |
rasterizePath(path, options) | Rasterize path commands to bitmap |
buildAtlas(font, glyphIds, options) | Build texture atlas |
Feature Helpers
standardLigatures()
discretionaryLigatures()
contextualAlternates()
smallCaps()
capsToSmallCaps()
allSmallCaps()
oldstyleFigures()
liningFigures()
tabularFigures()
proportionalFigures()
stylisticSet(n)
characterVariant(n)
swash()
Unicode Utilities
processBidi(text) | Process bidirectional text (UAX #9) |
getScript(codepoint) | Get Unicode script for codepoint |
getScriptRuns(text) | Split text into script runs |
countGraphemes(text) | Count grapheme clusters |
splitGraphemes(text) | Split into grapheme clusters |
analyzeLineBreaks(text) | Find line break opportunities (UAX #14) |
Supported Tables
Required
head, hhea, hmtx, maxp, cmap, loca, glyf, name, OS/2, post
OpenType Layout
GDEF, GSUB, GPOS, BASE
CFF
CFF, CFF2
Variable Fonts
fvar, gvar, avar, HVAR, VVAR, MVAR, STAT
AAT (Apple)
morx, kerx, kern, trak, feat
Color
COLR, CPAL, SVG, sbix, CBDT, CBLC
Vertical
vhea, vmtx, VORG
Hinting
fpgm, prep, cvt, gasp
License
MIT