Skip to content

Conversation

@gucci-on-fleek
Copy link

With tfmdata.mode = 2 (PDF stroke and fill), if you set tfmdata.width to a non-zero integer, then the glyphs are stroked with a line of that thickness; this is documented in the LuaTeX manual. If you set tfmdata.width to zero, then the stroke width will be inherited from the environment; this isn't documented anywhere, but it's somewhat useful, and someone is probably relying on it. If you set tfmdata.width to a non-zero value that rounds to zero, then you get the same behaviour as if you set it to exactly zero; this is somewhat surprising, since it means that in most cases, tfmdata.width = 0.49 produces much thicker glyphs than tfmdata.width = 0.51. Example:

\input{luaotfload.sty}

\font\TestFontA={lmroman10-regular:embolden=0.000;} at 10.0pt
\font\TestFontB={lmroman10-regular:embolden=0.006;} at 10.0pt
\font\TestFontC={lmroman10-regular:embolden=0.005;} at 10.0pt

\def\Test#1{%
     \par
     {\tt\string#1}:
     \pdfextension literal {10       w} #1 ooo
     \pdfextension literal { 1       w} #1 ooo
     \pdfextension literal { 0.00001 w} #1 ooo
}

\nopagenumbers
\Test\TestFontA (Expected)
\Test\TestFontB (Expected)
\Test\TestFontC (Weird!)
\bye

The LuaTeX engine developers aren't planning on fixing this and this current behaviour is incompatible with XeLaTeX so this commit modifies the embolden manipulator to check if the computed stroke width is non-zero but would round to zero; if so, it sets the stroke width to 1 instead. This ensures that the stroke width will be inherited from an outer PDF group if and only if the embolden factor is exactly zero.

An alternative (and simpler) solution would be to use math.ceil to unconditionally rounds up the computed embolden width; this could potentially cause backwards compatibility issues since this means that half of all embolden values would now be 0.001pt thicker than before. However, since this is applied only by the PDF renderer, this would not affect line breaking.

With "tfmdata.mode = 2" (PDF stroke and fill), if you set
"tfmdata.width" to a non-zero integer, then the glyphs are stroked with
a line of that thickness; this is documented in the LuaTeX manual. If
you set "tfmdata.width" to zero, then the stroke width will be inherited
from the environment; this isn't documented anywhere, but it's somewhat
useful, and someone is probably relying on it. If you set
"tfmdata.width" to a non-zero value that rounds to zero, then you get
the same behaviour as if you set it to exactly zero; this is somewhat
surprising, since it means that in most cases, "tfmdata.width = 0.49"
produces much thicker glyphs than "tfmdata.width = 0.51". Example:

     \input{luaotfload.sty}

     \font\TestFontA={lmroman10-regular:embolden=0.000;} at 10.0pt
     \font\TestFontB={lmroman10-regular:embolden=0.006;} at 10.0pt
     \font\TestFontC={lmroman10-regular:embolden=0.005;} at 10.0pt

     \def\Test#1{%
          \par
          {\tt\string#1}:
          \pdfextension literal {10       w} latex3#1 ooo
          \pdfextension literal { 1       w} latex3#1 ooo
          \pdfextension literal { 0.00001 w} latex3#1 ooo
     }

     \nopagenumbers
     \Test\TestFontA (Expected)
     \Test\TestFontB (Expected)
     \Test\TestFontC (Weird!)
     \bye

The LuaTeX engine developers aren't planning on fixing this

     https://mailman.ntg.nl/archives/list/dev-luatex@ntg.nl/thread/7BRXFIAOVQDHMBZADBREXOV6YECYFHS3/

and this current behaviour is incompatible with XeLaTeX

     https://tex.stackexchange.com/q/755049

so this commit modifies the "embolden" manipulator to check if the
computed stroke width is non-zero but would round to zero; if so, it
sets the stroke width to 1 instead. This ensures that the stroke width
will be inherited from an outer PDF group if and only if the embolden
factor is exactly zero.

An alternative (and simpler) solution would be to use "math.ceil" to
unconditionally rounds up the computed embolden width; this could
potentially cause backwards compatibility issues since this means that
half of all embolden values would now be 0.001pt thicker than before.
However, since this is applied only by the PDF renderer, this would not
affect line breaking.
@cfr42
Copy link

cfr42 commented Nov 22, 2025

The behaviour will still be incompatible with XeLaTeX. It just won't be quite as horrible. Moreover, not only very small values don't work. Only integer values are respected, whereas XeTeX seems to treat this as a continuum.

[But I agree that it is better to round to something than have the current behaviour. I just think the choices here are unfortunate.]

@gucci-on-fleek
Copy link
Author

@cfr42

Moreover, not only very small values don't work. Only integer values are respected, whereas XeTeX seems to treat this as a continuum.

Internally, only integer values work, but this is in units of 1/1000bp, which is small enough that it appears continuous. I would assume that XeTeX uses a similar representation, since it's fairly rare for TeX engines to use floating point. The default value of \pdfdecimaldigits in both pdfLaTeX and LuaLaTeX is 3, so units of 0.001bp is equally precise as every other PDF operation.

@cfr42
Copy link

cfr42 commented Nov 22, 2025

@cfr42

