Skip to content

LaTeX: support CSS3 length units #13657

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -308,6 +308,8 @@ jobs:
enable-cache: false
- name: Install dependencies
run: uv pip install . --group test
- name: Install Docutils' HEAD
run: uv pip install "docutils @ git+https://repo.or.cz/docutils.git#subdirectory=docutils"
- name: Test with pytest
run: python -m pytest -vv --durations 25
env:
Expand Down
1 change: 1 addition & 0 deletions sphinx/templates/latex/latex.tex.jinja
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
\ifdefined\pdfimageresolution
\pdfimageresolution= \numexpr \dimexpr1in\relax/\sphinxpxdimen\relax
\fi
\newdimen\sphinxremdimen\sphinxremdimen = <%= pointsize%>
%% let collapsible pdf bookmarks panel have high depth per default
\PassOptionsToPackage{bookmarksdepth=5}{hyperref}
<% if use_xindy -%>
Expand Down
24 changes: 22 additions & 2 deletions sphinx/texinputs/sphinx.sty
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
% by the Sphinx LaTeX writer.

\NeedsTeXFormat{LaTeX2e}[1995/12/01]
\ProvidesPackage{sphinx}[2025/04/24 v8.3.0 Sphinx LaTeX package (sphinx-doc)]
\ProvidesPackage{sphinx}[2025/06/11 v8.3.0 Sphinx LaTeX package (sphinx-doc)]

