diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index cf40554e6f4..3666fd5a9e3 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -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: diff --git a/sphinx/templates/latex/latex.tex.jinja b/sphinx/templates/latex/latex.tex.jinja index deb030504db..4ba2c46a793 100644 --- a/sphinx/templates/latex/latex.tex.jinja +++ b/sphinx/templates/latex/latex.tex.jinja @@ -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 -%> diff --git a/sphinx/texinputs/sphinx.sty b/sphinx/texinputs/sphinx.sty index 7e06eff7de8..4b7b194622d 100644 --- a/sphinx/texinputs/sphinx.sty +++ b/sphinx/texinputs/sphinx.sty @@ -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 @@ -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 + \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 diff --git a/sphinx/writers/latex.py b/sphinx/writers/latex.py index 39aef55ddfe..0553d14c303 100644 --- a/sphinx/writers/latex.py +++ b/sphinx/writers/latex.py @@ -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 + # situation perhaps best not changed (2025/06/11). match = re.match(r'^(\d*\.?\d*)\s*(\S*)$', width_str) if not match: raise ValueError @@ -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'}: @@ -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 diff --git a/tests/roots/test-latex-images-css3-lengths/conf.py b/tests/roots/test-latex-images-css3-lengths/conf.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/roots/test-latex-images-css3-lengths/img.png b/tests/roots/test-latex-images-css3-lengths/img.png new file mode 100644 index 00000000000..a97e86d66af Binary files /dev/null and b/tests/roots/test-latex-images-css3-lengths/img.png differ diff --git a/tests/roots/test-latex-images-css3-lengths/index.rst b/tests/roots/test-latex-images-css3-lengths/index.rst new file mode 100644 index 00000000000..52255262b1c --- /dev/null +++ b/tests/roots/test-latex-images-css3-lengths/index.rst @@ -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% diff --git a/tests/test_builders/test_build_latex.py b/tests/test_builders/test_build_latex.py index 16f3437c154..0e8dd4e004b 100644 --- a/tests/test_builders/test_build_latex.py +++ b/tests/test_builders/test_build_latex.py @@ -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): @@ -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 @@ -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): @@ -128,17 +134,17 @@ 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( @@ -146,7 +152,9 @@ def do_GET(self): 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 ) @@ -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')