glu
backend modules

Backend Modules

The backend modules provide low-level access to the boxes and glue typesetting engine. They expose the fundamental building blocks: scaled points, nodes, and font shaping.

For most use cases, the glu.frontend module is recommended. The backend modules are useful when you need fine-grained control over typesetting.

Modules Overview

Module Description
glu Scaled points, unit conversion, logging
glu.node Node types and list operations
glu.font Font instances and text shaping
local glu = require("glu")
local node = require("glu.node")
local font = require("glu.font")

glu Module

The glu module provides scaled point operations and logging functions.

Scaled Points

A scaled point (sp) is the internal unit used by boxes and glue. There are 65535 scaled points per DTP point.

The glu.sp() function returns a ScaledPoint userdata object, the same type used by the glu.frontend module. This provides type safety and arithmetic operations.

local glu = require("glu")

-- Convert string with unit to ScaledPoint
local sp = glu.sp("12pt")      -- ScaledPoint: "12pt"
local sp2 = glu.sp("1cm")      -- ScaledPoint: "28.35pt"
local sp3 = glu.sp("1in")      -- ScaledPoint: "72pt"
local sp4 = glu.sp("10mm")     -- ScaledPoint: "28.35pt"

-- Create from points
local sp = glu.sp_from_pt(12)  -- ScaledPoint from 12 points

-- Convert to points/units (returns number)
local pt = glu.sp_to_pt(sp)    -- 12.0
local cm = glu.sp_to_unit(sp, "cm")
local mm = glu.sp_to_unit(sp, "mm")

-- Min/Max (returns ScaledPoint)
local max = glu.max(sp1, sp2)
local min = glu.min(sp1, sp2)

-- Arithmetic operations
local sum = sp + sp2           -- ScaledPoint + ScaledPoint
local diff = sp - "5mm"        -- ScaledPoint - string
local scaled = sp * 2          -- ScaledPoint * number
local half = sp / 2            -- ScaledPoint / number
local ratio = sp / sp2         -- ScaledPoint / ScaledPoint → number

-- Comparisons
if sp > sp2 then ... end
if sp == glu.sp("12pt") then ... end

-- Properties
print(sp.pt)                   -- value in points (number)
print(sp.sp)                   -- raw scaled point value (number)
print(sp:to_mm())              -- value in mm (number)

See ScaledPoint in the frontend documentation for the full API.

Logging

Logging follows Go’s slog pattern with structured key-value pairs:

local glu = require("glu")

glu.debug("Processing file", "filename", "test.txt", "size", 1024)
glu.info("Document created", "pages", 10)
glu.warn("Font not found", "name", "Arial")
glu.error("Failed to load", "path", "/missing.ttf")

Constants

Name Value Description
glu.factor 65535 Scaled points per DTP point

glu.node Module

The glu.node module provides access to the fundamental typesetting nodes. Nodes are linked together to form horizontal and vertical lists.

Creating Nodes

local node = require("glu.node")

local glyph = node.new("glyph")
local glue = node.new("glue")
local kern = node.new("kern")
local disc = node.new("disc")
local penalty = node.new("penalty")
local rule = node.new("rule")
local hlist = node.new("hlist")
local vlist = node.new("vlist")
local image = node.new("image")
local lang = node.new("lang")
local startstop = node.new("startstop")

Common Node Attributes

All nodes have these attributes:

Name R/W Type Description
next R/W Node Next node in list
prev R/W Node Previous node in list
type R string Node type name
id R number Unique node ID

Glyph Node

A glyph represents a single character or ligature.

Name R/W Type Description
codepoint R/W number Font-specific glyph ID
components R/W string Character composition
width R/W sp Glyph width
height R/W sp Glyph height
depth R/W sp Glyph depth
yoffset R/W sp Vertical offset
hyphenate R/W boolean Part of hyphenatable word

Glue Node

Glue represents elastic space that can stretch or shrink.

Name R/W Type Description
width R/W sp Natural width
stretch R/W sp Maximum stretch
shrink R/W sp Maximum shrink
stretch_order R/W number Stretch infinity level
shrink_order R/W number Shrink infinity level
leader R/W HList Leader pattern (repeated to fill space). nil = normal glue.
leader_type R/W number Leader alignment mode (see constants below)
local glu = require("glu")
local node = require("glu.node")

local glue = node.new("glue")
glue.width = glu.sp("12pt")
glue.stretch = glu.sp("6pt")
glue.shrink = glu.sp("4pt")
glue.stretch_order = node.fil  -- infinite stretch

Leaders

A glue node can carry a leader pattern — an HList that is repeated to fill the available space. This is used for table-of-contents dot fills.

