6

Horizontal boxes in TeX are often padded to the right by trailing pieces of glue and kerns. As such, their width returned by \wd is larger than what is actually printed on the page. I am interested in the following problem:

Given a horizontal box, is there a way to remove (arbitrarily many consecutive) trailing pieces of glue, kerns, and whatsits from the box, while preserving the glue set ratio for all other pieces of glue in the box?

I am interested also in the case where we merely have the width of the resulting box (I will call this the real width of the original box), which would suffice for most applications. For example, if I have the box

\hbox to20pt{a\hskip0pt plus10pt a\hskip0pt plus10pt} 

Where both glues of natural width 0pt are set at a glue set ratio of +0.8 (ie. their actual width comes out to be 20pt). I wish to remove the latter piece of glue without affecting the first. A picture for reference:

Visual width of an hbox

Of course, the naïve solution of \unhboxing and using \unskip immediately fails by destroying any glue set information. Not to mention \unskip and friends cannot deal with arbitrarily many glue, kern, or whatsit items.

I suspect a solution for the general case in classical (Knuth or pdf-) TeX is impossible. However, a LuaTeX solution seems much more possible, given the existence of packages like lua-visual-debug which are far more advanced. This would be a great example to showcase the power of LuaTeX to access TeX internals.

There is a related problem that I am also interested in. Namely, the width of a vertical list is normally computed as the maximum of the widths of individual boxes in the list (ignoring rules, \moverights, etc. for now), but it would be useful to have this as the maximum of the real widths instead.

Visual width of a vertical list

It should be noted that even if we have an algorithm to compute the real width of arbitrary hboxes, it still does not suffice for this problem, since there are a myriad of issues with traversing vertical lists in classical TeX. Again, I expect LuaTeX will be able to handle this with much less difficulty.

6
  • 1
    This is actually a little tricky to do with Lua; see [1] or [2] for solutions to (somewhat) similar problems. Commented Jan 13 at 12:09
  • 1
    @UdiFogiel I think that \framed is just a fancy \hbox; I don't think that it shrinks itself to the size of the content while ignoring trailing whitespace. There might be some key/setting that makes it do this that I'm not aware of though. Commented Jan 13 at 13:01
  • 1
    If I understand well, your second problem is very similar to problem solved by OpTeX trick 0134 which is based on Lua code and LuaTeX. Commented Jan 13 at 16:22
  • 1
    @MaxChernoff see tug.org/TUGboat/tb29-3/tb93hagen-framed.pdf, I was talking about the second example. Commented Jan 13 at 17:34
  • 2
    @UdiFogiel That removes any stretch inside the box, not just the trailing stretch. Try the following (replace the | with newlines for readability): \let|=\relax | \define\content{\llap{\the\hsize\ }a\hskip 0pt plus 10pt a\hskip 0pt plus 10pt} | \setupframed[offset=overlay] | \startTEXpage[offset=1ex, loffset=\widthofstring{000.00000pt}] | \framed{\hbox to 20pt{\content}} | \framed[width=20pt, align=disable]{\content} | \framed[width=20pt, align=disable, autowidth=force]{\content} | \framed[width=fit]{\content} | \stopTEXpage Commented Jan 13 at 21:35

2 Answers 2

2

This can probably be a start, but there are probably cases where it will not give the expected result

