95

This question led to a new package:
lua-visual-debug

Some of you may know the Web Developer Toolbar for Firefox that can outline the block level elements of a page like this:alt text

Is there a way to do something similar with TeX boxes for a complete document?

topskip
  • 37,020
h0b0
  • 5,437
  • 7
  • 35
  • 34

4 Answers4

55

Another approach, usable with LuaLaTeX. The following sample document was made with the files below.

sample of the to-be-released-in-some-future lvdebug LaTeX package

The TeX input file

\documentclass{article}
\directlua{ require("drawboxes")}
\usepackage{graphicx,atbegshi}

\AtBeginShipout {\directlua{drawboxes.visual_debug()}}
\begin{document}
\hsize 3in
A wonderful serenity has taken possession of my entire soul, like these sweet
mornings of spring which I enjoy with my whole heart.

$$ e=mc^2 $$

\includegraphics[width=4cm]{cow} % from context distribution
\end{document}

and the Lua file (drawboxes.lua):

module(...,package.seeall)

local factor = 65782  -- PDF points vs. TeX points

-- This returns the node which has the glue settings
-- Old LuaTeX versions have a sub node called "spec"
-- new LuaTeX has width, stretch etc. within the glue node.
local function getgluespec(anode)
  if node.has_field(anode,"spec") then
    return anode.spec
  else
    return anode
  end
end

-- The argument must be a box (hbox or vbox)
local function draw_elements(box)
  local glue = node.id('glue')
  local hlist = node.id('hlist')
  local vlist = node.id('vlist')

  local parent = box
  local head   = box.list
  -- We are only interested in the contents of the box (box.list). But we
  -- keep the reference to the box (parent), so we know if we are in horizontal
  -- or vertical mode.

  -- head is a pointer to a node, which is a fundamental data structure in TeX
  -- for example: a node with id 1 denotes a \vbox with height, depth, and
  -- everything we know from TeX. A node with id 10 is a "glue" with the
  -- plus and minus and "1fill" values in a sub node (glue_spec)

  -- The contents of a box is a node list, connected by pointers in the attribute
  -- "next" that point to the next element in the list or nil, if there is no next
  -- element (= the last item in the box).
  while head do
    if head.id == hlist or head.id == vlist then
      -- we are in an hbox or in a vbox. Since we want to debug the contents
      -- of the box, we need to recursively call this function with the
      -- contents of the box. We supply the parent (= the current box), so
      -- we know if we are in vertical or horizontal mode and we know about the
      -- caluclated glue ratio (and sign)
      draw_elements(head)

      -- now that the contents of the current box is handled, we only need to draw
      -- a box around the contents of the box, which is stored in head.list. So we
      -- create a "pdf literal" node and insert it at the head of the list
      -- (and -- we must not forget that -- change the pointer to the contents
      -- of the list to the new "pdf literal", otherwise it exists but is not
      -- part of the box and therefore not inserted into the pdf).

      -- The dimensions of the box are stored in the attributes width, height and depth.
      local wd = head.width                  / factor
      local ht = (head.height + head.depth)  / factor
      local dp = head.depth                  / factor

      local pdfliteral = node.new("whatsit","pdf_literal")

      if head.id == hlist then -- hbox
        -- Wow, this looks complicated. It isn't. This instruction is a PDF instruction
        -- to draw a box (<ll_x> <ll_y> <ur_x> <ur_y> re s) with a 50% grey (0.5 g) and
        -- a rule width of 0.1 (0.1 w). This is enclosed in q .. Q so that the color
        -- change does not affect the next graphics operation in the PDF file.
        pdfliteral.data = string.format("q 0.5 G 0.1 w 0 %g %g %g re s Q", -dp, wd, ht)
      else
        -- a vbox is downwards, so the height must be negative
        pdfliteral.data = string.format("q 0.1 G 0.1 w 0 %g %g %g re s Q", 0, wd, -ht)
      end

      -- node.insert_before( head_of_list, current_node, node_to_insert)
      -- inserts the new node (pdfliteral) before the the first entry of the box
      -- and returns the new head of the list (which is identical to the pdf literal)
      head.list = node.insert_before(head.list,head.list,pdfliteral)

    elseif head.id == glue then
      local spec = getgluespec(head)
      local wd = spec.width -- the natural width of the glue
      local color = "0.5 G"
      -- The entries such as "plus 1fil" only take effect when the maximum glue order
      -- of the parent box has the same number of "l"s. If there is a glue with
      -- "0pt plus 1fil" and another one with "0pt plus 1 fill", the former has no effect.
      -- The glue_sign gives the "direction" (shrink/stretch) and the stretch_order and
      -- shrink_order give the maximum number of "l" of the fill commands. Only apply
      -- if they match. We also change the color of the markers to differantiate between
      -- no stretch/shrink (gray), stretch (blue) and shrink (magenta).
      if parent.glue_sign == 1 and parent.glue_order == spec.stretch_order then
        wd = wd + parent.glue_set * spec.stretch
        color = "0 0 1 RG"
      elseif parent.glue_sign == 2 and parent.glue_order == spec.shrink_order then
        wd = wd - parent.glue_set * spec.shrink
        color = "1 0 1 RG"
      end

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

      -- The parent.id tells us if the glue is horizontal or vertical
      if parent.id == hlist then
        -- The horizontal glue is drawn with a dash pattern of [0.2] 0 for small dots
        pdfliteral.data = string.format("q %s [0.2] 0 d  0.5 w 0 0  m %g 0 l s Q",color,wd / factor)
      else -- vlist
        -- The vertical glue is drawn with tiny marks at the beginning and the end
        -- and also a small dash pattern. Therefore the PDF string is rahter long.
        pdfliteral.data = string.format("q 0.1 G 0.1 w -0.5 0 m 0.5 0 l -0.5 %g m 0.5 %g l s [0.2] 0 d  0.5 w 0.25 0  m 0.25 %g l s Q",-wd / factor,-wd / factor,-wd / factor)
      end
      node.insert_before(parent.list,head,pdfliteral)
    else
      -- Any other node (for example a glyph node - a character). Probably not interesting.
    end
    -- next node in our list. If the list is at the end, head becomes nil and
    -- the loop ends.
    head = head.next
  end
