Using LuaTeX to create SVG of typeset formulae

Introduction

This is a current work-in-progress so I’ll keep it brief and outline the ideas.

There are, of course, a number of tools available to generate SVG from TeX or, more correctly, SVG from DVI. Indeed, I wrote one such tool myself some 9 years ago: as an event-driven COM object which fired events to a Perl backend. For sure, DVI to SVG works but with LuaTeX you can do it differently and, in my opinion, in a much more natural and integrated way. The key is the node structures which result from typeset maths. By parsing the node tree you can create the data to construct the layout and generate SVG (or whatever you like).

Math node structures

Let’s take some plain TeX math and store it in a box:

\setbox101=\hbox{$\displaystyle\eqalign{\left| {1 \over \zeta - z - h} - 
{1 \over \zeta - z} \right| & = \left|{(\zeta - z) - (\zeta - z - h) \over (\zeta - z - h)(\zeta - z)}
\right| \cr & =\left| {h \over (\zeta - z - h)(\zeta - z)} \right| \cr
& \leq {2 |h| \over |\zeta - z|^2}.\cr}$}

What does the internal node structure, resulting from this math, actually look like? Well, it looks pretty complicated but in reality it’s quite easy to parse with recursive functions, visiting each node in turn and exporting the data contained in each node. Note that you must take care to preserve context by “opening” and “closing” the node data for each hlist or vlist as you return from each level of recursion.

Download PDF

The idea is that you pass box101 to Lua function which starts at the root node of the box and works its way through and down the node tree. One such function might look like this:


<<snip lots of code>>

function listnodes(head)
while head do
		local id = head.id
				if id==0 then		
					mnodes.nodedispatch[id](head, hdepth+1)
				elseif id==1 then
					mnodes.nodedispatch[id](head, vdepth+1)
					else
					mnodes.nodedispatch[id](head)
				end
			if id == node.id('hlist') or id == node.id('vlist') then
				--print("enter recursing", depth)
					if id==0 then	
						hdepth=hdepth+1
					elseif id==1 then
						vdepth=vdepth+1
						else
					end
				--mnodes.open(id, depth,head)
    			listnodes(head.list)
					if id==0 then	
						mnodes.close(id, hdepth)	
						hdepth=hdepth-1
					elseif id==1 then
						mnodes.close(id, vdepth)	
						vdepth=vdepth-1
						else
					end
				--print("return recursing", depth)
		end
	head = head.next
end
end

What you do with the data in each node depends on your objectives. My preference (current thinking) is to generate a “Lua program” which is a set of Lua functions that you can run to do the conversion. The function definitions are dictated by the conversion you want to perform. For example, the result of parsing the node tree could be something like this (lots of lines omitted):

HLISTopen(1,13295174,0,2445314,2772995,0,0,0)
MATH(0,0)
HLISTopen(2,0,0,0,0,0,0,0)
HLISTclose(2)
GLUE(skip,109224,0,0,0,0)
VLISTopen(1,13076726,0,2445314,2772995,0,0,0)
HLISTopen(2,13076726,0,622600,950280,0,0,0)
GLUE(tabskip,0,0,0,0,0)
HLISTopen(3,5670377,0,622600,950280,0,0,0)
RULE(0,557056,229376)
GLUE(skip,0,65536,0,2,0)
MATH(0,0)
HLISTopen(4,5670377,0,622600,950280,0,0,0)
HLISTopen(5,5670377,0,622600,950280,0,0,0)
VLISTopen(2,218453,-950280,1572880,0,0,0,0)
HLISTopen(6,218453,0,393220,0,0,0,0)
GLYPH(12,19,218453,0,393220)
HLISTclose(6)
KERN(0,0)
HLISTopen(6,218453,0,393220,0,0,0,0)
GLYPH(12,19,218453,0,393220)
HLISTclose(6)
KERN(0,0)
HLISTopen(6,218453,0,393220,0,0,0,0)
GLYPH(12,19,218453,0,393220)
HLISTclose(6)
KERN(0,0)
HLISTopen(6,218453,0,393220,0,0,0,0)
GLYPH(12,19,218453,0,393220)
HLISTclose(6)
VLISTclose(2)
HLISTopen(6,2805531,0,576976,865699,0,0,0)
HLISTopen(7,2805531,0,576976,865699,0,0,0)
HLISTopen(8,78643,-163840,0,0,0,0,0)
HLISTclose(8)
VLISTopen(2,2648245,0,576976,865699,0,0,0)
HLISTopen(8,2648245,0,0,422343,2,1,17)
GLUE(skip,0,65536,65536,2,2)
GLYPH(49,1,327681,422343,0)
GLUE(skip,0,65536,65536,2,2)
HLISTclose(8)
KERN(266409,0)
RULE(-1073741824,26214,0)
KERN(145167,0)
HLISTopen(8,2648245,0,127431,455111,0,0,0)
GLYPH(16,7,286721,455111,127431)
KERN(48355,0)
GLUE(medmuskip,145632,72816,145632,0,0)
GLYPH(0,13,509726,382293,54613)
GLUE(medmuskip,145632,72816,145632,0,0)
GLYPH(122,7,304775,282168,0)
KERN(28823,0)
GLUE(medmuskip,145632,72816,145632,0,0)
GLYPH(0,13,509726,382293,54613)
GLUE(medmuskip,145632,72816,145632,0,0)
GLYPH(104,7,377591,455111,0)
HLISTclose(8)
VLISTclose(2)
HLISTopen(8,78643,-163840,0,0,0,0,0)
HLISTclose(8)
HLISTclose(7)
HLISTclose(6)
GLUE(medmuskip,145632,72816,145632,0,0)
GLYPH(0,13,509726,382293,54613)
GLUE(medmuskip,145632,72816,145632,0,0)
HLISTopen(6,1626950,0,576976,865699,0,0,0)
HLISTopen(7,1626950,0,576976,865699,0,0,0)


<<snip loads of lines>>


GLYPH(58,7,182045,69176,0)
HLISTclose(4)
MATH(1,0)
GLUE(skip,0,65536,0,2,0)
HLISTclose(3)
GLUE(tabskip,0,0,0,0,0)
HLISTclose(2)
VLISTclose(1)
GLUE(skip,109224,0,0,0,0)
MATH(1,0)
HLISTclose(1)

At each node you can emit a “function” such as GLUE(skip,0,65536,65536,2,2) or GLYPH(49,1,327681,422343,0) which contain the node data as arguments of the function call. Each of these “functions” can then be “run” by providing a suitable function body: perhaps one for SVG, HTML5 canvas and JavaScript, or EPS file or whatever. The point is you can create whatever you like simply by emitting the appropriate data from each node.

What about glyph outlines?

Fortunately, there is a truly wonderful C library called FreeType which has everything you need to generate the spline data, even with TeX’s wonderfully arcane font encodings for the 8-bit world of Type 1 fonts. Of course, to plug FreeType into the overall solution you will need to write a linkable library: I use DLLs on Windows. FreeType is a really nice library, easy to use and made even more enjoyable by Lua’s C API.

Summary

Even though I have omitted a vast amount of detail, and the work is not yet finished, I hope you can see that LuaTeX offers great potential for new and powerful solutions.

One thought on “Using LuaTeX to create SVG of typeset formulae

  1. Pingback: #StackBounty: #luatex #svg svg output and LuaTeX – TechUtils.in

Comments are closed.