textshape module

The glu.textshape module

The glu.textshape module provides low-level access to the text shaping engine (a HarfBuzz port). It exposes the textshape ot package directly, independent of the boxesandglue abstraction.

Use this module when you need direct control over text shaping: glyph IDs, positioning, OpenType features, variable font axes, and font metrics.

local ts = require("glu.textshape")

Module Functions

Name Parameters Returns Description
parse_font() filename, [index] Font Load and parse a font file
new_shaper() font Shaper Create a Shaper from a Font
new_face() font Face Create a Face from a Font (metrics)
new_buffer() - Buffer Create an empty Buffer
feature() string Feature Parse a single feature string
features() string {Feature,…} Parse comma-separated features

Quick Start

local ts = require("glu.textshape")

-- Load font and create shaper
local font = ts.parse_font("fonts/Roboto-Regular.ttf")
local shaper = ts.new_shaper(font)

-- Create buffer and add text
local buf = ts.new_buffer()
buf:add_string("office")
buf:guess_segment_properties()

-- Shape with features
shaper:shape(buf, {"+liga", "+kern"})

-- Read results
for i = 1, #buf do
    local info = buf.info[i]
    local pos = buf.pos[i]
    print(string.format("glyph=%d cluster=%d advance=%d",
        info.glyph_id, info.cluster, pos.x_advance))
end

Font

A Font wraps a parsed OpenType font. Create one with parse_font().

local font = ts.parse_font("fonts/MyFont.ttf")
local font = ts.parse_font("fonts/Collection.ttc", 1)  -- font index

Attributes

Name R Type Description
num_glyphs R number Number of glyphs in font

Face

A Face provides font metrics without the overhead of a full shaper. Create one with new_face().

local face = ts.new_face(font)

Attributes

Name R Type Description
upem R number Units per EM
ascender R number Typographic ascender
descender R number Typographic descender
cap_height R number Cap height
x_height R number x-height
postscript_name R string PostScript name
family_name R string Font family name
weight_class R number Weight class (100-900)
is_italic R boolean Is italic font
is_fixed_pitch R boolean Is monospaced font
is_cff R boolean CFF outlines (cubic) vs TrueType (quadratic)

Methods

Name Returns Description
has_variations() boolean Is this a variable font?
variation_axes() {axis,…} Array of variation axis info tables
glyph_outline() {segment,…} or nil Extract vector path for a glyph ID

Each axis info table contains:

Field Type Description
tag string Axis tag (e.g. “wght”)
min number Minimum value
default number Default value
max number Maximum value

Glyph Outline Segments

glyph_outline(gid) returns nil for empty glyphs (e.g. space), or a table of segment tables. Each segment has an op field and coordinate fields:

Op Fields Description
"M" x, y Move to point (start new contour)
"L" x, y Line to endpoint
"Q" x1, y1, x, y Quadratic Bezier (TrueType)
"C" x1, y1, x2, y2, x, y Cubic Bezier (CFF/OpenType)

All coordinates are in font units. TrueType fonts produce "Q" segments, CFF fonts produce "C" segments.

Examples

local face = ts.new_face(font)
print("Family:", face.family_name)
print("Units per EM:", face.upem)
print("Ascender:", face.ascender)
print("Descender:", face.descender)

if face:has_variations() then
    for _, axis in ipairs(face:variation_axes()) do
        print(string.format("  %s: %.0f - %.0f (default %.0f)",
            axis.tag, axis.min, axis.max, axis.default))
    end
end

Extracting a glyph outline:

-- Get glyph ID from shaping
local buf = ts.new_buffer()
buf:add_codepoints({0x41})  -- 'A'
buf:guess_segment_properties()
shaper:shape(buf)
local gid = buf.info[1].glyph_id

-- Extract outline
local segments = face:glyph_outline(gid)
if segments then
    for _, seg in ipairs(segments) do
        if seg.op == "M" then
            print(string.format("M %.1f,%.1f", seg.x, seg.y))
        elseif seg.op == "L" then
            print(string.format("L %.1f,%.1f", seg.x, seg.y))
        elseif seg.op == "Q" then
            print(string.format("Q %.1f,%.1f %.1f,%.1f",
                seg.x1, seg.y1, seg.x, seg.y))
        elseif seg.op == "C" then
            print(string.format("C %.1f,%.1f %.1f,%.1f %.1f,%.1f",
                seg.x1, seg.y1, seg.x2, seg.y2, seg.x, seg.y))
        end
    end
end

Buffer

A Buffer holds text to be shaped. After shaping, it contains the resulting glyph IDs and positions.

local buf = ts.new_buffer()

Methods

