|
| 1 | +# -*- coding: utf-8 -*- |
| 2 | +# |
| 3 | +# Licensed under the terms of the BSD 3-Clause |
| 4 | +# (see plotpy/LICENSE for details) |
| 5 | + |
| 6 | +"""Unit tests for the rectangular snapshot tool's "Original size" computation. |
| 7 | +
|
| 8 | +This test reproduces the issue reported in PlotPyStack/PlotPy#57: |
| 9 | +
|
| 10 | + The "Original size" option in the rectangular snapshot tool does not behave |
| 11 | + correctly under certain conditions: |
| 12 | +
|
| 13 | + * When the Y axis is not reversed (image-style plot) |
| 14 | + * When the X axis is reversed |
| 15 | + * When using an XYImageItem |
| 16 | +
|
| 17 | + In these cases, the "Original size" preview may display negative values or |
| 18 | + incorrect dimensions. The computation appears to rely on axis scaling |
| 19 | + rather than pixel coordinates, particularly for ``XYImageItem``. |
| 20 | +
|
| 21 | + Additionally, a ``ValueError`` is raised when either axis leads to negative |
| 22 | + values. |
| 23 | +""" |
| 24 | + |
| 25 | +from __future__ import annotations |
| 26 | + |
| 27 | +import numpy as np |
| 28 | +import pytest |
| 29 | +from guidata.qthelpers import qt_app_context |
| 30 | +from qtpy import QtCore as QC |
| 31 | + |
| 32 | +from plotpy.builder import make |
| 33 | +from plotpy.items import ( |
| 34 | + compute_image_items_original_size, |
| 35 | + compute_trimageitems_original_size, |
| 36 | +) |
| 37 | +from plotpy.tests import vistools as ptv |
| 38 | + |
| 39 | +# Image used by the tests (rows = Y, cols = X) |
| 40 | +NB_ROWS, NB_COLS = 100, 200 |
| 41 | + |
| 42 | + |
| 43 | +def _canvas_points(plot, x0_plot, y0_plot, x1_plot, y1_plot): |
| 44 | + """Convert plot-coordinate corners into canvas QPointF, the way the |
| 45 | + snapshot tool builds them from a rubber-band rectangle.""" |
| 46 | + from plotpy.constants import X_BOTTOM, Y_LEFT |
| 47 | + |
| 48 | + x0c = plot.transform(X_BOTTOM, x0_plot) |
| 49 | + x1c = plot.transform(X_BOTTOM, x1_plot) |
| 50 | + y0c = plot.transform(Y_LEFT, y0_plot) |
| 51 | + y1c = plot.transform(Y_LEFT, y1_plot) |
| 52 | + # Mimic the tool: p0 is top-left, p1 is bottom-right (in canvas pixels) |
| 53 | + p0 = QC.QPointF(min(x0c, x1c), min(y0c, y1c)) |
| 54 | + p1 = QC.QPointF(max(x0c, x1c), max(y0c, y1c)) |
| 55 | + return p0, p1 |
| 56 | + |
| 57 | + |
| 58 | +def test_compute_trimageitems_original_size_handles_reversed_axes(): |
| 59 | + """Regression: ``compute_trimageitems_original_size`` must return positive |
| 60 | + dimensions even when the source rectangle is given with negative width or |
| 61 | + height (which happens on reversed axes).""" |
| 62 | + # No items: legacy fallback path |
| 63 | + w, h = compute_trimageitems_original_size([], -50.0, -25.0) |
| 64 | + assert w == 50.0 and h == 25.0 |
| 65 | + |
| 66 | + |
| 67 | +def _expected_pixel_size(x0, y0, x1, y1): |
| 68 | + """Original (pixel) size for a selection on a non-transformed image |
| 69 | + spanning [0, NB_COLS] x [0, NB_ROWS] in plot units.""" |
| 70 | + return abs(x1 - x0), abs(y1 - y0) |
| 71 | + |
| 72 | + |
| 73 | +@pytest.mark.parametrize( |
| 74 | + "xreverse,yreverse", |
| 75 | + [(False, False), (False, True), (True, False), (True, True)], |
| 76 | +) |
| 77 | +def test_snapshot_original_size_with_image_item(xreverse, yreverse): |
| 78 | + """Original size must be positive and equal to the pixel selection size, |
| 79 | + regardless of axis orientation, for a regular ``ImageItem``.""" |
| 80 | + data = np.arange(NB_ROWS * NB_COLS, dtype=np.float64).reshape(NB_ROWS, NB_COLS) |
| 81 | + with qt_app_context(exec_loop=False): |
| 82 | + image = make.image(data) |
| 83 | + win = ptv.show_items([image], plot_type="image", auto_tools=False) |
| 84 | + plot = win.manager.get_plot() |
| 85 | + plot.set_axis_direction("bottom", xreverse) |
| 86 | + plot.set_axis_direction("left", yreverse) |
| 87 | + plot.replot() |
| 88 | + |
| 89 | + # Selection in plot coordinates: a 40x30 pixel rectangle |
| 90 | + x0, y0, x1, y1 = 30.0, 20.0, 70.0, 50.0 |
| 91 | + p0, p1 = _canvas_points(plot, x0, y0, x1, y1) |
| 92 | + |
| 93 | + width, height = compute_image_items_original_size([image], plot, p0, p1) |
| 94 | + |
| 95 | + exp_w, exp_h = _expected_pixel_size(x0, y0, x1, y1) |
| 96 | + # Allow 1 pixel tolerance for canvas rounding |
| 97 | + assert width > 0 and height > 0 |
| 98 | + assert abs(width - exp_w) <= 1.5 |
| 99 | + assert abs(height - exp_h) <= 1.5 |
| 100 | + win.close() |
| 101 | + |
| 102 | + |
| 103 | +def test_snapshot_original_size_with_xy_image_item(): |
| 104 | + """For an ``XYImageItem``, the original size must be expressed in **pixel** |
| 105 | + coordinates (independent of axis scaling), not in axis units.""" |
| 106 | + data = np.arange(NB_ROWS * NB_COLS, dtype=np.float64).reshape(NB_ROWS, NB_COLS) |
| 107 | + # Non-trivial axis scaling: 1 pixel == 5 axis units (X), 2 axis units (Y) |
| 108 | + x = np.linspace(0.0, NB_COLS * 5.0, NB_COLS + 1) |
| 109 | + y = np.linspace(0.0, NB_ROWS * 2.0, NB_ROWS + 1) |
| 110 | + with qt_app_context(exec_loop=False): |
| 111 | + image = make.xyimage(x, y, data) |
| 112 | + win = ptv.show_items([image], plot_type="image", auto_tools=False) |
| 113 | + plot = win.manager.get_plot() |
| 114 | + plot.replot() |
| 115 | + |
| 116 | + # Selection spanning ~40 columns and ~30 rows in pixel space: |
| 117 | + x0, x1 = 30.0 * 5.0, 70.0 * 5.0 # 40 columns |
| 118 | + y0, y1 = 20.0 * 2.0, 50.0 * 2.0 # 30 rows |
| 119 | + p0, p1 = _canvas_points(plot, x0, y0, x1, y1) |
| 120 | + |
| 121 | + width, height = compute_image_items_original_size([image], plot, p0, p1) |
| 122 | + |
| 123 | + # Must be in pixel units, not axis units (axis units would give |
| 124 | + # ~200 x ~60 instead of ~40 x ~30) |
| 125 | + assert width > 0 and height > 0 |
| 126 | + assert abs(width - 40) <= 5 |
| 127 | + assert abs(height - 30) <= 5 |
| 128 | + win.close() |
0 commit comments