Here is a rather elaborate solution that should not affect any spacing (either horizontal or vertical) at all.
Instead of actually printing the \vrule
immediately, you could store the coordinates where you would like it to be printed and print it at this location just before shipout (which is when the page is actually being created).
The following code requires two runs to work and I've tested it with LaTeX, pdfLaTeX, LuaLaTeX and XeLaTeX. The part between \begin{document}
and \end{document}
is identical to yours, except I've replaced \mathrlap{\smash{...}}
by \printlater{...}
.
(This code appears to be incompatible with the standalone
class, probably because \paperheight
isn't the actual paper height.)
%\documentclass[varwidth=3cm,margin=2mm]{standalone} %% <- does not work, not sure why
\documentclass[a4paper]{article} %% <- paper size option is important!
\usepackage{amsmath}
%% ^ remove \ifmeasuring@\else and \fi below if you don't use amsmath
% \usepackage{mathtools} %% <- not needed
\usepackage{xcolor}
\AtBeginDocument{%
\abovedisplayskip=0pt%12pt plus 3pt minus 9pt
\abovedisplayshortskip=0pt%0pt plus 3pt
\belowdisplayskip=0pt%12pt plus 3pt minus 9pt
\belowdisplayshortskip=0pt%7pt plus 3pt minus 4pt
}
\usepackage{atbegshi} %% <- for \AtBeginShipout and \AtBeginShipoutUpperLeft
\makeatletter %% <- change @ so that it can be used in command names
\@ifundefined{savepos}
{ %% <- For pdflatex and xelatex
\let\savepos\pdfsavepos
\let\lastxpos\pdflastxpos
\let\lastypos\pdflastypos
}{} %% <- For lualatex
\newcounter{printlatercounter} %% <- unique ids for things we want to print later
\newcommand*\printlater[1]{%
\ifmeasuring@\else %% <- Do nothing if in amsmath's "measuring" mode
\stepcounter{printlatercounter}%
\expandafter\printlater@writepos\expandafter{\number\c@printlatercounter}%
\expandafter\printlater@printnow\expandafter{\number\c@printlatercounter}{#1}%
\fi
}
\newcommand*\printlater@writepos[1]{% %% <- Writes the current coordinates to the aux file
\savepos %% <- get current position on page
\write\@auxout{\global\string\@namedef{printlater@anchorx@#1}{\the\lastxpos}}% %% <- write x coord.
\write\@auxout{\global\string\@namedef{printlater@anchory@#1}{\the\lastypos}}% %% <- write y coord.
}
\newcommand*\printlater@printnow[2]{% %% <- Prints #2 at the #1-th set of coordinates
\@ifundefined{printlater@anchorx@#1}{}{% %% <- test if the coordinates are set
\AtBeginShipoutNext{% %% <- execute just before shipout
\AtBeginShipoutUpperLeft{% %% <- insert things at the top left corner of the page
\setlength{\unitlength}{1sp}% %% <- coordinates are in sp (65536sp = 1pt)
\put(\@nameuse{printlater@anchorx@#1},\the\numexpr\@nameuse{printlater@anchory@#1}-\paperheight){#2}%
}%
}%
}%
}
\makeatother %% <- return @ to normal
\begin{document}
\begin{align*} % two plain \printlaters (red)
\Phi = \printlater{\color{red}\vrule depth 5pt height 10pt width .2pt}\sum
\printlater{\color{red}\vrule depth 20pt height 10pt width .2pt}
\end{align*}
\begin{align*} % one plain \printlater (orange)
\Phi = \sum
\printlater{\color{orange}\vrule depth 20pt height 10pt width .2pt}
\end{align*}
\begin{align*} % target spacing (untouched)
\Phi = \sum
\end{align*}
\begin{align*} % one \printlater in the beginning (green)
\printlater{\color{green}\vrule depth 30pt height 80pt width .2pt}
\Phi = \sum
\end{align*}
\begin{align*} % corrected \printlaters (blue)
\Phi = \mathopen{\printlater{\color{blue}\vrule depth 5pt height 95pt width .2pt}}
\sum
\mathclose{\printlater{\color{blue}\vrule depth 5pt height 60pt width .2pt}}
\end{align*}
\end{document}
Note: the [a4paper]
option provided to \documentclass
is important because its ensures that the value of \paperheight
(which we need) is correct. By default, \paperheight
has a value of 279.4mm (letter size) even if you are producing an a4 document, which would cause all rules to be offset by 297mm - 279.4mm = 17.6mm. (This only happens if the xcolor
package is also removed, because loading this package also changes the value of \paperheight
for some reason.)

The macro \printlater
which I've defined above takes one argument and prints it at the current location just before shipout.
It does four things
- It first checks if we are in
amsmath
's "measuring mode" using \ifmeasuring@
, and if we are it does nothing. Without this check, the argument of \printlater
would be printed twice whenever it is used inside e.g. an align
environment. (See this question.)
- It increment
printlatercounter
, which generates unique ids for the things we want to be printed.
- It retrieves the current x/y-coordinates using
\(pdf)savepos
and \(pdf)last<x/y>pos
and writes them to the aux files as \printlater@anchor<x/y>@<id>
. These coordinates are in sp (65536sp = 1pt) and relative to the bottom left corner of the page.
- If
printlater@x@<id>
is defined (which will be the case on a second run), it instructs TeX to print the argument right before shipping the page. The macros \AtBeginShipoutNext
and \AtBeginShipoutUpperLeft
from the atbegshi
package take care of this.
The reason why two runs are needed is because the exact coordinates of a piece of content are not known until the page is shipped, at which time it will be too late to print anything else. We can however still write these coordinates to the aux file and then use them on the second run.
If you want to manually shift one of your \vrule
s, say by .5pt
to the left, you can just precede it by a \hskip-.5pt
(within the \printlater
).
The content that is drawn at shipout is currently in the background, behind the text (you don't notice because it doesn't touch any of the text). This might be desirable, but if you prefer it to be in the foreground you need to use \AtBeginShipoutUpperLeftForeground
instead of \AtBeginShipoutUpperLeft
,
You could also use TikZ instead of \put
to print things. You'll need to load TikZ and use:
\newcommand*\printlater@printnow[2]{%
\@ifundefined{printlater@anchorx@#1}{}{%
\AtBeginShipoutNext{%
\AtBeginShipoutUpperLeftForeground{%
\tikz[overlay,remember picture]
\node[anchor=base]
at ([xshift=\@nameuse{printlater@anchorx@#1}sp,
yshift=\@nameuse{printlater@anchory@#1}sp]current page.south west)
{\rlap{#2}};%
}%
}%
}%
}
instead of the earlier definition.
I see no reason why you would currently want to do this, but it is useful to know if you at some point decide that you would like to draw more complicated things than horizontal/vertical lines.
Edit: Here is another test, which compares the spacing around the original \colon
, amsmath
's \colon
, :
, \mathpunct:
and \mathord:
.
The position of the coloured lines is automated, and the grey lines are each 1pt
apart (starting from the red line).
\documentclass[a4paper]{article} %% <- paper size option is important!
\let\oldcolon\colon
\usepackage{amsmath}
%% ^ remove \ifmeasuring@\else and \fi below if you don't use amsmath
\let\amsmathcolon\colon
\usepackage{xcolor}
\usepackage{atbegshi} %% <- for \AtBeginShipout and \AtBeginShipoutUpperLeft
\makeatletter %% <- change @ so that it can be used in command names
\@ifundefined{savepos}
{ %% <- For pdflatex and xelatex
\let\savepos\pdfsavepos
\let\lastxpos\pdflastxpos
\let\lastypos\pdflastypos
}{} %% <- For lualatex
\newcounter{printlatercounter} %% <- unique ids for things we want to print later
\newcommand*\printlater[1]{%
\ifmeasuring@\else %% <- Do nothing if in amsmath's "measuring" mode
\stepcounter{printlatercounter}%
\expandafter\printlater@writepos\expandafter{\number\c@printlatercounter}%
\expandafter\printlater@printnow\expandafter{\number\c@printlatercounter}{#1}%
\fi
}
\newcommand*\printlater@writepos[1]{% %% <- Writes the current coordinates to the aux file
\savepos %% <- get current position on page
\write\@auxout{\global\string\@namedef{printlater@anchorx@#1}{\the\lastxpos}}% %% <- write x coord.
\write\@auxout{\global\string\@namedef{printlater@anchory@#1}{\the\lastypos}}% %% <- write y coord.
}
\newcommand*\printlater@printnow[2]{% %% <- Prints #2 at the #1-th set of coordinates
\@ifundefined{printlater@anchorx@#1}{}{% %% <- test if the coordinates are set
\AtBeginShipoutNext{% %% <- execute just before shipout
\AtBeginShipoutUpperLeft{% %% <- insert things at the top left corner of the page
\setlength{\unitlength}{1sp}% %% <- coordinates are in sp (65536sp = 1pt)
\put(\@nameuse{printlater@anchorx@#1},\the\numexpr\@nameuse{printlater@anchory@#1}-\paperheight){#2}%
}%
}%
}%
}
\makeatother %% <- return @ to normal
\begin{document}
\newcommand*\myrule[2][0pt]{\printlater{\hskip#1\color{#2}\vrule depth 5pt height 10pt width .2pt}}
\newcommand*\grayrules{\myrule[1pt]{gray}\myrule[2pt]{gray!50!white}\myrule[3pt]{gray!25!white}\myrule[4pt]{gray!12.5!white}}
\begin{align*}
f\myrule{red}\grayrules\oldcolon \myrule{green}{\myrule{blue}X} &\to Y
\\
f\myrule{red}\grayrules\amsmathcolon \myrule{green} {\myrule{blue}X} &\to Y
\\
f\myrule{red}\grayrules: \myrule{green} {\myrule{blue}X} &\to Y
\\
f\myrule{red}\grayrules\mathpunct: \myrule{green} {\myrule{blue}X} &\to Y
\\
f\myrule{red}\grayrules\mathord: \myrule{green} {\myrule{blue}X} &\to Y
\end{align*}
\end{document}

Edit: Added a comment about the necessity of the a4paper
option and fixed the double-printing problem. Thanks to @nox for pointing these things out.
\smash
, which is the vertical equivalent of\llap
/\clap
/\rlap
(and their math versions,smash
tests if you are in math mode). You could use\clap{\smash{\vrule ...}}
, but this doesn't solve your second problem.lua-visual-debug
package. It visualises glue, kerns, hyphenation points, boxes and penalties. As the name suggests, it uses LuaLaTeX.