Name Parameters Description
add_string() text Add a string to the buffer
add_codepoints() {int,…} Add Unicode codepoints
guess_segment_properties() - Guess direction, script, and language
set_direction() string Set direction: “ltr”, “rtl”, “ttb”, “btt”
set_script() string Set script tag (4 chars, e.g. “latn”)
set_language() string Set language tag (e.g. “en”, “de”)
clear() - Clear the buffer
reverse() - Reverse glyph order

Attributes

Name R Type Description
direction R string Current direction as string
info R {table,…} Array of glyph info tables
pos R {table,…} Array of glyph position tables

Length Operator

#buf returns the number of glyphs in the buffer (after shaping).

Glyph Info Table

Each entry in buf.info has:

Field Type Description
glyph_id number Glyph index in the font
cluster number Maps back to original text
codepoint number Original Unicode codepoint

Glyph Position Table

Each entry in buf.pos has:

Field Type Description
x_advance number Horizontal advance
y_advance number Vertical advance
x_offset number Horizontal offset
y_offset number Vertical offset

Example:

local buf = ts.new_buffer()
buf:add_string("Hello")
buf:set_direction("ltr")
buf:set_script("latn")
buf:set_language("en")

shaper:shape(buf)

for i = 1, #buf do
    local info = buf.info[i]
    local pos = buf.pos[i]
    print(string.format("glyph %d: advance=%d offset=(%d,%d)",
        info.glyph_id, pos.x_advance, pos.x_offset, pos.y_offset))
end

Shaper

A Shaper performs the actual text shaping using OpenType layout rules. Create one with new_shaper().

local shaper = ts.new_shaper(font)

Methods

Name Parameters Description
shape() buffer, [features] Shape text in the buffer
has_variations() - Is this a variable font?
set_variation() tag, value Set a single variation axis
set_variations() {tag=value,…} Set all variation axes
set_synthetic_bold() x, y, [in_place] Set synthetic bold
set_synthetic_slant() slant Set synthetic slant
has_gsub() - Has GSUB table?
has_gpos() - Has GPOS table?
set_default_features() features Set default features for shaping

Shaping with Features

The shape() method accepts an optional table of features. Features can be strings or Feature objects:

-- Using feature strings
shaper:shape(buf, {"+liga", "+kern", "-calt"})

-- Using Feature objects
local liga = ts.feature("+liga")
local kern = ts.feature("+kern")
shaper:shape(buf, {liga, kern})

-- Using comma-separated parsing
local feats = ts.features("+liga,+kern,-calt")
shaper:shape(buf, feats)

Feature string formats (same as HarfBuzz):

Format Meaning
"+liga" Enable ligatures
"-calt" Disable calt
"kern" Enable kerning
"kern=0" Disable kerning
"aalt=2" Alternate #2

Variable Fonts

local font = ts.parse_font("fonts/RobotoFlex.ttf")
local shaper = ts.new_shaper(font)

if shaper:has_variations() then
    -- Set individual axis
    shaper:set_variation("wght", 700)

    -- Set multiple axes at once
    shaper:set_variations({wght = 700, wdth = 100})
end

local buf = ts.new_buffer()
buf:add_string("Bold text")
buf:guess_segment_properties()
shaper:shape(buf)

Synthetic Bold and Slant

-- Synthetic bold: x-strength, y-strength, [in_place]
shaper:set_synthetic_bold(0.02, 0.02)

-- Synthetic slant (italic angle)
shaper:set_synthetic_slant(0.2)

Feature

A Feature object represents a parsed OpenType feature. Create with feature() or features().

local feat = ts.feature("+liga")
local feats = ts.features("+liga,+kern,-calt")

Attributes

Name R Type Description
tag R string Feature tag (e.g. “liga”, “kern”)
value R number Value (0=off, 1=on, >1 for alts)

Complete Example

local ts = require("glu.textshape")

-- Load font
local font = ts.parse_font("fonts/Roboto-Regular.ttf")

-- Inspect font metrics
local face = ts.new_face(font)
print("Font:", face.family_name)
print("Units per EM:", face.upem)
print("Ascender:", face.ascender)
print("Descender:", face.descender)
print("Cap height:", face.cap_height)
print("Glyphs:", font.num_glyphs)

-- Shape text
local shaper = ts.new_shaper(font)
local buf = ts.new_buffer()
buf:add_string("office ffi")
buf:guess_segment_properties()
shaper:shape(buf, {"+liga", "+kern"})

print(string.format("\nShaped %d glyphs:", #buf))
for i = 1, #buf do
    local info = buf.info[i]
    local pos = buf.pos[i]
    print(string.format("  [%d] glyph=%d cluster=%d advance=%d",
        i, info.glyph_id, info.cluster, pos.x_advance))
end