% provides \ltx@ifundefined
% (many packages load ltxcmds: graphicx does for pdftex and lualatex but
Expand Down Expand Up @@ -1218,5 +1218,25 @@
% FIXME: this line should be dropped, as "9" is default anyhow.
\ifdefined\pdfcompresslevel\pdfcompresslevel = 9 \fi


%%% SUPPORT FOR CSS3 EXTRA LENGTH UNITS
% cf rstdim_to_latexdim in latex.py
%
\def\sphinxchdimen{\dimexpr\fontcharwd\font`0\relax}
% TODO: decide if we want rather \textwidth/\textheight.
\newdimen\sphinxvwdimen
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LaTeXnical note: we could define (via a suitable \edef) \sphinxvwdimen and all the others as a macro expanding to some \dimexpr...\relax, but since TeXLive 2003 pdflatex embeds e-TeX extensions (last released in 1999, but LaTeX documentation in books or online is woefully outdated and almost never mention them) and, besides providing \dimexpr and \numexpr, these extensions have lifted the restriction on the number of \newdimen one can use. e-TeX also provided \fontcharwd used above in the \sphinxchdimen, it will dynamically adapt to the font used at that point in document; although Sphinx documents will not likely change fonts, but if in a footnote this will take care of the smaller font size.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

% TODO: decide if we want rather \textwidth / \textheight.

As in HTML/CSS, vw and vh relate to the viewport (~ screen size) and not the text area, I recommend keeping the conversion to \paperwidth / \paperheight.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In HTML/CSS the viewport can be fully occupied, not really in LaTeX apart from margin notes for which we have no interface. This is why \textwidth and \textheight are a serious contender in my opinion. I do not have strong opinion though.

\sphinxvwdimen=\dimexpr0.01\paperwidth\relax
\newdimen\sphinxvhdimen
\sphinxvhdimen=\dimexpr0.01\paperheight\relax
\newdimen\sphinxvmindimen
\sphinxvmindimen=\dimexpr
\ifdim\paperwidth<\paperheight\sphinxvwdimen\else\sphinxvhdimen\fi
\relax
\newdimen\sphinxvmaxdimen
\sphinxvmaxdimen=\dimexpr
\ifdim\paperwidth<\paperheight\sphinxvhdimen\else\sphinxvwdimen\fi
\relax
\newdimen\sphinxQdimen
\sphinxQdimen=0.25mm
% MEMO: \sphinxremdimen is defined in the template as it needs
% the config variable pointsize.
\endinput
15 changes: 15 additions & 0 deletions sphinx/writers/latex.py
Original file line number Diff line number Diff line change
Expand Up @@ -305,6 +305,11 @@ def escape_abbr(text: str) -> str:

def rstdim_to_latexdim(width_str: str, scale: int = 100) -> str:
"""Convert `width_str` with rst length to LaTeX length."""
# MEMO: the percent unit is interpreted here as a percentage
# of \linewidth. Let's keep in mind though that \linewidth
# is dynamic in LaTeX (e.g. smaller in lists), and may even
# be zero in some contexts (some table cells). This is a legacy
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will have to be corrected. I apologize but it seems I made a confusion with some other LaTeX length, the \baselineskip which has nothing to do with \linewidth. But it is true that \linewidth is a somewhat obscure beast in tabulary tables.

# situation perhaps best not changed (2025/06/11).
match = re.match(r'^(\d*\.?\d*)\s*(\S*)$', width_str)
if not match:
raise ValueError
Expand All @@ -318,6 +323,8 @@ def rstdim_to_latexdim(width_str: str, scale: int = 100) -> str:
res = '%sbp' % amount # convert to 'bp'
elif unit == '%':
res = r'%.3f\linewidth' % (float(amount) / 100.0)
elif unit in {'ch', 'rem', 'vw', 'vh', 'vmin', 'vmax', 'Q'}:
res = rf'{amount}\sphinx{unit}dimen'
else:
amount_float = float(amount) * scale / 100.0
if unit in {'', 'px'}:
Expand All @@ -326,8 +333,16 @@ def rstdim_to_latexdim(width_str: str, scale: int = 100) -> str:
res = '%.5fbp' % amount_float
elif unit == '%':
res = r'%.5f\linewidth' % (amount_float / 100.0)
elif unit in {'ch', 'rem', 'vw', 'vh', 'vmin', 'vmax', 'Q'}:
res = rf'{amount_float:.5f}\sphinx{unit}dimen'
else:
res = f'{amount_float:.5f}{unit}'
# MEMO: non-recognized units will in all probability end up causing
# a low-level TeX error. The units not among those above which will
# be accepted by TeX are sp (all TeX dimensions are integer multiple
# of 1sp), em and ex (font dependent), bp, cm, mm, in, and pc.
# Non-CSS units are cc, nc, dd, and nd. Also the math only mu, which
# is not usable for example for LaTeX length assignments.
return res


Expand Down
Empty file.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
25 changes: 25 additions & 0 deletions tests/roots/test-latex-images-css3-lengths/index.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
=============
TEST IMAGES
=============

test-latex-images-css3-lengths
==============================

.. image:: img.png
:width: 10.03ch
:height: 9.97rem

.. image:: img.png
:width: 60vw
:height: 10vh

.. image:: img.png
:width: 10.5vmin
:height: 10.5vmax

.. image:: img.png
:width: 195.345Q

.. image:: img.png
:width: 195.345Q
:scale: 50%
46 changes: 35 additions & 11 deletions tests/test_builders/test_build_latex.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,9 @@ def kpsetest(*filenames):


# compile latex document with app.config.latex_engine
def compile_latex_document(app, filename='projectnamenotset.tex', docclass='manual'):
def compile_latex_document(
app, filename='projectnamenotset.tex', docclass='manual', runtwice=False
):
# now, try to run latex over it
try:
with chdir(app.outdir):
Expand All @@ -82,7 +84,7 @@ def compile_latex_document(app, filename='projectnamenotset.tex', docclass='manu
# as configured in the Makefile and in presence of latexmkrc
# or latexmkjarc and also sphinx.xdy and other xindy support.
# And two passes are not enough except for simplest documents.
if app.config.latex_engine == 'pdflatex':
if runtwice:
subprocess.run(args, capture_output=True, check=True)
except OSError as exc: # most likely the latex executable was not found
raise pytest.skip.Exception from exc
Expand All @@ -101,6 +103,10 @@ def compile_latex_document(app, filename='projectnamenotset.tex', docclass='manu
not kpsetest(*STYLEFILES),
reason='not running latex, the required styles do not seem to be installed',
)
skip_if_docutils_not_at_least_at_0_22 = pytest.mark.skipif(
docutils.__version_info__[:2] < (0, 22),
reason='this test requires Docutils at least at 0.22',
)


class RemoteImageHandler(http.server.BaseHTTPRequestHandler):
Expand Down Expand Up @@ -128,25 +134,27 @@ def do_GET(self):
@skip_if_requested
@skip_if_stylefiles_notfound
@pytest.mark.parametrize(
('engine', 'docclass', 'python_maximum_signature_line_length'),
('engine', 'docclass', 'python_maximum_signature_line_length', 'runtwice'),
# Only running test with `python_maximum_signature_line_length` not None with last
# LaTeX engine to reduce testing time, as if this configuration does not fail with
# one engine, it's almost impossible it would fail with another.
[
('pdflatex', 'manual', None),
('pdflatex', 'howto', None),
('lualatex', 'manual', None),
('lualatex', 'howto', None),
('xelatex', 'manual', 1),
('xelatex', 'howto', 1),
('pdflatex', 'manual', None, True),
('pdflatex', 'howto', None, True),
('lualatex', 'manual', None, False),
('lualatex', 'howto', None, False),
('xelatex', 'manual', 1, False),
('xelatex', 'howto', 1, False),
],
)
@pytest.mark.sphinx(
'latex',
testroot='root',
freshenv=True,
)
def test_build_latex_doc(app, engine, docclass, python_maximum_signature_line_length):
def test_build_latex_doc(
app, engine, docclass, python_maximum_signature_line_length, runtwice
):
app.config.python_maximum_signature_line_length = (
python_maximum_signature_line_length
)
Expand All @@ -170,7 +178,23 @@ def test_build_latex_doc(app, engine, docclass, python_maximum_signature_line_le
# file from latex_additional_files
assert (app.outdir / 'svgimg.svg').is_file()

compile_latex_document(app, 'sphinxtests.tex', docclass)
compile_latex_document(app, 'sphinxtests.tex', docclass, runtwice)


@skip_if_requested
@skip_if_stylefiles_notfound
@skip_if_docutils_not_at_least_at_0_22
@pytest.mark.parametrize('engine', ['pdflatex', 'lualatex', 'xelatex'])
@pytest.mark.sphinx(
'latex',
testroot='latex-images-css3-lengths',
)
def test_build_latex_with_css3_lengths(app, engine):
app.config.latex_engine = engine
app.config.latex_documents = [(*app.config.latex_documents[0][:4], 'howto')]
app.builder.init()
app.build(force_all=True)
compile_latex_document(app, docclass='howto')


@pytest.mark.sphinx('latex', testroot='root')
Expand Down
Loading