\directlua{ 
local scan_list = token.scan_list local hlist_id, glyph_id, local_par_id = node.id('hlist'), node.id('glyph'), node.id('local_par') local glue_id, kern_id, whatsit_id = node.id('glue'), node.id('kern'), node.id('whatsit') local vlist_id = node.id('vlist') local traverse, traverse_id, write = node.traverse, node.traverse_id, node.write local hpack, vpack = node.hpack, node.vpack local flush_node = node.flush_node local hlist_subs = table.swapped(node.subtypes("hlist")) local line_subid, box_subid, indent_subid = hlist_subs['line'], hlist_subs['box'], hlist_subs['indent'] local removable = { [glue_id] = true, [kern_id] = true } local ignore = { [whatsit_id] = true, [local_par_id] = true } local process_vbox local function process_hbox(box) function process_vbox(box) local width = 0 for n, sub in traverse_id(hlist_id, box.list) do if sub == line_subid or sub == box_subid then local line_width = process_hbox(n) if line_width > width then width = line_width end end end box.width = width return width end local current, next = box.head while current do next = current.next if removable[current.id] or (current.id == hlist_id and current.subtype == indent_subid) then box.head = node.remove(box.head, current) elseif current.list and current.id == hlist_id then if process_hbox(current) ~= 0 then break end elseif current.list and current.id == vlist_id then if process_vbox(current) ~= 0 then break end elseif not ignore[current.id] then break end current = next end local current, prev = node.tail(box.head) while current do prev = current.prev if removable[current.id] or (current.id == hlist_id and current.subtype == indent_subid) then box.head = node.remove(box.head, current) elseif current.list and current.id == hlist_id then if process_hbox(current) ~= 0 then break end elseif current.list and current.id == vlist_id then if process_vbox(current) ~= 0 then break end elseif not ignore[current.id] then break end current = prev end local width = box.head and node.rangedimensions(box, box.head) or 0 box.width = width return width end local function process_box() local box = scan_list() if box.id == hlist_id then process_hbox(box) else process_vbox(box) end write(box) end local function_table = lua.get_functions_table() local processalloc = luatexbase.new_luafunction('process') token.set_lua('process', processalloc) function_table[processalloc] = process_box 
} \documentclass{article} \usepackage{color} \begin{document} \fboxsep=0pt \fbox{\hbox to20pt{\color{red}a\hskip0pt plus1fill a\hskip0pt plus1fill\hbox{ }}} \fbox{\process\hbox to20pt{\color{red}a\hskip0pt plus1fill a\hskip0pt plus1fill\hbox{ }}} \fbox{\process\vbox{ \hbox to20pt{\color{red}a\hskip0pt plus1fill a\hskip0pt plus1fill\hbox{ }} \hbox to20pt{\color{red}a\hskip0pt plus1fill a\hskip0pt plus1fill\hbox{ }} }} \fbox{\hbox to30pt{a\hskip0pt plus1fill a\hskip0pt plus1fill\hbox{a\hskip 10pt}}} \fbox{\process\hbox to30pt{a\hskip0pt plus1fill a\hskip0pt plus1fill\hbox{a\hskip 10pt}}} \end{document} 

enter image description here

1
  • 1
    Thank you so much for your answer! I think this is exactly what I want. I will test and try to understand it. Commented Jan 14 at 15:42
6

You need to distinguish luatex from classical tex and whether you need to do it within TeX or using multiple runs.

Classical TeX can remove a trailing skip or kern from a box, so could answer the question in some cases. It can also remove a trailing box (so \hbox{ } at the end but telling that a box is "all white" is tricky.)

But it can not remove characters (or even detect which characters are in a box) so if the box ends with a white space character (zero width space, or thin space or even potentially a real character 32 space character) then TeX cannot see it.

Also (and this is really the factor that kills this plan), TeX can not look back past any kind of \special so if you have

\sbox0{\color{red} a b c\hspace{3cm}} 

The final item in the node list in the box will be a color restore whatsit, and TeX will not be able to detect the 3cm space.

LuaTeX can iterate over the node list so you can determine the length you want in principle, otherwise you can use \showoutput (latex macro or equivalent tracing primitives} which will dump the box structure to the log file. You can then analyse that structure (using TeX or an external tool such as perl or python).

In practice it's all too fragile and a more robust way, if you have control over the input, is use \pdfsavepos at the points you want to save

enter image description here

\documentclass{article} \usepackage{amsmath,color} \begin{document} \makeatletter \mbox{% \pdfsavepos\write\@auxout{\gdef\string\lena{\the\pdflastxpos sp}}% \color{red} a b c% \pdfsavepos\write\@auxout{\gdef\string\lenb{\the\pdflastxpos sp}}% \hspace{3cm}} \ifx\lena\undefined\else Length \the\dimexpr\lenb-\lena\relax \fi \end{document} 
1
  • Thank you for your answer (+1)! I had not thought of using pdfsavepos with multiple TeX runs. This looks like a possible plan of attack, but unfortunately it is not applicable to my specific case. Commented Jan 13 at 14:14

You must log in to answer this question.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.