Moreover, not only very small values don't work. Only integer values are respected, whereas XeTeX seems to treat this as a continuum.

Internally, only integer values work, but this is in units of 1/1000bp, which is small enough that it appears continuous. I would assume that XeTeX uses a similar representation, since it's fairly rare for TeX engines to use floating point. The default value of \pdfdecimaldigits in both pdfLaTeX and LuaLaTeX is 3, so units of 0.001bp is equally precise as every other PDF operation.

You are right. Sorry. I believed the OP.

However, XeTeX appears to round e.g. 0.001 down rather than up. I don't know how to do this for XeTeX in plain, but e.g.

\documentclass{article}
\usepackage{fontspec}
\setmainfont[FakeBold=0.001]{Latin Modern Roman}
\setsansfont[FakeBold=1]{Latin Modern Roman}
\setmonofont{Latin Modern Roman}
\begin{document}
\parindent=0pt
\rmfamily ABCDE (0.001)

\sffamily ABCDE (1)

\ttfamily ABCDE (N/A)
\end{document}

compiled with XeLaTeX suggests that, if it is rounding, the engine treats 0.001 as 0 rather than 1, which is arguably more intuitive. So setting the width to 1 will give significantly different results for the two engines. (I think?)

@gucci-on-fleek
Copy link
Author

@cfr42

However, XeTeX appears to round e.g. 0.001 down rather than up.

[…]

with XeLaTeX suggests that, if it is rounding, the engine treats 0.001 as 0 rather than 1, which is arguably more intuitive. So setting the width to 1 will give significantly different results for the two engines. (I think?)

Ah, we're both talking about different coordinate spaces. The LuaTeX engine expects for tfmdata.width to be an absolute stroke width (which can be directly written to the PDF) in units of 0.001bp; the XeTeX engine takes the embolden parameter, divides it by 10, multiplies it by the font size, and then writes it to the PDF (treating pt as bp for some reason). luaotfload internally converts from the “XeTeX enbolden coordinate space” to the “LuaTeX tfmdata.width coordinate space”; I'm only rounding the value after it's been transformed by luaotfload from the XeTeX coordinate space into the LuaTeX coordinate space (which is ~100× times larger for 10pt font).

Demonstration:

\ifdefined\luatexversion
    \input{luaotfload.sty}
    \def\pdfliteral#1{\pdfextension literal{#1}}
\else\ifdefined\XeTeXversion
    \def\pdfliteral#1{\special{pdf:code #1}}
\else
    \errmessage{This test requires either LuaTeX or XeTeX}
\fi\fi

\font\TestFontA="[lmroman10-regular]:embolden= 0.000;" at 10pt
\font\TestFontB="[lmroman10-regular]:embolden= 0.006;" at 10pt
\font\TestFontC="[lmroman10-regular]:embolden= 0.005;" at 10pt
\font\TestFontD="[lmroman10-regular]:embolden= 1.000;" at 10pt
\font\TestFontE="[lmroman10-regular]:embolden=10.000;" at 10pt

\def\Test#1{%
    \par
    {\tt\string#1:}
    \pdfliteral{10       w} #1 ooo
    \pdfliteral{ 1       w} #1 ooo
    \pdfliteral{ 0.00001 w} #1 ooo
}

\nopagenumbers
\Test\TestFontA
\Test\TestFontB
\Test\TestFontC
\Test\TestFontD
\Test\TestFontE
\bye
LuaTeX (Original) LuaTeX (With this PR) XeTeX
luatex before luatex after xetex

@u-fischer
Copy link
Member

As you just investigated all this: Could you also add as part of this PR a few sentences to the luaotfload documentation about which values of embolden make actually sense and what they roughly do?

@gucci-on-fleek
Copy link
Author

@u-fischer

Could you also add as part of this PR a few sentences to the luaotfload documentation about which values of embolden make actually sense and what they roughly do?

Sure, I'll add that in tomorrow.

@gucci-on-fleek
Copy link
Author

Ok, I've updated the documentation for the embolden parameter. I couldn't get the manual to compile, so I haven't tested it, but it's probably fine.

gucci-on-fleek added a commit to gucci-on-fleek/fontspec that referenced this pull request Nov 23, 2025
gucci-on-fleek added a commit to gucci-on-fleek/fontspec that referenced this pull request Nov 23, 2025
gucci-on-fleek added a commit to gucci-on-fleek/fontspec that referenced this pull request Nov 23, 2025
wspr pushed a commit to latex3/fontspec that referenced this pull request Nov 24, 2025
@cfr42
Copy link

cfr42 commented Dec 1, 2025

Ah, we're both talking about different coordinate spaces. The LuaTeX engine expects for tfmdata.width to be an absolute stroke width (which can be directly written to the PDF) in units of 0.001bp; the XeTeX engine takes the embolden parameter, divides it by 10, multiplies it by the font size, and then writes it to the PDF (treating pt as bp for some reason). luaotfload internally converts from the “XeTeX enbolden coordinate space” to the “LuaTeX tfmdata.width coordinate space”; I'm only rounding the value after it's been transformed by luaotfload from the XeTeX coordinate space into the LuaTeX coordinate space (which is ~100× times larger for 10pt font).

Thank you for taking the time to explain this.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants