Source code for plotpy.widgets.rotatecrop

# -*- coding: utf-8 -*-
#
# Licensed under the terms of the BSD 3-Clause
# (see plotpy/LICENSE for details)

"""
Rotate/crop dialog
------------------

The `rotatecrop` module provides a dialog box providing essential GUI elements
for rotating (arbitrary angle) and cropping an image:

    * :py:class:`.widgets.rotatecrop.RotateCropDialog`: dialog box
    * :py:class:`.widgets.rotatecrop.RotateCropWidget`: equivalent widget

Reference
~~~~~~~~~

.. autoclass:: RotateCropDialog
   :members:
.. autoclass:: RotateCropWidget
   :members:
"""

from __future__ import annotations

from typing import TYPE_CHECKING, Any

from guidata.qthelpers import win32_fix_title_bar_background
from qtpy import QtCore as QC
from qtpy import QtWidgets as QW
from qtpy.QtWidgets import QWidget  # Helping out python_qt_documentation

from plotpy.builder import make
from plotpy.config import _
from plotpy.items import get_image_in_shape
from plotpy.widgets import basetransform

if TYPE_CHECKING:
    import numpy as np

    from plotpy.items import AnnotatedRectangle, TrImageItem
    from plotpy.plot import PlotOptions
    from plotpy.plot.manager import PlotManager


class RotateCropTransform(basetransform.BaseTransform):
    """Rotate & Crop utils class, to be mixed with a class providing the
    get_plot method, like PlotDialog or RotateCropWidget (see below)

    Args:
        parent: Parent widget
        manager: Plot manager
    """

    def __init__(self, parent: RotateCropWidget, manager: PlotManager) -> None:
        super().__init__(parent, manager)
        self.crop_rect: AnnotatedRectangle = None
        self.manager = manager

    # ------BaseTransformUtils API----------------------------------------------

    def set_item(self, item: TrImageItem) -> None:
        """Set associated item

        Args:
            item: Associated item
        """
        super().set_item(item)
        crect: AnnotatedRectangle = make.annotated_rectangle(
            0, 0, 1, 1, _("Cropping rectangle")
        )
        self.crop_rect = crect
        crect.annotationparam.format = "%.1f cm"
        plot = self.manager.get_plot()
        plot.add_item(crect)
        plot.set_active_item(crect)
        x0, y0, x1, y1 = self.item.get_crop_coordinates()
        crect.set_rect(x0, y0, x1, y1)
        plot.replot()

    def reset_transformation(self) -> None:
        """Reset transformation"""
        x0, y0, x1, y1 = self.item.border_rect.get_rect()
        self.crop_rect.set_rect(x0, y0, x1, y1)

    def apply_transformation(self) -> None:
        """Apply transformation, e.g. crop or rotate"""
        # Let's crop!
        i_points = self.item.border_rect.get_points()
        xmin, ymin = i_points.min(axis=0)
        xmax, ymax = i_points.max(axis=0)
        xc0, yc0, xc1, yc1 = self.crop_rect.shape.get_rect()
        left = max([0, xc0 - xmin])
        right = max([0, xmax - xc1])
        top = max([0, yc0 - ymin])
        bottom = max([0, ymax - yc1])
        self.item.set_crop(left, top, right, bottom)
        #        print "set_crop:", left, top, right, bottom
        self.item.compute_bounds()
        self.manager.get_plot().replot()

    def compute_transformation(self) -> np.ndarray:
        """Compute transformation, return compute output array

        Returns:
            Compute output array
        """
        return get_image_in_shape(self.crop_rect, apply_interpolation=False)

    # ------Private API---------------------------------------------------------
    def show_crop_rect(self, state: bool) -> None:
        """Show/hide cropping rectangle shape

        Args:
            state: Show/hide state
        """
        self.crop_rect.setVisible(state)
        self.crop_rect.label.setVisible(state)
        self.manager.get_plot().replot()


[docs] class RotateCropDialog(QW.QDialog): """Rotate & Crop Dialog Rotate and crop a :py:class:`.TrImageItem` plot item Args: parent: Parent widget title: Dialog title options: Options dict resize_to: Resize dialog to (width, height) edit: Show "Edit" button toolbar: Show toolbar """ def __init__( self, parent: QWidget, title: str | None = None, options: PlotOptions | dict[str, Any] | None = None, resize_to: tuple[int, int] | None = None, edit: bool = True, toolbar: bool = False, ) -> None: super().__init__(parent) win32_fix_title_bar_background(self) if resize_to is not None: width, height = resize_to self.resize(width, height) self.button_box = None if title is None: title = _("Rotate & Crop") self.setWindowTitle(title) self.widget = RotateCropWidget(parent=parent, options=options, toolbar=toolbar) self.setWindowFlags(QC.Qt.WindowType.Window) buttonhlayout = QW.QHBoxLayout() self.add_buttons_to_layout(buttonhlayout, edit) dialogvlayout = QW.QVBoxLayout() dialogvlayout.addWidget(self.widget.plot_widget.toolbar) dialogvlayout.addWidget(self.widget) dialogvlayout.addLayout(buttonhlayout) self.setLayout(dialogvlayout) self.transform = self.widget.transform self.plot_widget = self.widget.plot_widget self.manager = self.widget.plot_widget.manager self.accepted.connect(self.transform.accept_changes) self.rejected.connect(self.transform.reject_changes)
[docs] def add_buttons_to_layout(self, layout: QW.QBoxLayout, edit: bool) -> None: """Add buttons to layout Args: layout: Layout """ if edit: self.button_box = bbox = QW.QDialogButtonBox( QW.QDialogButtonBox.Ok | QW.QDialogButtonBox.Cancel ) bbox.accepted.connect(self.accept) bbox.rejected.connect(self.reject) layout.addWidget(bbox)
[docs] class RotateCropWidget(basetransform.BaseTransformWidget): """Rotate & Crop Widget Rotate and crop a :py:class:`.TrImageItem` plot item Args: parent: Parent widget toolbar: Show toolbar options: Plot options """ def __init__( self, parent: QWidget, toolbar: bool = False, options: PlotOptions | dict[str, Any] | None = None, ) -> None: super().__init__(parent, toolbar=toolbar, options=options) self.transform = RotateCropTransform(self, self.plot_widget.manager) self.manager = self.plot_widget.manager
[docs] def add_buttons_to_layout(self, layout: QW.QBoxLayout) -> None: """Add tool buttons to layout Args: layout: Layout """ # Show crop rectangle checkbox show_crop = QW.QCheckBox(_("Show cropping rectangle"), self.plot_widget) show_crop.setChecked(True) show_crop.toggled.connect(self.show_crop_rect) layout.addWidget(show_crop) layout.addSpacing(15) basetransform.BaseTransformWidget.add_buttons_to_layout(self, layout)
[docs] def apply_transformation(self) -> None: """Apply transformation""" self.transform.apply_transformation()
[docs] def reset(self) -> None: """Reset transformation""" self.transform.reset()
[docs] def reject_changes(self) -> None: """Reject changes""" self.transform.reject_changes()
[docs] def show_crop_rect(self, state: bool) -> None: """Show/hide cropping rectangle shape Args: state: Show/hide state """ self.transform.show_crop_rect(state)
class MultipleRotateCropWidget(basetransform.BaseMultipleTransformWidget): """Multiple Rotate & Crop Widget Rotate and crop several :py:class:`.TrImageItem` plot items""" TRANSFORM_WIDGET_CLASS = RotateCropWidget