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).

Here is a test of various scenario.

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&%
\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()