Constant Value Description
node.leader_aligned 0 Global grid alignment (TeX \leaders)
node.leader_centered 1 Centered copies (TeX \cleaders)
node.leader_expanded 2 Evenly distributed copies (TeX \xleaders)
local glu = require("glu")
local node = require("glu.node")
local font = require("glu.font")

-- Create dot pattern
local dot_atoms = fnt:shape(" . ")
local head, tail = nil, nil
for _, atom in ipairs(dot_atoms) do
    local n
    if atom.is_space then
        n = node.new("glue")
        n.width = atom.advance
    else
        n = node.new("glyph")
        n.codepoint = atom.codepoint
        n.width = atom.advance
        n.height = atom.height
        n.depth = atom.depth
        n.components = atom.components
    end
    if head == nil then head = n; tail = n
    else tail.next = n; tail = n end
end
local pattern = node.hpack(head)

-- Create leader glue
local leader = node.new("glue")
leader.stretch = glu.factor      -- 1fil
leader.stretch_order = node.filll -- order 3: dominates over line-end glue
leader.leader = pattern
leader.leader_type = node.leader_aligned

Kern Node

A kern is a small fixed space, typically for letter spacing.

Name R/W Type Description
kern R/W sp Kern width

Penalty Node

A penalty indicates a potential break point with a cost.

Name R/W Type Description
penalty R/W number Break cost (-10000 to 10000)
width R/W sp Width if broken here

Rule Node

A rule draws a filled rectangle.

Name R/W Type Description
width R/W sp Rule width
height R/W sp Rule height
depth R/W sp Rule depth
pre R/W string PDF code before rule
post R/W string PDF code after rule
hide R/W boolean Don’t draw the rectangle
paint_last R/W boolean Paint after everything else on the page, as a border over the fills next to it

HList Node

A horizontal list contains nodes arranged horizontally.

Name R/W Type Description
width R/W sp Total width
height R/W sp Maximum height
depth R/W sp Maximum depth
list R/W Node First node in list
glue_set R/W number Glue ratio (stretch/shrink)
glue_sign R/W number 0=normal, 1=stretch, 2=shrink
glue_order R/W number Order of the glue that was set: 0=normal, 1=fil, 2=fill, 3=filll
shift R/W sp Vertical shift
badness R/W number Line badness

VList Node

A vertical list contains nodes arranged vertically.

Name R/W Type Description
width R/W sp Total width
height R/W sp Total height
depth R/W sp Depth
list R/W Node First node in list
glue_set R/W number Glue ratio
glue_sign R/W number 0=normal, 1=stretch, 2=shrink
shift_x R/W sp Horizontal shift
paint_last R/W boolean Paint after everything else on the page, keeping the list’s place
paint_last_scope R/W boolean Paint the paint_last nodes inside right after this list

Disc Node

A discretionary break point for hyphenation.

Name R/W Type Description
pre R/W Node Nodes that end the line when it breaks here
post R/W Node Nodes that start the next line when it breaks here
replace R/W Node Not used by the line breaker
replace_count R/W number Nodes after the Disc that a break here drops, TeX’s replace count: a ligature that pre and post set apart when the word breaks inside it
penalty R/W number Added to the hyphen penalty; 10000 or more forbids the break

Image Node

Name R/W Type Description
width R/W sp Image width
height R/W sp Image height
page R/W number Page number (PDF)
used R/W boolean Already output
crop R/W table Region of the image to show, { x, y, width, height } in the units of its natural size; nil shows all

List Operations

-- Link nodes
glyph.next = glue
glue.next = kern

-- Insert node after another
local head = node.insert_after(head, current, new_node)

-- Insert node before another
local head = node.insert_before(head, current, new_node)

-- Delete node from list
local head = node.delete(head, node_to_remove)

-- Copy entire list
local copy = node.copy_list(head)

-- Get last node
local last = node.tail(head)

-- Get dimensions
local width, height, depth = node.dimensions(head)

-- Debug string
local str = node.string(head)

Packing

Pack a node list into a box:

local glu = require("glu")
local node = require("glu.node")

-- Pack to natural width
local hlist = node.hpack(head)

-- Pack to specific width (stretches/shrinks glue)
local hlist = node.hpack_to(head, glu.sp("200pt"))

-- Pack vertically
local vlist = node.vpack(head)

Line Breaking

node.linebreak(head, settings) breaks a node list into lines of a given width with the Knuth-Plass algorithm and returns a VList of the lines and a list with one table per line. The list must end with a forced break: node.append_lineend(head, tail) appends a penalty that forbids a break, a glue that fills the last line and the forced break, and returns the head and the new tail.

local head, tail = node.append_lineend(head, tail)
local vlist, lines = node.linebreak(head, {
    hsize = "8cm",
    line_height = "12pt",
    tolerance = 2,
})
for i, line in ipairs(lines) do
    glu.info("line", i, "ratio", line.ratio, "demerits", line.demerits)