end

-- The box "AtBeginShipoutBox" holds the page contents.
function visual_debug()
  draw_elements(tex.box["AtBeginShipoutBox"])
end
topskip
  • 37,020
  • Patrick this is a now piece of code. Can you please put some commentary, especially the pdfliteral part interaction? – yannisl Feb 22 '12 at 17:11
  • 4
    @YiannisLazarides I have tried to be a bit more verbose. BTW this package (with some enhancements) is uploaded to CTAN. – topskip Feb 22 '12 at 19:14
  • 1
    Thanks very much appreciated. This is more than adequate, will have a look at CTAN (still propagating). – yannisl Feb 22 '12 at 19:52
  • This is a beautiful example of the power of LuaTeX. Thank you for this. I hope things like this can demystify TeX for a lot of people, and get them to appreciate its simple and powerful boxes-and-glue model of typesetting, hiding behind all the accumulated layers of LaTeX, package, and other macros. Perhaps the next step here would be something interactive: a log of all the boxes, glues, etc., where you can click or hover over an entry in the log to see its corresponding place in the output highlighted. (May require output to svg using dvisvgm, and different specials in place of pdfliteral.) – ShreevatsaR Jan 17 '17 at 16:32
  • After looking into this a bit more (especially at nodetree and dvisvgm) I think it should be possible to implement: a html file would be generated, having on the left side an included svg for each page, and on the right side a nested list of everything (nodes, lists, boxes, glue), which is expandable/collapsable to control the amount of detail. As you hover on each element on the right, it will highlight a corresponding box on the left, using ids that were inserted into the svgs as specials. Would be wonderful. – ShreevatsaR Jan 19 '17 at 23:45
  • What's the right way to report bugs? I'm getting xes.lua:65: attempt to index field 'spec' (a nil value) stack traceback: ./drawboxes.lua:65: in function 'draw_elements' ./drawboxes.lua:105: in function 'visual_debug' [\directlua]:1: in main chunk. <argument> \directlua {drawboxes.visual_debug()} – Clément Feb 25 '19 at 15:12
  • @Clément This is a good place to report the bug. I'll have a look. – topskip Feb 26 '19 at 03:46
  • Thanks! The error happens for me on any non-empty document, like \documentclass{article} \directlua{ require("drawboxes")} \usepackage{graphicx,atbegshi} \AtBeginShipout {\directlua{drawboxes.visual_debug()}} \begin{document}ABC\end{document} – Clément Feb 26 '19 at 04:55
  • 1
    @Clément should work now. Thanks for the bug report! – topskip Feb 26 '19 at 09:12
  • Thanks for the quick fix; it works great now :) – Clément Feb 26 '19 at 21:27
  • How can I make this work for content typeset in \vbox? I am working on a package that relies heavily on \vbox. For now nothing is marked in a \vbox, i tried package lua-visual-debug – codepoet Mar 28 '20 at 05:43
  • 2
    @reportaman You could send a bug report to the author of lua-visual debug, for example at the github page or via email listed in the package documentation. Bug reports are open for questions as well. – topskip Mar 29 '20 at 09:49
