# luatex node library: iterate over whatsit save_pos to get the position of linebreaks

Marcel's answer resolved the original question. I have added followup questions from his answer under my original question. Followup questions begin with a similar block quote as this stating "Followup questions begin here:"

Am trying to learn luatex node library, and here's what I thought I would want to do as an experiment:

1. Append a whatsit save_pos node to every line of text.
2. Iterate over all the whatsis save_pos nodes to retrieve position of linebreaks/lineendings and either save it to some array to use at the end of page or beginning of next page.

I know how to do 1., though don't know how to do 2 (could not decipher a way to do that from luatex manual).

AFAIK, positions on page are only known after a certain point in the processing of page, so I cannot probably use a linebreak callback right away to know that information. Though I do not know how to traverse all the elements of the page (at the end of page building process, or of previous page at the beginning of next page) to get a list of positions of all linebreaks. Ideally I would like to traverse all elements on the page as save_pos could be placed anywhere (inside hbox, inside vbox, in main vertical list, header/footers, or textpos overlays, etc).

Here's my code so far:

\documentclass[notitlepage,letterpaper]{article}

%\usepackage{lua-visual-debug}
\usepackage[absolute]{textpos}
\usepackage[letterpaper,left=0in,right=0in,top=1in,bottom=1in]{geometry}
\usepackage[expansion=alltext,shrink=20,stretch=20]{microtype}
\usepackage{fontspec}
\usepackage{blindtext}
%\setmainfont{Verdana} % Commented this as standard linux installs do not come with Verdana, check Marcel's comment to this question.

\directlua{
local HLIST = node.id("hlist") % node.id for a line of text in vertical list
for n in node.traverse(head) do % For every subnode within node head
if n.id==HLIST then % If its a line of text
local savepos = node.new("whatsit","save_pos")
end
end
end
luatexbase.add_to_callback('post_linebreak_filter', my_post_lb_filter, 'Play with luatex node library')
}

\begin{document}

\raggedright
\newbox\myoddvbox
\newbox\myoddvboxsplit
\global\setbox\myoddvbox=\vbox{{\hsize=2in \blindtext[1]\endgraf}}%
\setbox\myoddvboxsplit=\vsplit\myoddvbox to 4cm

\begin{textblock*}{0.5\linewidth}[0,0](2in,2in)
\begin{minipage}[t][1cm][t]{0.5\linewidth}%
\box\myoddvboxsplit%
\end{minipage}
\end{textblock*}

\begin{textblock*}{0.5\linewidth}[0,0](5in,2in)
\begin{minipage}[t][1cm][t]{0.5\linewidth}%
\box\myoddvbox%
\end{minipage}
\end{textblock*}

\null
\newpage

% replace following line with a iterate function to print all the positions from lastpage
\the\lastxpos, \the\lastypos.

\end{document}


Followup questions begin here:

1. What is the sequence of execution of late_lua nodes? Is it a FIFO model?
2. What is the nature of data that can be acted on? Can we access and late process hlist node (for instance insert_to) from a latelua function? Or is it too late for that? What is the life of a hlist node anyway, is it freed after a page is shipped out?
3. Can we pass arguments to a late_lua function? I tried passing arguments to the function savepos.data = savepos_func("hello"), and the lua segfaults.
4. luatex manual says the type of data field of late_lua node can be either string or function. When would one assign a string to a late_lua node's data field? I tried assigning a lua string savepos.data = "hello world", and it gave a error during shipout [\latelua]:1: syntax error near 'world'.
5. What is the scope of code that goes in directlua? I tried to print the size of table position_array using \AtBeginShipout of package \usepackage{atbegshi}, and it prints nil on terminal (code is simple print(tostring(table.getn(position_array))) in a \directlua macro). If I remove local from the declaration local position_array = {}, then it prints the correct value of the array (while processing next page).
• Why did you not select function getpos_and_reset to be local, while making function getpos_and_reset, and table position_array local?
• Using proprietary fonts like Verdana significantly restricts the number of people who can compile your document, so you have much better chances to get good answers if you use default and/or free fonts instead. Using default fonts also makes the example more minimal and helps in focusing on the important part of the question. Apr 22, 2020 at 7:37
• @MarcelKrüger Thanks for pointing that out. A good web-developer friend had once mentioned that Verdana & Georgia are available on all systems, from some google search it seems like not the case for standard linux installations. I will make sure not to specify any font in the *tex snippets here. As a side note, it seems like Microsoft had released its core fonts that include Georgia & Verdana, and is available for install pcworld.com/article/2863497/…; though i am not sure if its stretching the EULA a bit too far. Apr 22, 2020 at 19:33
• I think it's generally established that for the "core fonts for the web" the EULA allows it, but there are many systems where they are still not installed for multiple reasons. E.g. because people only install free software/fonts on principle, because some people don't want to install anything from Microsoft or because these fonts are just not worth the trouble (they often have to be installed manually because the license does not allow repackaging them in normal distribution packages, so you need some way to handle Windows installers...) So it's just safer not to assume anything. Apr 23, 2020 at 6:36
• @MarcelKrüger I added some followup questions from your solution. May 6, 2020 at 6:05
• Answering your third question if data field is a string it will be evaluated as a lua code so you can write savepos.data = "savepos_func('hello')" . But keep in mind that this function will run in some outer LUA scope so it needs to be global (this is just my experience) Dec 4, 2020 at 16:20

