Skip to main content
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:

Rendered Output

The TeX Box Hierarchy shows the progression from smallest to largest units: Character (single glyph box) flows into hbox (horizontal list of characters/words), which combines into vbox (vertical list of lines/paragraphs), ultimately forming the complete Page output. Each level nests within the next, building the document structure from individual glyphs up to full pages.

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:
  1. Direct access to TeX’s internal structures
  2. Ability to manipulate nodes and boxes
  3. Powerful debugging capabilities
  4. 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:

Rendered Output

The visualization displays “Sample Text” enclosed in a red rectangular border representing the hbox boundaries. A blue dashed line runs horizontally through the box indicating the baseline. Above the box, the width measurement “53.1pt” is displayed, showing the precise box dimensions calculated by TeX. This visual debugging technique helps identify box boundaries, baselines, and measurements during document development.

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}

3. Box Measurement Tools

\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}

Performance Profiling

\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

1. Performance Considerations

Performance Tips
  • 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

  1. Start simple: Test with minimal examples
  2. Use print statements: Track execution flow
  3. Visualize: Draw boxes to understand structure
  4. Compare: Check against known good output
  5. Profile: Measure performance impact

Quick Reference

Rendered Output

LuaTeX Box Commands Quick Reference covers three key areas: Box Access functions include tex.box[n] for accessing numbered boxes, node.traverse(head) for iterating node lists, node.traverse_id(id, head) for type-specific traversal, and node.copy_list(head) for duplicating lists. Node Properties include node.id/next/prev for navigation, box.width/height/depth for dimensions, glyph.char/font for character info, and glue.width/stretch for spacing. Common Callbacks include pre_linebreak_filter, post_linebreak_filter, pre_shipout_filter, and buildpage_filter for intercepting TeX processing at different stages.

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.