4

If you are familiar with amsthm, you probably know that the default font for theorems is italic. If you put inline math into a theorem (or more generally, inside italic text), it gets somehow shifted to the left with respect to what you would expect if the content was typeset as pure text only.

This is especially noticeable for isolated capital letters like $U$, and in my opinion this can be visually displeasing. Is there a way to fix this behavior (other than writing the symbols as non-math letters)? Maybe with some kind of italic (un)correction?

Here is a very minimal working example:

\documentclass{article}

\usepackage{amsmath,amsthm}

\newtheorem{theorem}{Theorem}

\begin{document}

\begin{theorem}
  Consider the following:

  Let $U$ and $V$ be open sets (math mode).

  Let U and V be open sets (pure text).
\end{theorem}

\end{document}

As you can see in the output, the $U$ and $V$ letters are shifted to the left with respect to their pure text equivalent (you can zoom on the image if it is too small to see the offset). As a result, there is a large gap after them, which in my opinion looks quite ugly.

Issue

3
2

I was very frustrated not to be able to fix the issue with CM font, because I use amsthm often and I like the plain theorem style and want their bodies to be italic. So I tried to improve the "crazy solution" proposed by Hendrik Vogt in Bad spacing of math letters within italic text. Below is a working example, which relies on defining a new theorem style: fixedplain.

If you want to use the code below to fix the position of math letters when using AMS plain theorem style, please note the following:

  • you need to put what's between "start of patch" and "end of patch" after \usepackage{amsthm} and before you use \newtheorem
  • it will only affect the theorem bodies
  • it will try to figure out if inline math enclosed with $...$ starts or ends with a letter, and apply some horizontal skips to mimic the usual text flow of italic text, thus removing the unwanted shift to the left you usually get;
  • if will try to mimic the behavior of the default font (cmr10), and will probably end up with wrong offsets for other fonts (although it could be more or less correct if you only change the font size). However, see below how you can generate an offset table for other fonts;
  • it will improperly detect e.g. that $2^n$ ends with a letter, but you can fix it by using $2^{n}$ instead;
  • you can still get the standard behavior if needed, by using \(...\);
  • it works by making $ active. As a consequence, it will fail:
    • whenever you use $$...$$ for display math (but this is considered bad practice and you should avoid using it anyway, see Why is \[\] preferable to $$ … $$?)
    • it will fail, possibly with a compile error, if you imbricate $'s, which can happen if you need math inside a \text{...} (still, I cannot figure a reasonable way that would cause a compile error with valid inline math input). But in that case, you could use \( and \) for the interior math, at least to avoid the error.
\documentclass{article}

\usepackage{amsthm,xcolor}

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
% Start of patch
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

\makeatletter

\def\fix@mchar#1#2#3{%
  \expandafter\edef\csname mcl@#1\endcsname{#2}%
  \expandafter\edef\csname mcr@#1\endcsname{#3}%
}

\fix@mchar a{+1.168}{+0.972}\fix@mchar b{+1.199}{+0.319}\fix@mchar c{+1.162}{+0.397}
\fix@mchar d{+1.043}{+0.862}\fix@mchar e{+1.156}{+0.890}\fix@mchar f{-1.500}{+3.336}
\fix@mchar g{+0.664}{+1.127}\fix@mchar h{+0.362}{+1.194}\fix@mchar i{+0.837}{+1.250}
\fix@mchar j{-0.534}{+2.108}\fix@mchar k{+0.362}{+1.603}\fix@mchar l{+0.866}{+1.837}
\fix@mchar m{+0.863}{+1.001}\fix@mchar n{+0.863}{+0.970}\fix@mchar o{+1.158}{+0.246}
\fix@mchar p{+0.832}{+0.381}\fix@mchar q{+1.073}{+1.163}\fix@mchar r{+0.863}{+1.662}
\fix@mchar s{+0.325}{+1.037}\fix@mchar t{+0.992}{+1.401}\fix@mchar u{+0.849}{+0.904}
\fix@mchar v{+0.847}{+1.479}\fix@mchar w{+0.853}{+1.529}\fix@mchar x{+0.351}{+1.817}
\fix@mchar y{+0.826}{+1.100}\fix@mchar z{+0.230}{+1.705}\fix@mchar A{+0.132}{-0.123}
\fix@mchar B{+0.274}{+1.419}\fix@mchar C{+1.662}{+2.411}\fix@mchar D{+0.270}{+1.207}
\fix@mchar E{+0.299}{+1.712}\fix@mchar F{+0.299}{+2.078}\fix@mchar G{+1.662}{+1.334}
\fix@mchar H{+0.277}{+2.528}\fix@mchar I{+0.300}{+2.346}\fix@mchar J{+0.087}{+2.076}
\fix@mchar K{+0.280}{+2.267}\fix@mchar L{+0.277}{+0.880}\fix@mchar M{+0.301}{+2.686}
\fix@mchar N{+0.284}{+2.536}\fix@mchar O{+1.662}{+1.434}\fix@mchar P{+0.270}{+1.669}
\fix@mchar Q{+1.541}{+1.406}\fix@mchar R{+0.252}{+0.272}\fix@mchar S{+0.322}{+1.715}
\fix@mchar T{+2.385}{+2.169}\fix@mchar U{+2.377}{+2.649}\fix@mchar V{+2.417}{+3.155}
\fix@mchar W{+2.291}{+2.885}\fix@mchar X{+0.167}{+2.666}\fix@mchar Y{+2.617}{+3.323}
\fix@mchar Z{+0.254}{+2.329}

\def\get@lastchar#1#2${%
  \if\relax#2\relax%
    #1%
  \else%
    \if#2 %
      #2%
    \else
      \get@lastchar#2$%
    \fi%
  \fi%
}

