Getting Started
This page walks through the basic workflow: loading a font, creating a buffer, shaping text, and reading the results. For background on what text shaping is and why it matters, see the overview.
Installation
go get github.com/boxesandglue/textshapeAll types live in the ot package:
import "github.com/boxesandglue/textshape/ot"Basic Workflow
Text shaping turns a string of Unicode characters into positioned glyphs. The process has four steps:
- Parse a font — Load OpenType/TrueType font data into a
Font - Create a shaper — Build a
Shaperthat holds the font’s shaping tables - Fill a buffer — Add text and set direction/script/language
- Shape — Run the shaper on the buffer; results appear in
buf.Infoandbuf.Pos
Complete Example
package main
import (
"fmt"
"log"
"os"
"github.com/boxesandglue/textshape/ot"
)
func main() {
// 1. Load font data
fontData, err := os.ReadFile("MyFont.ttf")
if err != nil {
log.Fatal(err)
}
// 2. Parse the font (index 0 for single-font files)
font, err := ot.ParseFont(fontData, 0)
if err != nil {
log.Fatal(err)
}
// 3. Create a shaper
shaper, err := ot.NewShaper(font)
if err != nil {
log.Fatal(err)
}
// 4. Create a buffer and add text
buf := ot.NewBuffer()
buf.AddString("Hello, World!")
buf.Direction = ot.DirectionLTR
buf.Script = ot.MakeTag('L', 'a', 't', 'n')
buf.GuessSegmentProperties()
// 5. Shape
shaper.Shape(buf, nil) // nil = use default features
// 6. Read results
for i := range buf.Info {
info := buf.Info[i]
pos := buf.Pos[i]
fmt.Printf("glyph=%d cluster=%d advance=(%d,%d) offset=(%d,%d)\n",
info.GlyphID, info.Cluster,
pos.XAdvance, pos.YAdvance,
pos.XOffset, pos.YOffset)
}
}Step by Step
Loading a Font
ParseFont reads raw font bytes and returns a Font. The second argument is the font index — use 0 for .ttf and .otf files. For TrueType Collections (.ttc) or DFONTs, use the appropriate index.
data, err := os.ReadFile("NotoSans-Regular.ttf")
if err != nil {
log.Fatal(err)
}
font, err := ot.ParseFont(data, 0)
if err != nil {
log.Fatal(err)
}The Font provides low-level access to font tables:
font.NumGlyphs() // Number of glyphs in the font
font.HasTable(ot.TagGSUB) // Check if a table exists
font.HasGlyph(ot.Codepoint('A')) // Check if a codepoint has a glyph
font.GetGlyphName(42) // Get glyph name by ID
Creating a Face
A Face wraps a Font with parsed metric tables (OS/2, hhea, head, etc.). You need a Face when you want to access font metrics directly.
face, err := ot.NewFace(font)
if err != nil {
log.Fatal(err)
}
fmt.Println("Units per em:", face.Upem())
fmt.Println("Ascender:", face.Ascender())
fmt.Println("Descender:", face.Descender())
fmt.Println("Cap height:", face.CapHeight())
fmt.Println("x-height:", face.XHeight())
fmt.Println("Family:", face.FamilyName())
fmt.Println("PostScript name:", face.PostscriptName())
fmt.Println("Weight class:", face.WeightClass())
fmt.Println("Is italic:", face.IsItalic())
fmt.Println("Is monospace:", face.IsFixedPitch())The bounding box of the entire font:
xMin, yMin, xMax, yMax := face.BBox()Creating a Shaper
The Shaper parses and caches the shaping tables (cmap, GSUB, GPOS, GDEF, kern, etc.) from a Font. You typically create one shaper per font and reuse it.
// From a Font (creates a Face internally)
shaper, err := ot.NewShaper(font)
// Or from an existing Face
shaper, err := ot.NewShaperFromFace(face)You can check which tables are available:
shaper.HasGSUB() // Glyph substitution
shaper.HasGPOS() // Glyph positioning
shaper.HasGDEF() // Glyph definition
shaper.HasHmtx() // Horizontal metrics
Shaping Text
Call Shape with a buffer and optional features. Pass nil for features to use the defaults (kern, liga, calt, mark, mkmk, etc.).
shaper.Shape(buf, nil)With custom features:
features := []ot.Feature{
ot.NewFeatureOn(ot.MakeTag('s', 'm', 'c', 'p')), // Small caps
ot.NewFeatureOff(ot.MakeTag('l', 'i', 'g', 'a')), // Disable ligatures
}
shaper.Shape(buf, features)See Features for details on feature configuration.
Convenience: ShapeString
For quick one-off shaping without manually setting up a buffer:
glyphIDs, positions := shaper.ShapeString("Hello")
for i, gid := range glyphIDs {
fmt.Printf("glyph=%d advance=%d\n", gid, positions[i].XAdvance)
}ShapeString auto-detects the text direction and uses default features. For more control, use the full buffer API.
Reading Results
After shaping, the buffer contains two parallel slices:
buf.Info— oneGlyphInfoper glyph: glyph ID, cluster, classificationbuf.Pos— oneGlyphPosper glyph: advance and offset
for i := 0; i < buf.Len(); i++ {
info := buf.Info[i]
pos := buf.Pos[i]
// GlyphID: the index into the font's glyph table
gid := info.GlyphID
// Cluster: maps back to the original text position
cluster := info.Cluster
// Positioning (in font units)
xAdvance := pos.XAdvance // How far to move the cursor after this glyph
yAdvance := pos.YAdvance // Vertical advance (non-zero for vertical text)
xOffset := pos.XOffset // Horizontal offset from the current position
yOffset := pos.YOffset // Vertical offset from the current position
fmt.Printf("gid=%d cluster=%d adv=(%d,%d) off=(%d,%d)\n",
gid, cluster, xAdvance, yAdvance, xOffset, yOffset)
}To extract just the glyph IDs:
glyphIDs := buf.GlyphIDs() // []GlyphID
Understanding Glyph Positions
Glyph positions are in font units (not points or pixels). To convert to a target size, scale by fontSize / upem:
face, _ := ot.NewFace(font)
upem := float64(face.Upem())
fontSize := 12.0 // points
scale := fontSize / upem
for i := range buf.Pos {
xAdvPt := float64(buf.Pos[i].XAdvance) * scale
yAdvPt := float64(buf.Pos[i].YAdvance) * scale
xOffPt := float64(buf.Pos[i].XOffset) * scale
yOffPt := float64(buf.Pos[i].YOffset) * scale
// Use these scaled values for rendering
_ = xAdvPt
_ = yAdvPt
_ = xOffPt
_ = yOffPt
}Glyph Classification
Each GlyphInfo carries classification data from the GDEF table:
info := buf.Info[i]
info.IsBaseGlyph() // Base glyph (letter, digit, ...)
info.IsMark() // Combining mark (accent, vowel sign, ...)
info.IsLigature() // Result of ligature substitution (e.g., "fi" → fi-ligature)
For ligatures, you can query the components:
if info.IsLigature() {
numComps := info.GetLigNumComps() // How many input glyphs were merged
}Shaping Right-to-Left Text
For Arabic, Hebrew, and other RTL scripts, set the direction to DirectionRTL:
buf := ot.NewBuffer()
buf.AddString("مرحبا بالعالم")
buf.Direction = ot.DirectionRTL
buf.Script = ot.MakeTag('A', 'r', 'a', 'b')
buf.GuessSegmentProperties()
shaper.Shape(buf, nil)Or let GuessSegmentProperties detect it automatically:
buf := ot.NewBuffer()
buf.AddString("מחרוזת")
buf.GuessSegmentProperties() // Detects RTL + Hebrew script
shaper.Shape(buf, nil)Shaping Vertical Text
For CJK vertical text, use DirectionTTB (top-to-bottom):
buf := ot.NewBuffer()
buf.AddString("日本語テキスト")
buf.Direction = ot.DirectionTTB
buf.Script = ot.MakeTag('H', 'a', 'n', 'i')
buf.GuessSegmentProperties()
shaper.Shape(buf, nil)In vertical mode, the shaper computes vertical origins and advances. The YAdvance values are negative (moving downward), and XOffset/YOffset include the shift from the horizontal origin to the vertical origin.
Reusing the Shaper
A Shaper is designed to be reused across multiple shaping calls. Create it once and shape as many buffers as you need:
shaper, _ := ot.NewShaper(font)
// Shape multiple texts
for _, text := range texts {
buf := ot.NewBuffer()
buf.AddString(text)
buf.GuessSegmentProperties()
shaper.Shape(buf, nil)
// process results ...
}Settings like synthetic bold, variable font axes, and default features persist on the shaper between calls.