From e0913d466ed4e8f5fb9b03195ecd8b0dab5f43ce Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Tue, 24 Mar 2026 18:41:07 -0400 Subject: [PATCH 1/3] ps/pdf: Override font height metrics to support AFM files When outputting files using the "core 14 fonts" (e.g., `rcParams['pdf.use14corefonts'] = True`), text will be measured using our internal AFM files instead of any external files. While we don't have a full API decided for how to pass full `Text` objects through backends, add in a small internal helper to allow the PS/PDF backends to override font height metrics with the AFM files. --- lib/matplotlib/_afm.py | 8 ++++ lib/matplotlib/backends/_backend_pdf_ps.py | 26 ++++++++++++ .../test_backend_pdf/pdf_use14corefonts.pdf | Bin 3589 -> 3257 bytes lib/matplotlib/text.py | 39 +++++++++++------- 4 files changed, 57 insertions(+), 16 deletions(-) diff --git a/lib/matplotlib/_afm.py b/lib/matplotlib/_afm.py index 3d7f7a44baca..af607b0374fc 100644 --- a/lib/matplotlib/_afm.py +++ b/lib/matplotlib/_afm.py @@ -478,10 +478,18 @@ def get_angle(self) -> float: """Return the fontangle as float.""" return self._header['ItalicAngle'] + def get_ascender(self) -> float: + """Return the ascent as float.""" + return self._header['Ascender'] + def get_capheight(self) -> float: """Return the cap height as float.""" return self._header['CapHeight'] + def get_descender(self) -> float: + """Return the descent as float.""" + return self._header['Descender'] + def get_xheight(self) -> float: """Return the xheight as float.""" return self._header['XHeight'] diff --git a/lib/matplotlib/backends/_backend_pdf_ps.py b/lib/matplotlib/backends/_backend_pdf_ps.py index 83a8566517a7..a06779b8efee 100644 --- a/lib/matplotlib/backends/_backend_pdf_ps.py +++ b/lib/matplotlib/backends/_backend_pdf_ps.py @@ -348,6 +348,32 @@ def get_canvas_width_height(self): # docstring inherited return self.width * 72.0, self.height * 72.0 + def _get_font_height_metrics(self, prop): + """ + Return the ascent, descent, and line gap for font described by *prop*. + + TODO: This is a temporary method until we design a proper API for the backends. + + Parameters + ---------- + prop : `.font_manager.FontProperties` + The properties describing the font to measure. + + Returns + ------- + ascent, descent, line_gap : float or None + The ascent, descent and line gap of the determined font, or None to fall + back to normal measurements. + """ + if not mpl.rcParams[self._use_afm_rc_name]: + return None, None, None + font = self._get_font_afm(prop) + scale = prop.get_size_in_points() / 1000 + a = font.get_ascender() * scale + d = -font.get_descender() * scale + g = (a + d) * 0.2 # Preserve previous line spacing of 1.2. + return a, d, g + def get_text_width_height_descent(self, s, prop, ismath): # docstring inherited if ismath == "TeX": diff --git a/lib/matplotlib/tests/baseline_images/test_backend_pdf/pdf_use14corefonts.pdf b/lib/matplotlib/tests/baseline_images/test_backend_pdf/pdf_use14corefonts.pdf index 5cdc2e34e25d21c805df535b9e1795de50917e4b..b7dbb9adec70559ea8d3cc0a214e443db6e9e146 100644 GIT binary patch delta 999 zcmZuwOKTHR6s9&wlZjmvONtQiv``VXx%13qGz5~gsam9(2o*|^$>cUpXlEpITWe8T zg@Q}<;KG%<5cevG;BRs5%75VF%%rg~b>MQ(YtHNY?m_m;>1Wx!TMzDWU{zb#8-5$u z4ZF<)P(g+|lu8o!T7I)56-W>7Vi8!)_XKEB0K2{1>~Keb*;5yG#9~9(f`i^O+^~h< zT@Q>N^G8QyAH1}R-oD#)SN(4phKrk z|90ry$FC!AQeh?q>WhGzCq7TeaER(Q4rj)~dkUY7W$<+*XYkk96lRkZ{Fa=; zhsg}SN|s2TkDEEmwD4SNC9dW)%fy4!T0)g`ilyQA)XTka+Hi@gY+8K;qh*A~Rod7g zs!E9BzuS($WsR+-h-eW)pa84b0q=RP@QrOQT*sE!g6H@Vnry}O%3k314cP(W3|6Qq zplBFN&tM}xFN17J&TDFjc76yt`~G%AD`XT^T$&e^6^v!W1wBpwcQQgjb2^D-Q_eBu)6PuV~6jPUG>&| ZPDueGd$=m5$z# zL#481*{7S(XohT_HP}2ly(x9B zE?y&;?-?FbMKJLKE$&lmS5jdcmxJgwt`}iy1nl6r=fh=@Cd#-7s8B9R!9wp>PzoA2 zErjmla_|5RWg})PC5-f$QZS#G?#erf<*xiCai=Sv#PE^6$u9n=FVcV07wP|u;pt?A zXOatD+r1e6DLK={eBrM?m zqFQh)Js&T!HZ}$xuDG_~a>($qpmfBw7P<<43|u`dBFnhTyhYJi Date: Fri, 20 Mar 2026 19:12:46 -0400 Subject: [PATCH 2/3] TST: Restore some tolerances for some arch/platform-specific failures The tolerances on these were increased in various PRs, with notes to reduce/remove them, so they were removed in #30184, but should have been reverted to a smaller value: - `test_floating_axes.py::test_curvelinear4[png]` fails on macOS - `test_grid_helper_curvelinear.py::test_axis_direction[png]` fails on ARM and macOS - `test_grid_helper_curvelinear.py::test_polar_box[png]` fails on Windows, ARM, and macOS These fail on macOS; there's tolerances on them already, they fail by just a small amount, so increase them: - `test_bbox_tight.py::test_bbox_inches_tight_suptile_legend[png]` - `test_patheffects.py::test_collection[png]` - `test_axes3d.py::test_scale3d_artists_log[png]` This fails only on AppVeyor (Windows), but not Azure. There is a tolerance there already, so just add it to the list: - `test_mathtext.py::test_mathtext_rendering` --- lib/matplotlib/tests/test_bbox_tight.py | 2 +- lib/matplotlib/tests/test_mathtext.py | 7 +++++-- lib/matplotlib/tests/test_patheffects.py | 2 +- lib/mpl_toolkits/axisartist/tests/test_floating_axes.py | 2 +- .../axisartist/tests/test_grid_helper_curvelinear.py | 4 ++-- lib/mpl_toolkits/mplot3d/tests/test_axes3d.py | 2 +- 6 files changed, 11 insertions(+), 8 deletions(-) diff --git a/lib/matplotlib/tests/test_bbox_tight.py b/lib/matplotlib/tests/test_bbox_tight.py index 99b56bee91ba..677cdf37dd24 100644 --- a/lib/matplotlib/tests/test_bbox_tight.py +++ b/lib/matplotlib/tests/test_bbox_tight.py @@ -47,7 +47,7 @@ def test_bbox_inches_tight(text_placeholders): @image_comparison(['bbox_inches_tight_suptile_legend'], savefig_kwarg={'bbox_inches': 'tight'}, - tol=0 if platform.machine() == 'x86_64' else 0.022) + tol=0 if platform.machine() == 'x86_64' else 0.024) def test_bbox_inches_tight_suptile_legend(): plt.plot(np.arange(10), label='a straight line') plt.legend(bbox_to_anchor=(0.9, 1), loc='upper left') diff --git a/lib/matplotlib/tests/test_mathtext.py b/lib/matplotlib/tests/test_mathtext.py index c85cbc5e21a8..33fb8918d22a 100644 --- a/lib/matplotlib/tests/test_mathtext.py +++ b/lib/matplotlib/tests/test_mathtext.py @@ -224,8 +224,11 @@ def baseline_images(request, fontset, index, text): @pytest.mark.parametrize( 'fontset', ['cm', 'stix', 'stixsans', 'dejavusans', 'dejavuserif']) @pytest.mark.parametrize('baseline_images', ['mathtext'], indirect=True) -@image_comparison(baseline_images=None, style='mpl20', - tol=0.011 if platform.machine() in ('ppc64le', 's390x') else 0) +@image_comparison( + baseline_images=None, style='mpl20', + tol=(0.013 + if platform.machine() in ('ppc64le', 's390x') or platform.system() == 'Windows' + else 0)) def test_mathtext_rendering(baseline_images, fontset, index, text): mpl.rcParams['mathtext.fontset'] = fontset fig = plt.figure(figsize=(5.25, 0.75)) diff --git a/lib/matplotlib/tests/test_patheffects.py b/lib/matplotlib/tests/test_patheffects.py index d957ef2a5510..0b99a954afb3 100644 --- a/lib/matplotlib/tests/test_patheffects.py +++ b/lib/matplotlib/tests/test_patheffects.py @@ -120,7 +120,7 @@ def test_SimplePatchShadow_offset(): assert pe._offset == (4, 5) -@image_comparison(['collection'], tol=0.03, style='mpl20') +@image_comparison(['collection'], tol=0.032, style='mpl20') def test_collection(): x, y = np.meshgrid(np.linspace(0, 10, 150), np.linspace(-5, 5, 100)) data = np.sin(x) + np.cos(y) diff --git a/lib/mpl_toolkits/axisartist/tests/test_floating_axes.py b/lib/mpl_toolkits/axisartist/tests/test_floating_axes.py index 6672bd0ac3a0..5575a48d499a 100644 --- a/lib/mpl_toolkits/axisartist/tests/test_floating_axes.py +++ b/lib/mpl_toolkits/axisartist/tests/test_floating_axes.py @@ -63,7 +63,7 @@ def test_curvelinear3(): l.set_clip_path(ax1.patch) -@image_comparison(['curvelinear4.png'], style='mpl20') +@image_comparison(['curvelinear4.png'], style='mpl20', tol=0.04) def test_curvelinear4(): fig = plt.figure(figsize=(5, 5)) diff --git a/lib/mpl_toolkits/axisartist/tests/test_grid_helper_curvelinear.py b/lib/mpl_toolkits/axisartist/tests/test_grid_helper_curvelinear.py index f49d02766421..62feaee4279a 100644 --- a/lib/mpl_toolkits/axisartist/tests/test_grid_helper_curvelinear.py +++ b/lib/mpl_toolkits/axisartist/tests/test_grid_helper_curvelinear.py @@ -78,7 +78,7 @@ def inverted(self): ax1.grid(True) -@image_comparison(['polar_box.png'], style='mpl20') +@image_comparison(['polar_box.png'], style='mpl20', tol=0.04) def test_polar_box(): plt.rcParams.update({"xtick.direction": "inout", "ytick.direction": "out"}) fig = plt.figure(figsize=(5, 5)) @@ -138,7 +138,7 @@ def test_polar_box(): ax1.grid(True) -@image_comparison(['axis_direction.png'], style='mpl20') +@image_comparison(['axis_direction.png'], style='mpl20', tol=0.04) def test_axis_direction(): fig = plt.figure(figsize=(5, 5)) diff --git a/lib/mpl_toolkits/mplot3d/tests/test_axes3d.py b/lib/mpl_toolkits/mplot3d/tests/test_axes3d.py index 8d2441393dde..1cff9fdbe76e 100644 --- a/lib/mpl_toolkits/mplot3d/tests/test_axes3d.py +++ b/lib/mpl_toolkits/mplot3d/tests/test_axes3d.py @@ -2878,7 +2878,7 @@ def _make_triangulation_data(): @mpl3d_image_comparison(['scale3d_artists_log.png'], style='mpl20', - remove_text=False, tol=0.03) + remove_text=False, tol=0.032) def test_scale3d_artists_log(): """Test all 3D artist types with log scale.""" fig = plt.figure(figsize=(16, 12)) From 5e1c1ca5a5d6e84d2388e54c622b7f2b835b0ab8 Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Thu, 26 Mar 2026 16:30:28 -0400 Subject: [PATCH 3/3] BLD: Temporarily pin setuptools-scm<10 This is currently causing warnings at runtime in the editable install, which breaks almost all tests. --- .github/workflows/cygwin.yml | 2 +- .github/workflows/tests.yml | 2 +- pyproject.toml | 4 ++-- requirements/dev/build-requirements.txt | 2 +- requirements/testing/mypy.txt | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/cygwin.yml b/.github/workflows/cygwin.yml index 8a01d76c00f5..39fcedcd3862 100644 --- a/.github/workflows/cygwin.yml +++ b/.github/workflows/cygwin.yml @@ -182,7 +182,7 @@ jobs: export PATH="/usr/local/bin:$PATH" python -m pip install --no-build-isolation 'contourpy>=1.0.1' python -m pip install --upgrade cycler fonttools \ - packaging pyparsing python-dateutil setuptools-scm \ + packaging pyparsing python-dateutil 'setuptools-scm<10' \ -r requirements_test.txt sphinx ipython python -m pip install --upgrade pycairo 'cairocffi>=0.8' PyGObject && python -c 'import gi; gi.require_version("Gtk", "3.0"); from gi.repository import Gtk' && diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 6a794bfe3f6b..86c90c77b21a 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -256,7 +256,7 @@ jobs: # Preinstall build requirements to enable no-build-isolation builds. python -m pip install --upgrade $PRE \ 'contourpy>=1.0.1' cycler fonttools kiwisolver importlib_resources \ - packaging pillow 'pyparsing!=3.1.0' python-dateutil setuptools-scm \ + packaging pillow 'pyparsing!=3.1.0' python-dateutil 'setuptools-scm<10' \ 'meson-python>=0.13.1' 'pybind11>=2.13.2' \ -r requirements/testing/all.txt \ ${{ matrix.extra-requirements }} diff --git a/pyproject.toml b/pyproject.toml index 7fd25147eb05..2ede26597a31 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -49,7 +49,7 @@ requires-python = ">=3.11" dev = [ "meson-python>=0.13.2,!=0.17.*", "pybind11>=2.13.2,!=2.13.3", - "setuptools_scm>=7", + "setuptools_scm>=7,<10", # Not required by us but setuptools_scm without a version, cso _if_ # installed, then setuptools_scm 8 requires at least this version. # Unfortunately, we can't do a sort of minimum-if-installed dependency, so @@ -75,7 +75,7 @@ requires = [ # you really need it and aren't using an sdist. "meson-python>=0.13.2,!=0.17.*", "pybind11>=2.13.2,!=2.13.3", - "setuptools_scm>=7", + "setuptools_scm>=7,<10", ] [tool.meson-python.args] diff --git a/requirements/dev/build-requirements.txt b/requirements/dev/build-requirements.txt index 4d2a098c3c4f..372a7d669fb1 100644 --- a/requirements/dev/build-requirements.txt +++ b/requirements/dev/build-requirements.txt @@ -1,3 +1,3 @@ pybind11>=2.13.2,!=2.13.3 meson-python -setuptools-scm +setuptools-scm<10 diff --git a/requirements/testing/mypy.txt b/requirements/testing/mypy.txt index 343517263f40..451a096f6a96 100644 --- a/requirements/testing/mypy.txt +++ b/requirements/testing/mypy.txt @@ -22,5 +22,5 @@ packaging>=20.0 pillow>=9 pyparsing>=3 python-dateutil>=2.7 -setuptools_scm>=7 +setuptools_scm>=7,<10 setuptools>=64