diff --git a/src/spatialdata_plot/pl/utils.py b/src/spatialdata_plot/pl/utils.py index de10715d..6068533f 100644 --- a/src/spatialdata_plot/pl/utils.py +++ b/src/spatialdata_plot/pl/utils.py @@ -244,14 +244,17 @@ def _prepare_params_plot( # handle axes and size wspace = 0.75 / rcParams["figure.figsize"][0] + 0.02 if wspace is None else wspace figsize = rcParams["figure.figsize"] if figsize is None else figsize - dpi = rcParams["figure.dpi"] if dpi is None else dpi + # When creating a new figure, fall back to rcParams; when the user provides + # their own axes, preserve the figure's existing DPI (only override if + # the user explicitly passed dpi= to show()). + resolved_dpi = rcParams["figure.dpi"] if dpi is None else dpi if num_panels > 1 and ax is None: fig, grid = _panel_grid( num_panels=num_panels, hspace=hspace, wspace=wspace, ncols=ncols, - dpi=dpi, + dpi=resolved_dpi, figsize=figsize, ) axs: None | Sequence[Axes] = [plt.subplot(grid[c]) for c in range(num_panels)] @@ -266,14 +269,16 @@ def _prepare_params_plot( ) assert ax is None or isinstance(ax, Sequence), f"Invalid type of `ax`: {type(ax)}, expected `Sequence`." axs = ax + if dpi is not None: + fig.set_dpi(dpi) else: axs = None if ax is None: - fig, ax = plt.subplots(figsize=figsize, dpi=dpi, constrained_layout=True) + fig, ax = plt.subplots(figsize=figsize, dpi=resolved_dpi, constrained_layout=True) elif isinstance(ax, Axes): - # needed for rasterization if user provides Axes object fig = ax.get_figure() - fig.set_dpi(dpi) + if dpi is not None: + fig.set_dpi(dpi) # set scalebar if scalebar_dx is not None: @@ -1955,10 +1960,10 @@ def _rasterize_if_necessary( target_y_dims = dpi * height target_x_dims = dpi * width - # Heuristics for when to rasterize + # Rasterize when the source image is substantially larger than what the + # current figure DPI × size requires. The +100 margin avoids rasterizing + # when the image is only slightly larger than the target. do_rasterization = y_dims > target_y_dims + 100 or x_dims > target_x_dims + 100 - if x_dims < 2000 and y_dims < 2000: - do_rasterization = False if do_rasterization: logger.info("Rasterizing image for faster rendering.") diff --git a/tests/_images/ColorbarControls_colorbar_can_have_colorbars_on_different_sides.png b/tests/_images/ColorbarControls_colorbar_can_have_colorbars_on_different_sides.png index 0df7acc8..6a20052c 100644 Binary files a/tests/_images/ColorbarControls_colorbar_can_have_colorbars_on_different_sides.png and b/tests/_images/ColorbarControls_colorbar_can_have_colorbars_on_different_sides.png differ diff --git a/tests/_images/ColorbarControls_colorbar_img_bottom.png b/tests/_images/ColorbarControls_colorbar_img_bottom.png index 70023e16..9ffa0f96 100644 Binary files a/tests/_images/ColorbarControls_colorbar_img_bottom.png and b/tests/_images/ColorbarControls_colorbar_img_bottom.png differ diff --git a/tests/_images/ColorbarControls_colorbar_img_top.png b/tests/_images/ColorbarControls_colorbar_img_top.png index 0d04d952..6924ca0e 100644 Binary files a/tests/_images/ColorbarControls_colorbar_img_top.png and b/tests/_images/ColorbarControls_colorbar_img_top.png differ diff --git a/tests/_images/Labels_can_control_label_outline.png b/tests/_images/Labels_can_control_label_outline.png index b8773818..b4d9e948 100644 Binary files a/tests/_images/Labels_can_control_label_outline.png and b/tests/_images/Labels_can_control_label_outline.png differ diff --git a/tests/_images/Labels_can_render_outline_color.png b/tests/_images/Labels_can_render_outline_color.png index e8587204..3b40f031 100644 Binary files a/tests/_images/Labels_can_render_outline_color.png and b/tests/_images/Labels_can_render_outline_color.png differ diff --git a/tests/_images/Labels_can_render_outline_with_fill.png b/tests/_images/Labels_can_render_outline_with_fill.png index 59b7938a..67d5143e 100644 Binary files a/tests/_images/Labels_can_render_outline_with_fill.png and b/tests/_images/Labels_can_render_outline_with_fill.png differ diff --git a/tests/_images/Labels_can_stack_render_labels.png b/tests/_images/Labels_can_stack_render_labels.png index 4b77937f..c7796b84 100644 Binary files a/tests/_images/Labels_can_stack_render_labels.png and b/tests/_images/Labels_can_stack_render_labels.png differ diff --git a/tests/_images/Labels_label_colorbar_uses_alpha_of_less_transparent_infill.png b/tests/_images/Labels_label_colorbar_uses_alpha_of_less_transparent_infill.png index cab6937f..f6b5a95a 100644 Binary files a/tests/_images/Labels_label_colorbar_uses_alpha_of_less_transparent_infill.png and b/tests/_images/Labels_label_colorbar_uses_alpha_of_less_transparent_infill.png differ diff --git a/tests/_images/Labels_outline_uses_data_driven_colors.png b/tests/_images/Labels_outline_uses_data_driven_colors.png index 2f624c7b..18cd7956 100644 Binary files a/tests/_images/Labels_outline_uses_data_driven_colors.png and b/tests/_images/Labels_outline_uses_data_driven_colors.png differ diff --git a/tests/_images/NotebooksTransformations_can_render_transformations_raccoon_affine.png b/tests/_images/NotebooksTransformations_can_render_transformations_raccoon_affine.png index 7c61a3c7..467d81a2 100644 Binary files a/tests/_images/NotebooksTransformations_can_render_transformations_raccoon_affine.png and b/tests/_images/NotebooksTransformations_can_render_transformations_raccoon_affine.png differ diff --git a/tests/_images/Show_user_ax_dpi_preserved.png b/tests/_images/Show_user_ax_dpi_preserved.png new file mode 100644 index 00000000..ca509852 Binary files /dev/null and b/tests/_images/Show_user_ax_dpi_preserved.png differ diff --git a/tests/pl/test_show.py b/tests/pl/test_show.py index fec5b49a..de3fb5a8 100644 --- a/tests/pl/test_show.py +++ b/tests/pl/test_show.py @@ -42,6 +42,16 @@ def test_plot_no_decorations(self, sdata_blobs: SpatialData): """Visual test: frameon=False + title='' produces just the plot content (regression for #204).""" sdata_blobs.pl.render_images(element="blobs_image").pl.show(frameon=False, title="", colorbar=False) + def test_plot_user_ax_dpi_preserved(self, sdata_blobs: SpatialData): + """Visual test: low DPI produces visibly pixelated rasterization (regression for #310). + + Uses dpi=15 so the 512x512 blobs image is downsampled to ~96x72. + If the bug regresses and DPI is overridden to the default (~100), + no rasterization occurs and the sharper render fails comparison. + """ + fig, ax = plt.subplots(dpi=15) + sdata_blobs.pl.render_images(element="blobs_image").pl.show(ax=ax) + def test_no_plt_show_when_ax_provided(self, sdata_blobs: SpatialData): """plt.show() must not be called when the user supplies ax= (regression for #362).""" _, ax = plt.subplots() @@ -110,3 +120,27 @@ def test_frameon_false_multi_panel(sdata_blobs: SpatialData): for ax in axs: assert not ax.axison plt.close("all") + + +def test_user_figure_dpi_preserved_when_ax_provided(sdata_blobs: SpatialData): + """User's figure DPI must not be overridden when ax is passed without explicit dpi (regression for #310).""" + fig, ax = plt.subplots(dpi=300) + sdata_blobs.pl.render_images(element="blobs_image").pl.show(ax=ax, show=False) + assert fig.get_dpi() == 300 + plt.close(fig) + + +def test_explicit_dpi_overrides_figure_dpi(sdata_blobs: SpatialData): + """Explicit dpi= in show() should override the figure's DPI.""" + fig, ax = plt.subplots(dpi=300) + sdata_blobs.pl.render_images(element="blobs_image").pl.show(ax=ax, dpi=150, show=False) + assert fig.get_dpi() == 150 + plt.close(fig) + + +def test_dpi_default_used_when_no_ax(sdata_blobs: SpatialData): + """When no ax is provided and dpi is not set, rcParams default should be used.""" + ax = sdata_blobs.pl.render_images(element="blobs_image").pl.show(return_ax=True, show=False) + fig = ax.get_figure() + assert fig.get_dpi() == matplotlib.rcParams["figure.dpi"] + plt.close(fig)