Creating graphics with LuaTeX nodes

Introduction

In this post I’ll give a very simple example of how you can use LuaTeX’s node machinery to create graphics. The example really is very basic but, hopefully, indicates what could be achieved with more sophisticated code. I hope it is interesting or useful and would certainly appreciate being advised if any of my explanations contain technical inaccuracies, which I will fix.

I have used Google Doc’s viewer to display PDFs throughout.

Creating your own LuaTeX setup

If you are new to LuaTeX, just how do you start exploring this amazing piece of software? My personal preference and advice is start your journey by creating your own super-minimal LuaTeX installation. For sure, it takes a bit of time to set it up, and to understand how all the bits fit together but, in my personal opinion, it’s really worth the effort and you learn a huge amount during the process. For my own experiments I use a “version” of the Plain TeX format simply because, compared to LaTeX, it has a far less complex page layout mechanism (i.e., \output routine). You can of course tame LaTeX’s page layout as I have documented here but Plain TeX is, for me, ideal for learning about the internals of LuaTeX.

A simple Lua module to help

To get started I’ve provided a very simple-minded Lua module called “nodelist” which takes a TeX box number and scans through the list of nodes from which the content of the box is built. Note that nodelist only reports on a few node types but it is easy to extend the code to report on many more node types with as much information as you want. To do that you’ll need to read The LuaTeX Reference Manual which, as of this writing, means Chapter 4 (Section 4.1) and Chapter 8.

Installing Lua modules

If you want to use the nodelist module you’ll need to put the Lua code in a location that LuaTeX will find it. That location is, or should be, defined by the LUAINPUTS entry in your texmf.cnf file. For example, in my custom setup LUAINPUTS is set to:

LUAINPUTS = .;$TEXMF/scripts

Note that the $TEXMF variable means the root directory of your TeX installation. Your LUAINPUTS variable may point to a different location, especially if you have a standard installation.

To load and initialise the nodelist module you simply issue the \directlua call

\directlua{require(“nodelist”) nodelist.inilists()}

This loads the code and then runs a function nodelist.inilists() which initialises some Lua tables containing lookup data.

Avoiding catcode problems: One of the biggest advantages of putting your Lua code into a module or executing it via dofile() or loadfile() is that you avoid complications with TeX’s category codes. For a great explanation of catcodes and in-line Lua code see the LuaTeX Wiki.

A very basic Plain TeX page

The following example provides a basic starting point. Here, we define the PDF document to be 100mm wide and 100mm tall. In addition, we set TeX’s \vsize and \hsize parameters to ensure that the final box shipped out (box 255) is the same size as our PDF page. \hoffset and \voffset are both set to -1 inch to prevent TeX from shifting the final output. With these settings, TeX’s origin is the top-left corner of our PDF page.

\pdfoutput=1
\hoffset-1in
\voffset-1in
\pdfpageheight=100mm
\pdfpagewidth=100mm
\vsize=\pdfpageheight
\hsize=\pdfpagewidth
\topskip=0pt
\directlua{require("nodelist") nodelist.inilists()}
\output={\shipout\box255}
\noindent Hello.
\bye

The typeset output of the above code is:

Onto the graphics

The following example is deliberately kept simple so that the basic ideas are easier to understand. If you have used pdfTeX you may have experimented with the \pdfliteral facility which lets you inject raw PDF “code” into the PDF being built by pdfTeX, allowing you to create all sorts of fancy effects. LuaTeX takes this one step further and lets you create nodes containing PDF data which will be output when the PDF is generated.

The following code defines a macro \makegraphic#1#2#3{…} which draws a very simple graphic using LuaTeX nodes.

\pdfoutput=1
\hoffset-1in
\voffset-1in
\pdfpageheight=100mm
\pdfpagewidth=100mm
\vsize=\pdfpageheight
\hsize=\pdfpagewidth
\topskip=0pt
\directlua{require("nodelist") nodelist.inilists()}
\output={\shipout\box255}