46

Hans Hagen has written a "package" for that. See https://www.pragma-ade.com/articles/art-visi.pdf

This is what the introduction looks like:

visual debugging introduction

user202729
  • 7,143
topskip
  • 37,020
7

You could probably write an output routine that does this, possibly by using \vsplit and \unvbox or something. Seems like it'd be tricky.

It is possible to get TeX to write the contents of every box it outputs to the log (also to standard out, but that's probably overkill).

\documentclass{article}
\usepackage{lipsum}
\newtoks\realoutput
\realoutput\output
\output{%
        \batchmode
        \showboxbreadth\maxdimen
        \showboxdepth\maxdimen
        \showbox255
        \the\realoutput
}
\begin{document}
\lipsum
\end{document}

This saves the current output routine into a new token register \realoutput and then when the output routine is called from the page builder (I think), it dumps the contents of \box255 which contains the page. Of course, the output routine need not actually use all of \box255 so the log can contain extra information.

Of course, the output is, uh, verbose.

> \box255=
\vbox(550.0+1.94444)x345.0
.\write-{}
.\glue(\topskip) 3.05556
.\hbox(6.94444+1.94444)x345.0, glue set 0.85849
..\hbox(0.0+0.0)x15.0
..\OT1/cmr/m/n/10 L
..\OT1/cmr/m/n/10 o
..\OT1/cmr/m/n/10 r
..\OT1/cmr/m/n/10 e
..\OT1/cmr/m/n/10 m
..\glue 3.33333 plus 1.66666 minus 1.11111
..\OT1/cmr/m/n/10 i
..\OT1/cmr/m/n/10 p
..\discretionary
...\OT1/cmr/m/n/10 -
..\OT1/cmr/m/n/10 s
..\OT1/cmr/m/n/10 u
..\OT1/cmr/m/n/10 m

You can see the "Lorem ipsum" there as well as glue for spaces and basically every bit of information you'd want to know about a box.

Unfortunately, it's not exactly a visual output.

TH.
  • 62,639
3

I believe I once came across a tool that parsed synctex files (produced by running pdftex with the --synctex=-1 option) to produce this kind of display, but I can not find it at the moment.

Edit: I found it. Jerome Laurens demonstrates the tool in this presentation, starting at about 6:30. I am not sure whether this is a publicly available tool.

Dayakar
  • 39
Lev Bishop
  • 45,462
  • It might be The SyncTeX Viewer http://itexmac.sourceforge.net/SyncTeX.html I'm searching how to debug synctex files too. Unfortunately I have no MAC so I can't test it. – Linuxss Feb 15 '19 at 09:12