From 0c021c7d2c4c2dc6fc0f594963d9b503d11333df Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Wed, 8 Sep 2021 22:33:20 -0400 Subject: [PATCH 1/3] Use correct DPI when drawing in Cairo renderers Unlike the Agg renderer, the Cairo renderer exists forever and is at the DPI when the figure was created. This needs to be updated before drawing or things in physical sizes (e.g., text or line widths) will be the wrong size. --- lib/matplotlib/backends/backend_gtk3cairo.py | 1 + lib/matplotlib/backends/backend_gtk4cairo.py | 1 + lib/matplotlib/backends/backend_qtcairo.py | 2 ++ lib/matplotlib/backends/backend_tkcairo.py | 1 + lib/matplotlib/backends/backend_wxcairo.py | 1 + 5 files changed, 6 insertions(+) diff --git a/lib/matplotlib/backends/backend_gtk3cairo.py b/lib/matplotlib/backends/backend_gtk3cairo.py index af290902d8a6..9759e15adac8 100644 --- a/lib/matplotlib/backends/backend_gtk3cairo.py +++ b/lib/matplotlib/backends/backend_gtk3cairo.py @@ -28,6 +28,7 @@ def on_draw_event(self, widget, ctx): allocation.width, allocation.height) self._renderer.set_width_height( allocation.width, allocation.height) + self._renderer.dpi = self.figure.dpi self.figure.draw(self._renderer) diff --git a/lib/matplotlib/backends/backend_gtk4cairo.py b/lib/matplotlib/backends/backend_gtk4cairo.py index 391a1a372856..53d319392727 100644 --- a/lib/matplotlib/backends/backend_gtk4cairo.py +++ b/lib/matplotlib/backends/backend_gtk4cairo.py @@ -27,6 +27,7 @@ def on_draw_event(self, widget, ctx): allocation.width, allocation.height) self._renderer.set_width_height( allocation.width, allocation.height) + self._renderer.dpi = self.figure.dpi self.figure.draw(self._renderer) diff --git a/lib/matplotlib/backends/backend_qtcairo.py b/lib/matplotlib/backends/backend_qtcairo.py index 6d0a90b985b4..9619f4fdb686 100644 --- a/lib/matplotlib/backends/backend_qtcairo.py +++ b/lib/matplotlib/backends/backend_qtcairo.py @@ -13,6 +13,7 @@ def __init__(self, figure=None): def draw(self): if hasattr(self._renderer.gc, "ctx"): + self._renderer.dpi = self.figure.dpi self.figure.draw(self._renderer) super().draw() @@ -23,6 +24,7 @@ def paintEvent(self, event): surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, width, height) self._renderer.set_ctx_from_surface(surface) self._renderer.set_width_height(width, height) + self._renderer.dpi = self.figure.dpi self.figure.draw(self._renderer) buf = self._renderer.gc.ctx.get_target().get_data() if QT_API == "PyQt6": diff --git a/lib/matplotlib/backends/backend_tkcairo.py b/lib/matplotlib/backends/backend_tkcairo.py index a81fd0d92bb8..b4099db24828 100644 --- a/lib/matplotlib/backends/backend_tkcairo.py +++ b/lib/matplotlib/backends/backend_tkcairo.py @@ -18,6 +18,7 @@ def draw(self): surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, width, height) self._renderer.set_ctx_from_surface(surface) self._renderer.set_width_height(width, height) + self._renderer.dpi = self.figure.dpi self.figure.draw(self._renderer) buf = np.reshape(surface.get_data(), (height, width, 4)) _backend_tk.blit( diff --git a/lib/matplotlib/backends/backend_wxcairo.py b/lib/matplotlib/backends/backend_wxcairo.py index 6cb0b9d68414..230ebcf527d1 100644 --- a/lib/matplotlib/backends/backend_wxcairo.py +++ b/lib/matplotlib/backends/backend_wxcairo.py @@ -35,6 +35,7 @@ def draw(self, drawDC=None): surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, width, height) self._renderer.set_ctx_from_surface(surface) self._renderer.set_width_height(width, height) + self._renderer.dpi = self.figure.dpi self.figure.draw(self._renderer) self.bitmap = wxcairo.BitmapFromImageSurface(surface) self._isDrawn = True From 630f25f6b1c44c63fbdaa553ff311a652751b20d Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Wed, 8 Sep 2021 23:06:51 -0400 Subject: [PATCH 2/3] Set device pixel ratio when saving a figure Otherwise, `FigureCanvasBase.get_width_height` returns a size scaled down by the current pixel ratio, even though the DPI is not scaled up. This causes the saved figure to be cropped. --- lib/matplotlib/backend_bases.py | 1 + lib/matplotlib/tests/test_figure.py | 29 +++++++++++++++++++++++++++-- 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/lib/matplotlib/backend_bases.py b/lib/matplotlib/backend_bases.py index fda7bd1c9613..12208d08ef00 100644 --- a/lib/matplotlib/backend_bases.py +++ b/lib/matplotlib/backend_bases.py @@ -2259,6 +2259,7 @@ def print_figure( # Remove the figure manager, if any, to avoid resizing the GUI widget. with cbook._setattr_cm(self, manager=None), \ cbook._setattr_cm(self.figure, dpi=dpi), \ + cbook._setattr_cm(canvas, _device_pixel_ratio=1), \ cbook._setattr_cm(canvas, _is_saving=True), \ ExitStack() as stack: diff --git a/lib/matplotlib/tests/test_figure.py b/lib/matplotlib/tests/test_figure.py index 317528879304..bc2256ce3c54 100644 --- a/lib/matplotlib/tests/test_figure.py +++ b/lib/matplotlib/tests/test_figure.py @@ -6,6 +6,10 @@ from types import SimpleNamespace import warnings +import numpy as np +import pytest +from PIL import Image + import matplotlib as mpl from matplotlib import cbook, rcParams from matplotlib._api.deprecation import MatplotlibDeprecationWarning @@ -16,8 +20,6 @@ import matplotlib.pyplot as plt import matplotlib.dates as mdates import matplotlib.gridspec as gridspec -import numpy as np -import pytest @image_comparison(['figure_align_labels'], extensions=['png', 'svg'], @@ -496,6 +498,29 @@ def test_savefig_backend(): fig.savefig("test.png", backend="pdf") +@pytest.mark.parametrize('backend', [ + pytest.param('Agg', marks=[pytest.mark.backend('Agg')]), + pytest.param('Cairo', marks=[pytest.mark.backend('Cairo')]), +]) +def test_savefig_pixel_ratio(backend): + fig, ax = plt.subplots() + ax.plot([1, 2, 3]) + with io.BytesIO() as buf: + fig.savefig(buf, format='png') + ratio1 = Image.open(buf) + ratio1.load() + + fig, ax = plt.subplots() + ax.plot([1, 2, 3]) + fig.canvas._set_device_pixel_ratio(2) + with io.BytesIO() as buf: + fig.savefig(buf, format='png') + ratio2 = Image.open(buf) + ratio2.load() + + assert ratio1 == ratio2 + + def test_figure_repr(): fig = plt.figure(figsize=(10, 20), dpi=10) assert repr(fig) == "
" From 959820538eb6fd418af09095d764e8f6a14c099c Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Wed, 8 Sep 2021 23:12:31 -0400 Subject: [PATCH 3/3] De-duplicate some unit conversion in Cairo --- lib/matplotlib/backends/backend_cairo.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/lib/matplotlib/backends/backend_cairo.py b/lib/matplotlib/backends/backend_cairo.py index 61cd66ce2e31..05a760542f4f 100644 --- a/lib/matplotlib/backends/backend_cairo.py +++ b/lib/matplotlib/backends/backend_cairo.py @@ -245,7 +245,7 @@ def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None): ctx.save() ctx.select_font_face(*_cairo_font_args_from_font_prop(prop)) - ctx.set_font_size(prop.get_size_in_points() * self.dpi / 72) + ctx.set_font_size(self.points_to_pixels(prop.get_size_in_points())) opts = cairo.FontOptions() opts.set_antialias( cairo.ANTIALIAS_DEFAULT if mpl.rcParams["text.antialiased"] @@ -271,7 +271,7 @@ def _draw_mathtext(self, gc, x, y, s, prop, angle): ctx.move_to(ox, -oy) ctx.select_font_face( *_cairo_font_args_from_font_prop(ttfFontProperty(font))) - ctx.set_font_size(fontsize * self.dpi / 72) + ctx.set_font_size(self.points_to_pixels(fontsize)) ctx.show_text(chr(idx)) for ox, oy, w, h in rects: @@ -303,9 +303,7 @@ def get_text_width_height_descent(self, s, prop, ismath): # save/restore prevents the problem ctx.save() ctx.select_font_face(*_cairo_font_args_from_font_prop(prop)) - # Cairo (says it) uses 1/96 inch user space units, ref: cairo_gstate.c - # but if /96.0 is used the font is too small - ctx.set_font_size(prop.get_size_in_points() * self.dpi / 72) + ctx.set_font_size(self.points_to_pixels(prop.get_size_in_points())) y_bearing, w, h = ctx.text_extents(s)[1:4] ctx.restore()