% A macro to draw a graphic using \directlua
\def\makegraphic#1#2#3{
\directlua{

% Start by creating a new pdf_literal node

n = node.new("whatsit","pdf_literal")

% The mode value defines how the origin is established 
% see the pdfTeX reference manual for a discussion of \pdfliteral

n.mode = 0

% Here we are generating the PDF data for our graphic
% note how we can use TeX's parameters in the for loop!

local data=""
for x=#1,#2, #3 do
data=data.." 0 "..x.." m " ..x.." 5 l "
end
data= "q".." "..data.." .5 w S Q "

% Now we have the PDF data we can attach it to our pdf_literal node

n.data=data

% Here we are creating some very stretchy glue. I'll explain why
% in the text...

g=node.new("glue")
g.subtype=0
gs=node.new("glue_spec")
gs.width=0
gs.stretch=65536
gs.stretch_order=2
g.spec=gs

% Create a copy of the glue node
 
f=node.copy(g) 

% We now have two glue nodes and a pdf_literal node so we
% need to join them together. One way is to set the "next" field
% value to chain our node list together to give [glue] [pdf_literal] [glue]

n.next=g
f.next=n

%The node.hpack function "packs" our node list together into 
% a horizontal list --- there is a node.vpack(...) too. 
% Here it sets the glue because we have given the size of 10 TeX points
% = 10*65536 and the keyword "exactly". This is like saying
% \hbox to 10pt {.....}

% We store the result in box 1000 which we can refer
% to in regular TeX code; e.g., \box1000, \copy1000

tex.box[1000]=node.hpack(f, 10*65536,"exactly")

}}

\makegraphic{0}{10}{1}
% Here we take a look at the node lists in box 1000
\directlua{nodelist.listnodes(tex.box[1000])}
\noindent This is box 1000\box1000 which is cool

\bye

Here’s the typeset result:

Some more explanations

One of the most interesting questions is where is the origin for starting our drawing? In the above code I mentioned that we set

n.mode = 0

By setting mode=0, what will happen is that when LuaTeX generates the PDF it will make a PDF transformation to establish the origin to be wherever this node ends up on the page. Other values of mode will set the origin to be the lower-left corner of the PDF page so that all your drawing operations are relative to the page corner. However, in the above example there’s a deliberate complication. Remember the stretchy glue on either side of our pdf_literal node? This has an effect on the origin (0,0) of the pdf_literal.

Drawing outside the box

Another important point is that there is nothing preventing our graphic from drawing anywhere on the page and spilling outside the box in which it is contained. To prevent this you may need to set a clipping path or make sure you don’t draw outside the bounds (width, depth, height) of the box containing your graphic.

The origin

The key point is that to TeX our pdf_literal does not have any width and it is sandwiched between two glue values. You can think of following function call

node.hpack(f, 10*65536,”exactly”)

as packaging our node list ready for assigning to a box (it creates an hlist and sets the glue). Now the point is that the size we are packinging is 10 TeX points. So, we have two very flexible glues sandwiching something of zero size and having to “fill a box” of 10 TeX points. So the glue on either side sets to 5 points and the result is that the left-hand glue pushes the origin 5 points to the right: the pdf_literal sees the origin as the middle of the box simply because the glue is set to equal values.

If you have successfully installed the nodelist module you should see the following output on your terminal:

HLIST:  width:  655360  depth:  0       height:         0       shift:  0 glue order:     2       glue sign:      1       glue set:       5
GLUE skip       width:  0       stretch:        65536   shrink:         0 stretch order:  2       shrink order:   0
pdf_literal q  0 0 m 0 5 l  0 1 m 1 5 l  0 2 m 2 5 l  0 3 m 3 5 l  0 4 m 4 5 l
0 5 m 5 5 l  0 6 m 6 5 l  0 7 m 7 5 l  0 8 m 8 5 l  0 9 m 9 5 l  0 10 m 10 5 l .5 w S Q
GLUE skip       width:  0       stretch:        65536   shrink:         0 stretch order:  2       shrink order:   0

Here we can see details of box 1000 and that the glue set ratio is 5: there is a total of 10 points to be filled (pdf_litral contributes nothing) with total glue stretch of 2 fil, hence each glue streches by 5 points.

Another view of the node list is provided by the node tree structure:

Conclusions

LuaTeX’s node machinery opens up many interesting opportunities and applications. Here we have seen a simple example but through building boxes and glue at the node level you can create very powerful and sophisticated document engineering applications.