Callbacks
glu provides a callback system that covers the full document lifecycle — from initialization through page-level and element-level events to final cleanup. Callbacks are Lua functions that run at specific points during PDF generation.
Registering a callback
Use frontend.add_callback() to register a named callback for an event:
local frontend = require("glu.frontend")
-- Lifecycle: runs before the aux file is loaded
frontend.add_callback("document_start", "my_init", function()
print("Starting document processing")
end)
-- Page-level: runs when a new page is created (for backgrounds)
frontend.add_callback("page_init", "page_bg", function(doc, page, pagenum, pageinfo)
-- draw a background, watermark, etc.
end)
-- Page-level: runs before each page is shipped out (for overlays)
frontend.add_callback("pre_shipout", "page_frame", function(doc, page, pagenum, pageinfo)
-- draw a frame, add a page number, etc.
end)
-- Element-level: runs after each block element is built
frontend.add_callback("post_element", "heading_style", function(element, doc)
-- inspect or decorate the element
end)
-- Lifecycle: runs after the PDF has been written
frontend.add_callback("document_end", "my_cleanup", function()
print("PDF finished")
end)Each callback has a name (here "page_frame") that identifies it. Names must be unique per event — registering a callback with the same name replaces the previous one.
Events
Lifecycle events
| Event | When it fires | Arguments |
|---|---|---|
document_start |
After companion Lua, before aux file is loaded | (none) |
content_ready |
After aux file is loaded, before content processing | (none) |
document_end |
After the PDF file has been written | (none) |
Processing events
| Event | When it fires | Arguments |
|---|---|---|
page_init |
After a new page is created | doc, page, pagenum, pageinfo |
pre_shipout |
Before a page is written to the PDF | doc, page, pagenum, pageinfo |
post_element |
After a block element’s VList is built | element, doc |
document_start
Fires once at the very beginning, after the companion Lua file has been loaded but before the auxiliary file is read. This is the earliest point where user code can run. The _aux table is not yet available.
Use this for initialization tasks that must happen before any data from previous runs is loaded.
frontend.add_callback("document_start", "init", function()
print("Starting at " .. os.date())
end)content_ready
Fires once after the auxiliary file has been loaded and the _aux / _toc globals are set. This is the last callback before content processing begins (Lua blocks, Markdown conversion, or HTML parsing).
Use this to inspect or modify _aux data before it influences the document, or to set up state that depends on cross-run data like the page count or table of contents.
frontend.add_callback("content_ready", "setup", function()
if _aux._pages and _aux._pages > 0 then
print("Previous run had " .. _aux._pages .. " pages")
end
end)document_end
Fires once after the PDF file has been written and closed. This is the last callback in the lifecycle.
Use this for cleanup, logging, or writing custom data to _aux that should persist to the next run.
frontend.add_callback("document_end", "done", function()
_aux.last_build = os.date()
print("PDF written successfully")
end)Any modifications to _aux made in this callback are included when the auxiliary file is saved.
page_init
Fires once for every page, immediately after it is created. The page is empty at this point — content will be placed on it afterwards. This is the ideal place for backgrounds, watermarks, or any decoration that should appear behind the page content (PDF rendering order: first drawn = furthest back).
Arguments
The arguments are the same as for pre_shipout:
| Argument | Type | Description |
|---|---|---|
doc |
Document | The current document |
page |
Page | The newly created page |
pagenum |
number | The page number (1-based) |
pageinfo |
PageInfo | Page layout and CSS information (see PageInfo below) |
pre_shipout
Fires once for every page, just before it is written to the PDF file. The page content has already been placed. This is the ideal place for overlays like page numbers, frames, or headers that should appear on top of the content.
Arguments
| Argument | Type | Description |
|---|---|---|
doc |
Document | The current document (same as from frontend.new()) |
page |
Page | The page about to be shipped out |
pagenum |
number | The page number (1-based) |
pageinfo |
PageInfo | Page layout and CSS information (see below) |
The page object provides page.width and page.height as dimensions. Use page.width.pt to get the numeric value in points (for calculations or SVG generation).
Place content on the page with:
page:output_at(x, y, content)PageInfo object
The pageinfo parameter provides access to the current page dimensions and CSS page areas. All dimension values are ScaledPoints (use .pt for the numeric value in points).
| Property | Type | Description |
|---|---|---|
margin_top |
ScaledPoint | Top margin from the CSS @page rule |
margin_bottom |
ScaledPoint | Bottom margin |
margin_left |
ScaledPoint | Left margin |
margin_right |
ScaledPoint | Right margin |
content_width |
ScaledPoint | Available width for content (page width minus margins, borders, padding) |
content_height |
ScaledPoint | Available height for content |
page_area_left |
ScaledPoint | X position where the content area starts |
page_area_top |
ScaledPoint | Y position where the content area starts |
page_areas |
table or nil | CSS page margin boxes (see below) |
The page_areas table contains entries from the CSS @page rule for margin boxes like @top-center or @bottom-right. Each entry is a table of CSS properties:
local areas = pageinfo.page_areas
if areas and areas["@top-center"] then
local content = areas["@top-center"].content
-- use the content value ...
endpost_element
Fires once for every block-level HTML element (<h1>, <p>, <ul>, <table>, etc.) after its vertical list (VList) has been built.
This callback runs during the VList building phase — before any pages are created. It is useful for:
- Collecting data — e.g. headings for a table of contents
- Adding visual decorations — e.g. SVG backgrounds or underlines behind specific elements
- Inspecting elements — e.g. logging element sizes for debugging
Arguments
| Argument | Type | Description |
|---|---|---|
element |
ElementInfo | Information about the processed block element |
doc |
Document | The current document (for creating SVG nodes etc.) |
ElementInfo object
Properties (read-only):
| Property | Type | Description |
|---|---|---|
tag_name |
string | The HTML tag name (e.g. "h1", "p", "ul") |
text_content |
string | The concatenated text content of the element |
width |
ScaledPoint | Width of the element’s VList |
height |
ScaledPoint | Height of the element’s VList |
depth |
ScaledPoint | Depth of the element’s VList |
Methods:
element:set_background(node)
Places an SVGNode or VList behind the element content. The background is drawn first in the PDF content stream, so the element text appears on top.
element:set_background(svgnode) -- SVGNode from doc:create_svg_node()
element:set_background(vlist) -- VList from doc:format_paragraph()Timing
post_element fires during CreateVlist(), which runs before OutputPages(). This means:
- All element callbacks complete before the first page is created. Data collected in
post_element(like a heading list) is fully available whenpre_shipoutfires. - Modifications via
set_background()are included in the page layout. The background nodes become part of the element’s VList before pages are broken.
Lua companion file loaded
↓
document_start ← before aux file is read
↓
Read aux file → _aux/_toc globals set
↓
content_ready ← aux loaded, before processing
↓
CreateVlist() ← post_element fires here for each block element
↓
OutputPages()
├─ new page ← page_init fires here
├─ place content
├─ shipout ← pre_shipout fires here
└─ repeat for each page
↓
PDF written
↓
document_end ← after PDF is closed
↓
Write aux file (includes _aux modifications)Callback ordering
By default, callbacks run in the order they were registered. The optional fourth argument to add_callback controls positioning:
-- Append (default)
frontend.add_callback("pre_shipout", "background", fn)
frontend.add_callback("pre_shipout", "background", fn, "back")
-- Prepend
frontend.add_callback("pre_shipout", "watermark", fn, "front")
-- Relative positioning
frontend.add_callback("pre_shipout", "border", fn, { after = "background" })
frontend.add_callback("pre_shipout", "header", fn, { before = "footer" })| Position | Description |
|---|---|
"back" (default) |
Append to the end of the callback list |
"front" |
Insert at the beginning |
{after = "name"} |
Insert after the named callback |
{before = "name"} |
Insert before the named callback |
If the referenced callback name does not exist, before/after falls back to appending.
Removing and listing callbacks
Remove a callback by name:
frontend.remove_callback("pre_shipout", "page_frame")List all registered callback names for an event (in execution order):
local names = frontend.list_callbacks("pre_shipout")
for _, name in ipairs(names) do
print(name)
endWhere to register callbacks
In Markdown mode, callbacks are typically registered in the companion Lua file. For example, if your document is report.md, put callback registrations in report.lua:
report.md ← your document
report.lua ← callback registrations (loaded automatically)In Lua script mode, register callbacks before creating pages.
Example: page background (page_init)
This example draws a light gray background in the content area using page_init. Because it fires before any content is placed, the background appears behind the text:
local frontend = require("glu.frontend")
frontend.add_callback("page_init", "page_bg", function(doc, page, pagenum, pageinfo)
local w = page.width.pt
local h = page.height.pt
local mt = pageinfo.margin_top.pt
local mb = pageinfo.margin_bottom.pt
local ml = pageinfo.margin_left.pt
local mr = pageinfo.margin_right.pt
local svg = string.format(
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 %g %g">' ..
'<rect x="%g" y="%g" width="%g" height="%g" fill="#f5f5f5"/>' ..
'</svg>',
w, h, ml, mt, w - ml - mr, h - mt - mb
)
local svgdoc = frontend.parse_svg_string(svg)
local svgnode = doc:create_svg_node(svgdoc, page.width, page.height)
page:output_at(0, page.height, svgnode)
end)Example: page frame (pre_shipout)
This example draws a decorative blue frame at the exact page margins using pre_shipout. Because it fires after content is placed, the frame appears on top:
local frontend = require("glu.frontend")
frontend.add_callback("pre_shipout", "page_frame", function(doc, page, pagenum, pageinfo)
local w = page.width.pt
local h = page.height.pt
local mt = pageinfo.margin_top.pt
local mb = pageinfo.margin_bottom.pt
local ml = pageinfo.margin_left.pt
local mr = pageinfo.margin_right.pt
local svg = string.format(
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 %g %g">' ..
'<rect x="%g" y="%g" width="%g" height="%g" ' ..
'fill="none" stroke="#336699" stroke-width="1.5" rx="4" ry="4"/>' ..
'</svg>',
w, h, ml, mt, w - ml - mr, h - mt - mb
)
local svgdoc = frontend.parse_svg_string(svg)
local svgnode = doc:create_svg_node(svgdoc, page.width, page.height)
page:output_at(0, page.height, svgnode)
end)Both callbacks use pageinfo to read the actual CSS margins, so decorations adapt automatically when the page layout changes.
Example: page numbers
local frontend = require("glu.frontend")
frontend.add_callback("pre_shipout", "page_number", function(doc, page, pagenum, pageinfo)
local ff = doc:load_font_family("serif")
local txt = frontend.text({
font_family = ff,
font_size = "9pt",
color = "#666666",
})
txt:append(tostring(pagenum))
local vl = doc:format_paragraph(txt, "50pt", { alignment = "center" })
local x = (page.width - frontend.sp("50pt")) / 2
page:output_at(x, frontend.sp("1.5cm"), vl)
end)Example: collecting headings
This example collects all <h1> and <h2> headings during document processing. The toc table is fully populated before any pre_shipout callback runs, so it can be used for rendering a table of contents.
local frontend = require("glu.frontend")
local toc = {}
frontend.add_callback("post_element", "toc_collect", function(element)
if element.tag_name == "h1" or element.tag_name == "h2" then
toc[#toc + 1] = {
level = element.tag_name,
text = element.text_content,
}
end
end)Example: SVG background behind headings
This example places a light blue background with a bottom line behind every <h1> heading. It uses doc:create_svg_node() to render the SVG and element:set_background() to insert it behind the heading text.
local frontend = require("glu.frontend")
frontend.add_callback("post_element", "h1_background", function(element, doc)
if element.tag_name ~= "h1" then return end
local w = element.width.pt
local h = element.height.pt
local svg = string.format(
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 %g %g">' ..
'<rect x="0" y="0" width="%g" height="%g" fill="#dbe9f6" rx="4" ry="4"/>' ..
'<line x1="0" y1="%g" x2="%g" y2="%g" stroke="#336699" stroke-width="2"/>' ..
'</svg>',
w, h, w, h, h, w, h
)
local svgdoc = frontend.parse_svg_string(svg)
local svgnode = doc:create_svg_node(svgdoc, element.width, element.height)
element:set_background(svgnode)
end)The SVG viewBox matches the element dimensions (in points), so the background covers the full heading area. The set_background() call inserts the rendered SVG behind the heading text using a negative-kern overlay technique.
A complete working example combining all callbacks is available in glu/playground/example.lua.