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))
endFont
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 indexAttributes
| 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
endExtracting 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
endBuffer
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))
endShaper
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