\def\get@firstchar#1#2${#1}

\let\math@org=$

\newlength\mylength

\def\itinlinemath#1{%
  \edef\leftskip@csname{mcl@\expandafter\get@firstchar\detokenize{#1}$}%
  \edef\rightskip@csname{mcr@\expandafter\get@lastchar\detokenize{#1}$}%
  \math@org%
  \ifcsname\leftskip@csname\endcsname%
    \null\mskip\csname\leftskip@csname\endcsname mu%
  \fi%
  #1%
  \ifcsname\rightskip@csname\endcsname%
    \mskip-\csname\rightskip@csname\endcsname mu%
  \fi%
  \math@org%
}

\begingroup
  \catcode`\$=13
  \gdef\activateitalicmath{%
    \catcode`\$=13%
    \def${\math@org}%
    \def${\math@org}%
    \def$##1${\itinlinemath{##1}}%
  }
\endgroup

\newtheoremstyle{fixedplain}{\topsep}{\topsep}{\itshape\activateitalicmath}{0pt}{\bfseries}{.}{5pt plus 1pt minus 1pt}{}

%Remove the following line to disable patch and restore default behavior
\theoremstyle{fixedplain}

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
% End of patch
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

\newtheorem{theorem}{Theorem}

\begin{document}

\begin{theorem}[Single letters]
  If $U$ or $V$ and $X$, and $f$ from $j$. Let $T$ be $S$ if $Y$.
\end{theorem}

\begin{theorem}[Unaffected math]
  If $2U^2$ or $+V_1$ and $-X\alpha$, and $\frac f2$ from $2+j^{x}$. Let $\overline{T}$ be ${{S}}$ if \(Y\).
\end{theorem}

\end{document}

Here is the result (with the default behavior below).

Theorem test

Here is a test of various scenario.

More tests

To "mimic" the behavior of the italic font and generate an offset table for letters, here is the LaTeX code I have been using.

\documentclass{article}

\usepackage[paperheight=40cm,paperwidth=5cm,margin=0pt]{geometry}

\pagestyle{empty}

\def\refrule{\raisebox{-1ex}{\rule{2em}{4ex}}}

\def\glyph#1{%
  \par%
  \noindent%
  {\refrule} #1 {\refrule} $#1$ {\refrule}%
}

\def\multiglyph #1#2.{
  \glyph#1%
  \ifx&#2&%
  \else%
    \multiglyph#2.
  \fi%
}

\begin{document}

\raggedright\itshape\multiglyph abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ.

\end{document}

This file is then compiled and converted to png image (using imagemagick) by the following python script, which will then compute approximate left and right offsets for each character. It is quite experimental, but at least it does work for cmr10 and seems to give consistent results. If you want to use another font, simply change the beginning of the above file to load your font, and relaunch the python script.

from PIL import Image
import numpy as np
import matplotlib.pyplot as plt
import subprocess

def open_image(path):
    i = Image.open(path).convert('L')
    a = np.frombuffer(i.tobytes(), np.uint8).astype(np.float32)
    return a.reshape((i.size[1], i.size[0]))

subprocess.run("pdflatex glyphs.tex", check = True, shell = True)
subprocess.run("convert -density 1000 glyphs.pdf glyphs.png", check = True, shell = True)

image = open_image("glyphs.png")
image[:, 0] = 255

glyph_centers = []
rule_width, rule_height = 0.0, 0
in_glyph = False
for k in range(image.shape[0]):
    bounds = np.where(np.diff(image[k]))[0]
    glyph_found = len(bounds) > 5
    if in_glyph ^ glyph_found:
        if glyph_found:
            glyph_top = k
            left_before, left_after, left_height, right_before, right_after, right_height = 0.0, 0.0, 0, 0.0, 0.0, 0
        else:
            left_before /= left_height
            left_after /= left_height
            right_before /= right_height
            right_after /= right_height
            glyph_centers.append([left_before, left_after, right_before, right_after])
        in_glyph = glyph_found
    if in_glyph:
        lengths = (bounds - np.roll(bounds, 1))[1::2]
        rules = np.sort(np.argpartition(lengths, -3)[-3:])
        rule_width += np.sum(bounds[rules * 2 + 1] - bounds[rules * 2])
        rule_height += 1
        a, b = bounds[rules[0] * 2 + 2], bounds[rules[1] * 2 - 1]
        image[k, a + 1: b + 1] = 127
        if b > a:
            left_before += a - bounds[rules[0] * 2 + 1]
            left_after += bounds[rules[1] * 2] - b
            left_height += 1
        a, b = bounds[rules[1] * 2 + 2], bounds[rules[2] * 2 - 1]
        image[k, a + 1: b + 1] = 127
        if b > a:
            right_before += a - bounds[rules[1] * 2 + 1]
            right_after += bounds[rules[2] * 2] - b
            right_height += 1
rule_width = rule_width / (3 * rule_height)
assert(len(glyph_centers) == 52)
k = 0
ruletoem = 2 # Rule is 2 em large
emtomu = 18  # 1em -> 18 mu

f = open("cmr10.fix","wt") #Change file name as needed
for c in "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ":
    delta_before = (glyph_centers[k][0] - glyph_centers[k][2]) / rule_width * ruletoem * emtomu
    delta_after = (glyph_centers[k][3] - glyph_centers[k][1]) / rule_width * ruletoem * emtomu
    s = "\\fix@mchar {:}{{{:+.3f}}}{{{:+.3f}}}\n".format(c, delta_before, delta_after)
    f.write(s)
    k += 1
f.close()

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.