Getting Started

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/textshape

All 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:

  1. Parse a font — Load OpenType/TrueType font data into a Font
  2. Create a shaper — Build a Shaper that holds the font’s shaping tables
  3. Fill a buffer — Add text and set direction/script/language
  4. Shape — Run the shaper on the buffer; results appear in buf.Info and buf.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 — one GlyphInfo per glyph: glyph ID, cluster, classification
  • buf.Pos — one GlyphPos per 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.