If you are using Lua code, you basically never need save_pos whatsits. save_pos whatsits are the whatsits inserted by pdfTeX's \savepos primitive. Only the position of the last save_pos is saved, so if you don't run code between two of these whatsits the first one is basically useless. (The typical use for \pdfsavepos is in combination with \write. Using code like \pdfsavepos\write42{(\the\pdflastxpos,\the\pdflastypos)} it allows to write a position to some file.) While you could use this with late_lua callback to get the current coordinates, that is already available without save_pos through pdf.getpos.

So to get an array of all linebreaks, the easiest way is to directly add late_lua nodes which save the linebreak locations during shipout:

\documentclass[notitlepage,letterpaper]{article}

%\usepackage{lua-visual-debug}
\usepackage[absolute]{textpos}
\usepackage[letterpaper,left=0in,right=0in,top=1in,bottom=1in]{geometry}
\usepackage[expansion=alltext,shrink=20,stretch=20]{microtype}
\usepackage{blindtext}

\directlua{
local position_array = {}
local function savepos_func()
position_array[\csstring\#position_array+1] = {pdf.getpos()}
end
function getpos_and_reset()
local oldpos = position_array
position_array = {}
return oldpos
end
local HLIST = node.id("hlist") % node.id for a line of text in vertical list
for n in node.traverse(head) do % For every subnode within node head
if n.id==HLIST then % If its a line of text
local savepos = node.new("whatsit","late_lua")
savepos.data = savepos_func
end
end
end
luatexbase.add_to_callback('post_linebreak_filter', my_post_lb_filter, 'Play with luatex node library')
}

\begin{document}

\raggedright
\newbox\myoddvbox
\newbox\myoddvboxsplit
\global\setbox\myoddvbox=\vbox{{\hsize=2in \blindtext[1]\endgraf}}%
\setbox\myoddvboxsplit=\vsplit\myoddvbox to 4cm

\begin{textblock*}{0.5\linewidth}[0,0](2in,2in)
\begin{minipage}[t][1cm][t]{0.5\linewidth}%
\box\myoddvboxsplit%
\end{minipage}
\end{textblock*}

\begin{textblock*}{0.5\linewidth}[0,0](5in,2in)
\begin{minipage}[t][1cm][t]{0.5\linewidth}%
\box\myoddvbox%
\end{minipage}
\end{textblock*}

\null
\newpage

% replace following line with a iterate function to print all the positions from lastpage
\directlua{
for _, pos in ipairs(getpos_and_reset()) do
tex.sprint('(' .. tostring(pos[1]) .. ', ' .. tostring(pos[2]) .. ') ')
end
}

\end{document}


A note about the use of local and global variables: As you have seen, I declared most variables to be local. This is generally a good idea to avoid polluting the global namespace. (It is also a tiny bit faster than using globals) Especially savepos_func is never used outside of the \directluablock, so it is best declaredlocal. Similar formy_post_lb_filter.

position_array can be local or global depending on when you need to access it. The current code requires you to reset the array on every page to get reliable entries, so getpos_and_reset is used as a global interface instead of making position_array directly accessible in an attempt to force people to reset the array. If you add another way of reliably resetting it before every shipout, you can safely make position_array global.

• If you want to avoid \csstring\# (which looks quite ugly), you could use table.insert instead. In any case, you should use table.pack like table.insert(position_array, table.pack(pdf.getpos())) because that preserves nils. Apr 22, 2020 at 7:48
• @HenriMenke Especially for such small tables with a known number of elements, I would recommend against using table.pack. It does not really have an advantage (since the number of elements is fixed), it is quite verbose, slow and it requires a hash part for a table which would otherwise need only an array, leading to quite some memory overhead(>=50%). Apr 22, 2020 at 8:27
• @HenriMenke, and Marcel: How about this position_array[table.getn(position_array)+1] = {pdf.getpos()}? Is there a disadvantage of using table.getn() function? (performance, or other) It removes the need for \csstring\#. I have updated my original questions with a bunch of followup questions to understand your code. Apr 22, 2020 at 22:38
• @reportaman table.getn is not standard Lua, it's an extension from the lualibs package, but in this case it should work just fine. The reason you need \csstring\# is that you cannot type # in \directlua because it still has catcode 6 and you will get an error like “invalid parameter number” or so. Apr 22, 2020 at 22:43
• @HenriMenke Seems like table.setn was deprecated, and table.getn` is only available by a compiler option in lua since version 5.1, and seems like its usage is discouraged: lua.org/manual/5.1/manual.html#7.2 Apr 22, 2020 at 23:47