end

Each line table has ratio (how far its glue stretches or shrinks as a multiple of its stretchability or shrinkability), demerits, fitness (0 tight, 1 decent, 2 loose, 3 very loose) and emergency (no feasible break could end the line).

Lengths are scaled points, strings with a unit or ScaledPoint values. An unknown key is an error.

Name Type Description
hsize sp Width of every line, required
line_height sp Room a line takes up, its box and the glue below it
tolerance number Largest adjustment ratio a line may have, default 4; 0 takes only lines that fit exactly
emergency_stretch sp Stretch added to every line in a last pass, which runs only when the paragraph does not break within the tolerance (TeX’s \emergencystretch)
hyphen_penalty number Added when a line ends at a Disc, default 50 (\hyphenpenalty)
line_penalty number Added to the badness of every line, default 10 (\linepenalty)
double_hyphen_demerits number Added when two lines in a row end at a Disc, default 10000 (\doublehyphendemerits)
final_hyphen_demerits number Added when the second to last line ends at a Disc, default 5000 (\finalhyphendemerits)
demerits_fitness number Added when a line’s fitness class is more than one away from the line before, default 10000 (\adjdemerits)
font_expansion number Share of its width by which a glyph may be narrowed or widened, 0.02 is 2%
indent, indent_rows sp, number Left inset and the rows it applies to: n > 0 the first n, n < 0 all but the first -n, 0 all
indent_right, indent_right_rows sp, number Right inset, narrows the lines without moving their start
indent_steps, indent_right_steps table A staircase of insets, { { inset, rows }, ... }, replaces the plain inset of its side
line_start_glue, line_end_glue Glue node Glue at the start and end of every line (\leftskip, \rightskip); fil stretch sets the lines ragged
squeeze_overfull_boxes boolean Let a line that is still too wide shrink its glue past its shrinkability
hanging_punctuation_end boolean Let punctuation and the hyphen at the line end hang into the margin
omit_last_leading boolean Leave out the glue below the last line
half_leading boolean Put the leading into the line boxes, half above and half below (CSS)
font Font The paragraph’s font, to record each line’s trims for text-box-trim
direction string "ltr" (default) or "rtl", the side a forced break leaves its slack on

Glue Order Constants

Name Value Description
node.normal 0 Finite glue
node.fil 1 First order infinity
node.fill 2 Second order infinity
node.filll 3 Third order infinity

glu.font Module

The glu.font module provides font instances for text shaping.

Creating a Font

local glu = require("glu")
local pdf = require("glu.pdf")
local font = require("glu.font")

-- Load a face first (via glu.pdf module)
local pw = pdf.new("out.pdf")
local face = pw:load_face("fonts/CrimsonPro-Regular.ttf")

-- Create font instance with size
local fnt = font.new(face, glu.sp("12pt"))

Font Attributes

Name R Type Description
size R sp Font size
space R sp Space width
space_stretch R sp Space stretchability
space_shrink R sp Space shrinkability

Shaping Text

The shape method converts text into atoms (glyph metrics):

local atoms = fnt:shape("Hello World")

-- With OpenType features
local atoms = fnt:shape("fficacy", "+liga", "+kern")

for i, atom in ipairs(atoms) do
    print(atom.components, atom.advance)
end

Atom Attributes

Name R Type Description
advance R sp Advance width
height R sp Glyph height
depth R sp Glyph depth
codepoint R number Font-specific glyph ID
components R string Character(s) represented
is_space R boolean Is a space character
hyphenate R boolean Part of word
kern_after R sp Kerning after this glyph

Example: Manual Typesetting

local glu = require("glu")
local pdf = require("glu.pdf")
local font = require("glu.font")
local node = require("glu.node")

-- Low-level typesetting example
local pw = pdf.new("manual.pdf")
local face = pw:load_face("fonts/CrimsonPro-Regular.ttf")
local fnt = font.new(face, glu.sp("12pt"))

-- Shape text into atoms
local atoms = fnt:shape("Hello World", "+kern", "+liga")

-- Convert atoms to glyph nodes
local head = nil
local tail = nil

for _, atom in ipairs(atoms) do
    local g
    if atom.is_space then
        g = node.new("glue")
        g.width = atom.advance
        g.stretch = glu.sp("3pt")
        g.shrink = glu.sp("2pt")
    else
        g = node.new("glyph")
        g.codepoint = atom.codepoint
        g.width = atom.advance
        g.height = atom.height
        g.depth = atom.depth
        g.components = atom.components
    end

    if head == nil then
        head = g
        tail = g
    else
        tail.next = g
        tail = g
    end
end

-- Pack into hlist
local hlist = node.hpack(head)

-- Pack into vlist for output
local vlist = node.vpack(hlist)

-- Output to page
-- (use glu.frontend for actual page output)