Explore the inner workings of TeX’s box model using LuaTeX’s powerful Lua integration. This advanced guide reveals how LaTeX constructs documents internally and provides practical techniques for debugging and manipulating the typesetting process.
Advanced Topic: This guide assumes strong LaTeX knowledge and basic programming experience. For LaTeX basics, start with Creating Your First Document.
What You’ll Learn
- ✅ TeX’s fundamental box model concepts
- ✅ How LaTeX builds pages from boxes
- ✅ Using LuaTeX to inspect box contents
- ✅ Practical debugging techniques
- ✅ Manipulating boxes programmatically
- ✅ Real-world applications
- ✅ Performance considerations
Introduction to TeX Boxes
What Are Boxes?
In TeX, everything on a page is built from boxes. Think of boxes as rectangular containers that hold content:
Character
Single glyph box
→
→
→
Box Types
hbox (Horizontal Box)
Contains items arranged horizontally:
- Characters in a word
- Words in a line
- Inline math
\hbox{Hello World}vbox (Vertical Box)
Contains items arranged vertically:
- Lines in a paragraph
- Paragraphs on a page
- Display math
\vbox{Line 1\Line 2}Glue (Flexible Space)
Stretchable/shrinkable space:
- Between words
- Between paragraphs
- For justification
\hskip 1em plus 2pt minus 1pt
LuaTeX: Opening Pandora’s Box
What Makes LuaTeX Special?
LuaTeX embeds the Lua programming language directly into TeX, providing:
- Direct access to TeX’s internal structures
- Ability to manipulate nodes and boxes
- Powerful debugging capabilities
- Performance optimizations through callbacks
Basic Box Inspection
\documentclass{article}
\begin{document}
% Create a simple box
\setbox0=\hbox{Hello World}
% Inspect it with Lua
\directlua{
local box = tex.box[0]
print("Box width: " .. box.width / 65536 .. "pt")
print("Box height: " .. box.height / 65536 .. "pt")
print("Box depth: " .. box.depth / 65536 .. "pt")
}
% Use the box
\box0
\end{document}
Console output:
Box width: 50.27774pt
Box height: 6.94444pt
Box depth: 0.0pt
Understanding Node Lists
Every box contains a node list - a linked list of items:
\documentclass{article}
\begin{document}
\setbox0=\hbox{Hi}
\directlua{
local box = tex.box[0]
local head = box.head
-- Traverse the node list
for node in node.traverse(head) do
print("Node type: " .. node.id ..
" (" .. node.type(node.id) .. ")")
if node.id == node.id("glyph") then
print(" Character: " .. unicode.utf8.char(node.char))
print(" Font: " .. node.font)
elseif node.id == node.id("glue") then
print(" Width: " .. node.width / 65536 .. "pt")
end
end
}
\end{document}
Console output:
Node type: 0 (glyph)
Character: H
Font: 1
Node type: 0 (glyph)
Character: i
Font: 1
Practical Box Visualization
Creating a Box Inspector
\documentclass{article}
\usepackage{xcolor}
\directlua{
function inspect_box(n)
local box = tex.box[n]
if not box then
print("Box " .. n .. " is empty")
return
end
print("\string\nBox " .. n .. " properties:")
print(" Type: " .. (box.id == 0 and "hbox" or "vbox"))
print(" Width: " .. box.width / 65536 .. "pt")
print(" Height: " .. box.height / 65536 .. "pt")
print(" Depth: " .. box.depth / 65536 .. "pt")
-- Count nodes
local count = 0
for node in node.traverse(box.head) do
count = count + 1
end
print(" Nodes: " .. count)
end
}
\newcommand{\inspectbox}[1]{%
\directlua{inspect_box(#1)}%
}
\begin{document}
% Create different types of boxes
\setbox1=\hbox{Simple text}
\setbox2=\hbox{$x^2 + y^2 = z^2$}
\setbox3=\vbox{\hsize=3cm Lorem ipsum dolor sit amet.}
\inspectbox{1}
\inspectbox{2}
\inspectbox{3}
\end{document}
Visualizing Box Structure
\documentclass{article}
\usepackage{tikz}
\directlua{
function draw_box_structure(n, x, y)
local box = tex.box[n]
if not box then return end
-- Draw box outline
tex.print("\string\\draw[red,thick] (" .. x .. "," .. y .. ") rectangle +("
.. box.width/65536 .. "pt," .. box.height/65536 .. "pt);")
-- Draw baseline
tex.print("\string\\draw[blue,dashed] (" .. x .. "," .. y .. ") -- +("
.. box.width/65536 .. "pt,0);")
-- Add dimensions
tex.print("\string\\node[above,font=\string\\tiny] at ("
.. x + box.width/131072 .. "," .. y + box.height/65536
.. ") {" .. string.format("%.1f", box.width/65536) .. "pt};")
end
}
\begin{document}
\setbox0=\hbox{Sample Text}
\begin{tikzpicture}
\directlua{draw_box_structure(0, 0, 0)}
\node at (0,0) {\box0};
\end{tikzpicture}
\end{document}
Rendered output:
Advanced Box Manipulation
Modifying Box Contents
\documentclass{article}
\directlua{
function add_color_to_glyphs(head)
for n in node.traverse(head) do
if n.id == node.id("glyph") then
-- Insert color node before glyph
local color = node.new(node.id("whatsit"),
node.subtype("pdf_colorstack"))
color.stack = 0
color.cmd = 1 -- push
color.data = "1 0 0 rg" -- red color
head = node.insert_before(head, n, color)
-- Insert color reset after glyph
local reset = node.new(node.id("whatsit"),
node.subtype("pdf_colorstack"))
reset.stack = 0
reset.cmd = 2 -- pop
head = node.insert_after(head, n, reset)
end
end
return head
end
-- Register callback
luatexbase.add_to_callback("pre_linebreak_filter",
add_color_to_glyphs,
"color glyphs")
}
\begin{document}
This text will have each character colored individually!
\end{document}
Box Metrics Analysis
\documentclass{article}
\directlua{
function analyze_paragraph_boxes()
local head = tex.lists.page_head
if not head then return end
local line_count = 0
local total_badness = 0
for n in node.traverse(head) do
if n.id == node.id("hlist") then
line_count = line_count + 1
-- Check glue settings
if n.glue_sign == 1 then -- stretching
print("Line " .. line_count ..
" stretched by factor " .. n.glue_set)
elseif n.glue_sign == 2 then -- shrinking
print("Line " .. line_count ..
" shrunk by factor " .. n.glue_set)
end
end
end
end
-- Add to shipout callback
luatexbase.add_to_callback("pre_shipout_filter",
function(head)
analyze_paragraph_boxes()
return head
end, "analyze paragraphs")
}
\begin{document}
\parbox{3cm}{
This is a narrow paragraph that will likely have
some badly stretched or compressed lines that we
can detect and analyze using our Lua code.
}
\end{document}
Real-World Applications
1. Debugging Overfull/Underfull Boxes
\documentclass{article}
\usepackage{xcolor}
\directlua{
-- Highlight overfull hboxes
function highlight_overfull_boxes(head, groupcode)
for n in node.traverse(head) do
if n.id == node.id("hlist") and n.width > tex.hsize then
-- Create a rule to highlight
local rule = node.new(node.id("rule"))
rule.width = n.width
rule.height = n.height
rule.depth = n.depth
-- Add color
local color = node.new(node.id("whatsit"),
node.subtype("pdf_colorstack"))
color.stack = 0
color.cmd = 1
color.data = "1 0 0 0.2 k" -- light red
n.head = node.insert_before(n.head, n.head, color)
n.head = node.insert_before(n.head, n.head, rule)
end
end
return head
end
luatexbase.add_to_callback("post_linebreak_filter",
highlight_overfull_boxes,
"highlight overfull")
}
\begin{document}
This line contains a verylongwordthatwillcauseanoverfullhbox in our text.
\end{document}
2. Custom Line Breaking
\documentclass{article}
\directlua{
function custom_linebreak_filter(head, is_display)
-- Get natural breaks
local copy = node.copy_list(head)
local params = {
hsize = tex.hsize,
emergencystretch = tex.emergencystretch,
pretolerance = tex.pretolerance,
tolerance = tex.tolerance
}
local breaks, info = tex.linebreak(copy, params)
-- Analyze break quality
if info.prevgraf > 5 then
-- For long paragraphs, try different parameters
params.tolerance = 2000
params.emergencystretch = tex.sp("3em")
local alt_breaks, alt_info = tex.linebreak(head, params)
if alt_info.demerits < info.demerits then
print("Using alternative line breaking")
return alt_breaks
end
end
return breaks
end
luatexbase.add_to_callback("linebreak_filter",
custom_linebreak_filter,
"custom linebreak")
}
\begin{document}
\noindent This paragraph demonstrates custom line
breaking logic that adjusts parameters based on
paragraph length and quality metrics.
\end{document}
\documentclass{article}
\directlua{
function measure_content(text)
-- Create temporary box
local box = node.hpack(
node.copy_list(
tex.nest[tex.nest.ptr].head
)
)
print("Content measurements:")
print(" Width: " .. box.width / 65536 .. "pt")
print(" Height: " .. box.height / 65536 .. "pt")
print(" Depth: " .. box.depth / 65536 .. "pt")
-- Calculate ink coverage
local glyph_area = 0
for n in node.traverse(box.head) do
if n.id == node.id("glyph") then
glyph_area = glyph_area + n.width * n.height
end
end
local total_area = box.width * (box.height + box.depth)
local coverage = glyph_area / total_area * 100
print(" Ink coverage: " .. string.format("%.1f%%", coverage))
end
}
\newcommand{\measure}[1]{%
\setbox0=\hbox{#1}%
\directlua{
local b = tex.box[0]
measure_content()
}%
\box0%
}
\begin{document}
\measure{Sample text for measurement}
\end{document}
Debugging Techniques
Visual Box Debugging
\documentclass{article}
\usepackage{xcolor}
\directlua{
local show_boxes = true
function visualize_boxes(head, groupcode)
if not show_boxes then return head end
for n in node.traverse(head) do
if n.id == node.id("hlist") or n.id == node.id("vlist") then
-- Add colored frame
local rule = node.new(node.id("rule"))
rule.width = tex.sp("0.1pt")
rule.height = n.height + tex.sp("2pt")
rule.depth = n.depth + tex.sp("2pt")
-- Different colors for different box types
local color = n.id == node.id("hlist") and
"0 0 1" or "1 0 0" -- blue/red
-- Insert visualization
-- (simplified for clarity)
end
end
return head
end
}
% Toggle command
\newcommand{\showboxes}{\directlua{show_boxes = true}}
\newcommand{\hideboxes}{\directlua{show_boxes = false}}
\begin{document}
\showboxes
This text will show box boundaries.
\hideboxes
This text will not.
\end{document}
\documentclass{article}
\directlua{
local stats = {
total_boxes = 0,
total_glyphs = 0,
total_glue = 0,
processing_time = 0
}
function profile_document(head)
local start_time = os.clock()
for n in node.traverse_id(node.id("hlist"), head) do
stats.total_boxes = stats.total_boxes + 1
for m in node.traverse(n.head) do
if m.id == node.id("glyph") then
stats.total_glyphs = stats.total_glyphs + 1
elseif m.id == node.id("glue") then
stats.total_glue = stats.total_glue + 1
end
end
end
stats.processing_time = os.clock() - start_time
return head
end
function print_stats()
print("\string\nDocument Statistics:")
print(" Total boxes: " .. stats.total_boxes)
print(" Total glyphs: " .. stats.total_glyphs)
print(" Total glue nodes: " .. stats.total_glue)
print(" Processing time: " ..
string.format("%.3f", stats.processing_time) .. "s")
end
luatexbase.add_to_callback("post_linebreak_filter",
profile_document,
"profile")
}
\AtEndDocument{\directlua{print_stats()}}
\begin{document}
Your document content here...
\end{document}
Best Practices
- Cache calculations: Store results of expensive operations
- Minimize traversals: Use specific node types when possible
- Batch operations: Group modifications together
- Clean up: Free unused nodes with
node.free()
2. Safety Guidelines
Important Safety Rules:
- Always check if nodes exist before accessing
- Use
node.copy_list() when modifying shared content
- Be careful with callbacks - they affect all processing
- Test thoroughly - box manipulation can break output
3. Debugging Workflow
- Start simple: Test with minimal examples
- Use print statements: Track execution flow
- Visualize: Draw boxes to understand structure
- Compare: Check against known good output
- Profile: Measure performance impact
Quick Reference
LuaTeX Box Commands
Box Access
tex.box[n]node.traverse(head)node.traverse_id(id, head)node.copy_list(head)Node Properties
node.id, node.next, node.prevbox.width, box.height, box.depthglyph.char, glyph.fontglue.width, glue.stretchCallbacks
pre_linebreak_filterpost_linebreak_filterpre_shipout_filterbuildpage_filter
Further Resources
LaTeX Cloud Studio supports LuaTeX! Enable it in your project settings to use these advanced features. Our platform provides enhanced debugging output and visualization tools.