├── .gitignore ├── Makefile ├── README.md ├── lua-visual-debug.lua ├── lua-visual-debug.sty ├── lvdebug-doc.tex ├── lvdebugdetail1-num.png ├── sample-plain.tex ├── sample.tex └── strut.png /.gitignore: -------------------------------------------------------------------------------- 1 | ctan 2 | tmp 3 | *tgz 4 | *.aux 5 | *.log 6 | *.pdf -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | VERSION = 0.9 2 | DESTDIR = lua-visual-debug 3 | DOCDEST = $(DESTDIR)/doc 4 | CP = cp -X 5 | 6 | DATE_ISO = $(shell date +"%F") 7 | DATE_TEX = $(shell date +"%Y\/%m\/%d") 8 | 9 | all: 10 | @echo "make: dist doc zip clean" 11 | 12 | dist: doc 13 | mkdir -p $(DESTDIR) 14 | mkdir -p $(DOCDEST) 15 | $(CP) README.md $(DOCDEST) 16 | $(CP) lua-visual-debug.sty $(DESTDIR) 17 | $(CP) lua-visual-debug.lua $(DESTDIR) 18 | $(CP) tmp/lvdebug-doc.tex tmp/lvdebug-doc.pdf $(DOCDEST) 19 | $(CP) sample.pdf sample.tex sample-plain.pdf sample-plain.tex $(DOCDEST) 20 | $(CP) *png $(DOCDEST) 21 | perl -pi -e 's/(lvdebugpkgversion)\{.*\}/$$1\{$(VERSION)\}/' $(DESTDIR)/lua-visual-debug.sty 22 | perl -pi -e 's/(lvdebugpkgdate)\{.*\}/$$1\{$(DATE_TEX)\}/' $(DESTDIR)/lua-visual-debug.sty 23 | perl -pi -e 's/(^-- Version:).*/$$1 $(VERSION)/' $(DESTDIR)/lua-visual-debug.lua 24 | perl -pi -e 's/(Package version:).*/$$1 $(VERSION)/' $(DOCDEST)/README.md 25 | rm -f $(DESTDIR)/README.md 26 | ( cd $(DESTDIR) ; ln -s doc/README.md ) 27 | 28 | 29 | 30 | doc: texsample latexsample 31 | mkdir -p tmp 32 | rm -rf tmp/* 33 | cp lvdebug-doc.tex tmp 34 | perl -pi -e 's/(pkgversion)\{.*\}/$$1\{$(VERSION)\}/' tmp/lvdebug-doc.tex 35 | cp *png sample-plain.tex sample.tex sample-plain-crop.pdf sample-crop.pdf tmp 36 | ( cd tmp ; lualatex lvdebug-doc.tex) 37 | ( cd tmp ; lualatex lvdebug-doc.tex) 38 | 39 | zip: clean dist 40 | -rm lvdebug-$(VERSION).tgz 41 | tar czvf lvdebug-$(VERSION).tgz $(DESTDIR)/* 42 | 43 | clean: 44 | -rm -rf tmp $(DESTDIR) 45 | -rm sample-plain.pdf sample-plain-crop.pdf sample.pdf sample-crop.pdf 46 | find . -name ".DS_Store" -exec rm {} \; 47 | 48 | 49 | 50 | sample-plain.pdf: sample-plain.tex 51 | luatex sample-plain.tex 52 | pdfcrop --margins "1 1 1 1" sample-plain.pdf 53 | 54 | sample.pdf: sample.tex 55 | lualatex sample.tex 56 | pdfcrop --margins "1 1 1 1" sample.pdf 57 | 58 | texsample: sample-plain.pdf 59 | latexsample: sample.pdf 60 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Visual debugging for LuaTeX 2 | =========================== 3 | 4 | The LuaTeX package `lua-visual-debug` shows boxes, glues, kerns and penalties in the PDF output. 5 | 6 | Usage: 7 | 8 | LaTeX: 9 | 10 | \usepackage{lua-visual-debug} 11 | 12 | or (plain) 13 | 14 | \input lua-visual-debug.sty 15 | 16 | 17 | 18 | Requirements: The package has only been tested with LuaTeX and 19 | the formats plain and LaTeX. Other formats might work as well, 20 | but other engines only show a warning message. 21 | 22 | 23 | Copyright 2012–2023 Patrick Gundlach () and others (see Git information) 24 | 25 | Package version: see Makefile 26 | 27 | Public repository: 28 | 29 | Licensed under the MIT license. See the Lua file for details. 30 | 31 | The idea is heavily inspired by Hans Hagen's 32 | 33 | 34 | Example output 35 | -------------- 36 | 37 |

38 | -------------------------------------------------------------------------------- /lua-visual-debug.lua: -------------------------------------------------------------------------------- 1 | -- Copyright 2012-2023 Patrick Gundlach, patrick@gundla.ch Public repository: 2 | -- https://github.com/pgundlach/lvdebug (issues/pull requests,...) Version: see 3 | -- Makefile 4 | 5 | -- Permission is hereby granted, free of charge, to any person obtaining a copy 6 | -- of this software and associated documentation files (the "Software"), to deal 7 | -- in the Software without restriction, including without limitation the rights 8 | -- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | -- copies of the Software, and to permit persons to whom the Software is 10 | -- furnished to do so, subject to the following conditions: 11 | -- 12 | -- The above copyright notice and this permission notice shall be included in 13 | -- all copies or substantial portions of the Software. 14 | -- 15 | -- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | -- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | -- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | -- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | -- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | -- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | -- SOFTWARE. 22 | 23 | 24 | -- There are 65782 scaled points in a PDF point 25 | -- Therefore we need to divide all TeX lengths by 26 | -- this amount to get the PDF points. 27 | local number_sp_in_a_pdf_point = 65782 28 | 29 | 30 | -- The idea is the following: at page shipout, all elements on a page are fixed. 31 | -- TeX creates an intermediate data structure before putting that into the PDF 32 | -- We can "intercept" that data structure and add pdf_literal (whatist) nodes, 33 | -- that makes glues, kerns and other items visible by drawing a rule, rectangle 34 | -- or other visual aids. This has no influence on typeset material, because 35 | -- these pdf_literal instructions are only visible to the PDF file (PDF 36 | -- renderer) and have no size themselves. 37 | 38 | -- We recursively loop through the contents of boxes and look at the (linear) 39 | -- list of items in that box. We start at the "shipout box". 40 | 41 | -- The "algorithm" goes like this: 42 | -- 43 | -- head = pointer_to_beginning_of_box_material 44 | -- while head is not nil 45 | -- if this_item_is_a_box 46 | -- recurse_into_contents 47 | -- draw a rectangle around the contents 48 | -- elseif this_item_is_a_glue 49 | -- draw a rule that has the length of that glue 50 | -- elseif this_item_is_a_kern 51 | -- draw a rectangle with width of that kern 52 | -- ... 53 | -- end 54 | -- move pointer to the next item in the list 55 | -- -- the pointer is "nil" if there is no next item 56 | -- end 57 | 58 | local HLIST = node.id("hlist") 59 | local VLIST = node.id("vlist") 60 | local RULE = node.id("rule") 61 | local DIR = node.id("dir") 62 | local DISC = node.id("disc") 63 | local GLUE = node.id("glue") 64 | local KERN = node.id("kern") 65 | local PENALTY = node.id("penalty") 66 | 67 | local function math_round(num, idp) 68 | if idp and idp>0 then 69 | local mult = 10^idp 70 | return math.floor(num * mult + 0.5) / mult 71 | end 72 | return math.floor(num + 0.5) 73 | end 74 | 75 | local curdir = {} 76 | 77 | local show_page_elements 78 | 79 | function show_page_elements(parent) 80 | local head = parent.list 81 | while head do 82 | local has_dir = false 83 | if head.dir == "TLT" then 84 | table.insert(curdir,"ltr") 85 | has_dir=true 86 | elseif head.dir == "TRT" then 87 | table.insert(curdir,"rtl") has_dir=true 88 | end 89 | if head.id == HLIST or head.id == VLIST then 90 | 91 | local rule_width = 0.1 92 | local wd = math_round(head.width / number_sp_in_a_pdf_point - rule_width ,2) 93 | local ht = math_round((head.height + head.depth) / number_sp_in_a_pdf_point - rule_width ,2) 94 | local dp = math_round(head.depth / number_sp_in_a_pdf_point - rule_width / 2 ,2) 95 | 96 | -- recurse into the contents of the box 97 | show_page_elements(head) 98 | local rectangle = node.new("whatsit","pdf_literal") 99 | if curdir[#curdir] == "rtl" then wd = wd * -1 end 100 | if head.id == HLIST then -- hbox 101 | rectangle.data = string.format("q 0.5 G %g w %g %g %g %g re s Q", rule_width, -rule_width / 2, -dp, wd, ht) 102 | else 103 | rectangle.data = string.format("q 0.1 G %g w %g %g %g %g re s Q", rule_width, -rule_width / 2, 0, wd, -ht) 104 | end 105 | head.list = node.insert_before(head.list,head.list,rectangle) 106 | 107 | 108 | elseif head.id == RULE then 109 | local show_rule = node.new("whatsit","pdf_literal") 110 | if head.width == -1073741824 or head.height == -1073741824 or head.depth == -1073741824 then 111 | -- ignore for now -- these rules are stretchable 112 | else 113 | local dp = math_round( head.depth / number_sp_in_a_pdf_point ,2) 114 | local ht = math_round( head.height / number_sp_in_a_pdf_point ,2) 115 | show_rule.data = string.format("q 1 0 0 RG 1 0 0 rg 0.4 w 0 %g m 0 %g l S Q",-dp,ht) 116 | end 117 | parent.list = node.insert_before(parent.list,head,show_rule) 118 | 119 | 120 | elseif head.id == DISC then 121 | local hyphen_marker = node.new("whatsit","pdf_literal") 122 | hyphen_marker.data = "q 0 0 1 RG 0.3 w 0 -1 m 0 0 l S Q" 123 | parent.list = node.insert_before(parent.list,head,hyphen_marker) 124 | 125 | elseif head.id == DIR then 126 | local mode = string.sub(head.dir,1,1) 127 | local texdir = string.sub(head.dir,2,4) 128 | local ldir 129 | if texdir == "TLT" then ldir = "ltr" else ldir = "rtl" end 130 | if mode == "+" then 131 | table.insert(curdir,ldir) 132 | elseif mode == "-" then 133 | local x = table.remove(curdir) 134 | if x ~= ldir then 135 | print(string.format("paragraph direction incorrect, found %s, expected %s",ldir,x)) 136 | end 137 | end 138 | 139 | elseif head.id == GLUE then 140 | local head_spec = head.spec 141 | if not head_spec then 142 | head_spec = head 143 | end 144 | local wd = head_spec.width 145 | local color = "0.5 G" 146 | if parent.glue_sign == 1 and parent.glue_order == head_spec.stretch_order then 147 | wd = wd + parent.glue_set * head_spec.stretch 148 | color = "0 0 1 RG" 149 | elseif parent.glue_sign == 2 and parent.glue_order == head_spec.shrink_order then 150 | wd = wd - parent.glue_set * head_spec.shrink 151 | color = "1 0 1 RG" 152 | end 153 | local pdfstring = node.new("whatsit","pdf_literal") 154 | local wd_bp = math_round(wd / number_sp_in_a_pdf_point,2) 155 | if curdir[#curdir] == "rtl" then wd_bp = wd_bp * -1 end 156 | 157 | if parent.id == HLIST then 158 | pdfstring.data = string.format("q %s [0.2] 0 d 0.5 w 0 0 m %g 0 l S Q", color, wd_bp) 159 | else -- vlist 160 | pdfstring.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_bp,-wd_bp,-wd_bp) 161 | end 162 | parent.list = node.insert_before(parent.list,head,pdfstring) 163 | 164 | elseif head.id == KERN then 165 | local rectangle = node.new("whatsit","pdf_literal") 166 | local color = "1 1 0 rg" 167 | if head.kern < 0 then color = "1 0 0 rg" end 168 | local k = math_round(head.kern / number_sp_in_a_pdf_point,2) 169 | if parent.id == HLIST then 170 | rectangle.data = string.format("q %s 0 w 0 0 %g 1 re B Q",color, k ) 171 | else 172 | rectangle.data = string.format("q %s 0 w 0 0 1 %g re B Q",color, -k ) 173 | end 174 | parent.list = node.insert_before(parent.list,head,rectangle) 175 | 176 | 177 | elseif head.id == PENALTY then 178 | local color = "1 g" 179 | local rectangle = node.new("whatsit","pdf_literal") 180 | if head.penalty < 10000 then 181 | color = string.format("%d g", 1 - math.floor(head.penalty / 10000)) 182 | end 183 | rectangle.data = string.format("q %s 0 w 0 0 1 1 re B Q",color) 184 | parent.list = node.insert_before(parent.list,head,rectangle) 185 | end 186 | if has_dir then 187 | table.remove(curdir) 188 | end 189 | head = head.next 190 | end 191 | return true 192 | end 193 | 194 | 195 | return { 196 | show_page_elements = show_page_elements 197 | } -------------------------------------------------------------------------------- /lua-visual-debug.sty: -------------------------------------------------------------------------------- 1 | % see lua file for copyright information (MIT License) 2 | 3 | \def\lvdebugpkgdate{2012/04/05} 4 | \def\lvdebugpkgversion{0.3} 5 | 6 | \ifx\ProvidesPackage\undefined 7 | \input ifluatex.sty\relax 8 | \else 9 | \NeedsTeXFormat{LaTeX2e} 10 | \ProvidesPackage{lua-visual-debug}[\lvdebugpkgdate\space v\lvdebugpkgversion\space Visual debugging in LuaLaTeX (PGU)] 11 | \RequirePackage{ifluatex} 12 | \fi 13 | 14 | \def\luavisualdebug@dothings{% 15 | \directlua{ lvd = require("lua-visual-debug")}% 16 | \AtBeginShipout {\directlua{lvd.show_page_elements(tex.box["AtBeginShipoutBox"])}}% 17 | } 18 | 19 | \ifx\ProvidesPackage\undefined 20 | \ifluatex 21 | \input atbegshi.sty\relax 22 | \luavisualdebug@dothings 23 | \AtBeginShipoutInit 24 | \else 25 | \message{Warning: lua-visual-debug only works with LuaTeX (plain and LaTeX format)} 26 | \fi 27 | \else 28 | \ifluatex 29 | \RequirePackage{atbegshi} 30 | \luavisualdebug@dothings 31 | \else 32 | \PackageWarning{lua-visual-debug}{You are using this package without LuaTeX. This is not supported, so you don't get any visual debugging.} 33 | \fi 34 | \fi 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /lvdebug-doc.tex: -------------------------------------------------------------------------------- 1 | \documentclass{article} 2 | \usepackage{graphicx,listings,lmodern,luatextra} 3 | \newcommand\pkgversion{see Makefile} 4 | 5 | \newcommand*\pgsmall{\fontsize{8.5}{8.7}\selectfont\ttfamily} 6 | \lstset{basicstyle=\pgsmall, 7 | basewidth=0.55em, 8 | columns=fullflexible, 9 | breakautoindent=true, 10 | breaklines=true, 11 | prebreak=, 12 | postbreak=\mbox{$\hookrightarrow$}, 13 | } 14 | 15 | \begin{document} 16 | \title{The lua-visual-debug package (V\pkgversion)} 17 | \author{Patrick Gundlach} 18 | % \address{patrick@gundla.ch} 19 | \maketitle 20 | 21 | \tableofcontents 22 | \section{About} 23 | 24 | This package aids debugging your \TeX\ and \LaTeX\ document by drawing rectangles around boxes and rules where glue is inserted. Other items are marked as well: kerns, hyphenation points and penalties. 25 | 26 | \section{How to use} 27 | 28 | When you load the package \texttt{lua-visual-debug} in your \LuaLaTeX\ document (or use \verb|\input lua-visual-debug.sty| in plain \TeX), \LuaTeX\ will highlight boxes, penalties, glues and kerns in the PDF. This package requires you to process the document with \LuaTeX\ (plain and \LaTeX formats). 29 | 30 | \section{A \LaTeX\ example} 31 | \lstinputlisting[language=tex]{sample.tex} 32 | 33 | \noindent yields \vspace{5mm} 34 | 35 | \noindent \includegraphics[width=\textwidth]{sample-crop.pdf} 36 | 37 | \section{A plain \TeX\ example} 38 | 39 | \lstinputlisting[language=tex]{sample-plain.tex} 40 | 41 | \noindent yields \vspace{5mm} 42 | 43 | \noindent \includegraphics{sample-plain-crop.pdf} 44 | 45 | \section{How to interpret the markers} % (fold) 46 | \label{sec:how_to_interpret_the_markers} 47 | 48 | \noindent\includegraphics[width=.9\textwidth]{lvdebugdetail1-num} 49 | \begin{enumerate} 50 | \item A vertical glue. Beginning and end are marked with a small tick. At the mark 1, two vertical glues are connected. 51 | \item A horizontal glue. Blue dashed lines represent stretched glues, magenta lines represent shrunk glues, gray at their natural width. 52 | \item A negative kern. Positive kerns are yellow. 53 | \item A possible hyphenation point. 54 | \item Horizontal and vertical boxes are drawn with a border. 55 | \item Penalties are marked with a square. A penalty of 10,000 is marked with a blank square, a penalty less than 10,000 is filled with a gray square (that will improve in the future, currently it is grayness of penalty / 10000). 56 | \end{enumerate} 57 | 58 | A strut box (zero width box) is marked with a red rule: 59 | 60 | \noindent\includegraphics[scale=0.8]{strut} 61 | 62 | % section how_to_interpret_the_ (end) 63 | 64 | 65 | \section{Copying} 66 | 67 | Copyright 2012–2023 Patrick Gundlach (patrick@gundla.ch) and other authors (see Git for information), licensed under the MIT license. See the Lua file for details. 68 | 69 | 70 | \end{document} 71 | -------------------------------------------------------------------------------- /lvdebugdetail1-num.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pgundlach/lvdebug/c0c2d32a1863f395d4487213b3be6fbb948aa42b/lvdebugdetail1-num.png -------------------------------------------------------------------------------- /sample-plain.tex: -------------------------------------------------------------------------------- 1 | \input lua-visual-debug.sty 2 | 3 | \hsize 3in \vsize 3in 4 | \centerline{A centered line with \TeX} 5 | 6 | \vskip .5in 7 | 8 | A wonderful serenity has taken possession of my entire soul, like these sweet 9 | mornings of spring which I enjoy with my whole heart. I am alone, and feel 10 | the charm of existence in this spot, which was created for the bliss of souls 11 | like mine. I am so happy, my dear friend, so absorbed in the exquisite sense 12 | of mere tranquil existence, that I neglect my talents. 13 | 14 | $$\int_e^x=mc^2$$ 15 | 16 | \bye 17 | -------------------------------------------------------------------------------- /sample.tex: -------------------------------------------------------------------------------- 1 | \documentclass{article} 2 | \usepackage{lua-visual-debug} 3 | 4 | \usepackage{lmodern} 5 | \setlength\textwidth{300pt} 6 | \setlength\textheight{10cm} 7 | 8 | \begin{document} 9 | 10 | \section{A short story} 11 | 12 | A wonderful serenity has taken possession of my entire soul, like these sweet 13 | mornings of spring which I enjoy with my whole heart. I am alone, and feel 14 | the charm of existence in this spot, which was created for the bliss of souls 15 | like mine. I am so happy, my dear friend, so absorbed in the exquisite sense 16 | of mere tranquil existence, that I neglect my talents\footnote{A very special note for you}. 17 | 18 | \begin{itemize} 19 | \item one 20 | \item two 21 | \item three 22 | \end{itemize} 23 | 24 | \bgroup\fontsize{30}{34}\selectfont 25 | \centerline{\TeX} 26 | \egroup 27 | 28 | \vbox{\strut Hello}\kern .5cm\vbox{\strut World} 29 | 30 | \[ \int_e^x=mc^2 \] 31 | 32 | \end{document} 33 | -------------------------------------------------------------------------------- /strut.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pgundlach/lvdebug/c0c2d32a1863f395d4487213b3be6fbb948aa42b/strut.png --------------------------------------------------------------------------------