Skip to main content
`Ctrl`+`K`

# Source code for silx.gui.plot3d.actions.io

    # /*##########################################################################
    #
    # Copyright (c) 2016-2022 European Synchrotron Radiation Facility
    #
    # Permission is hereby granted, free of charge, to any person obtaining a copy
    # of this software and associated documentation files (the "Software"), to deal
    # in the Software without restriction, including without limitation the rights
    # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
    # copies of the Software, and to permit persons to whom the Software is
    # furnished to do so, subject to the following conditions:
    #
    # The above copyright notice and this permission notice shall be included in
    # all copies or substantial portions of the Software.
    #
    # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
    # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
    # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
    # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
    # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
    # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
    # THE SOFTWARE.
    #
    # ###########################################################################*/
    """This module provides Plot3DAction related to input/output.

    It provides QAction to copy, save (snapshot and video), print a Plot3DWidget.
    """

    __authors__ = ["T. Vincent"]
    __license__ = "MIT"
    __date__ = "06/09/2017"

    import logging
    import os

    import numpy

    from silx.gui import qt, printer
    from silx.gui.icons import getQIcon
    from .Plot3DAction import Plot3DAction
    from ..utils import mng
    from ...utils.image import convertQImageToArray

    _logger = logging.getLogger(__name__)

    class CopyAction(Plot3DAction):
        """QAction to provide copy of a Plot3DWidget

        :param parent: See :class:`QAction`
        :param ~silx.gui.plot3d.Plot3DWidget.Plot3DWidget plot3d:
            Plot3DWidget the action is associated with
        """

        def __init__(self, parent, plot3d=None):
            super(CopyAction, self).__init__(parent, plot3d)

            self.setIcon(getQIcon("edit-copy"))
            self.setText("Copy")
            self.setToolTip("Copy a snapshot of the 3D scene to the clipboard")
            self.setCheckable(False)
            self.setShortcut(qt.QKeySequence.Copy)
            self.setShortcutContext(qt.Qt.WidgetShortcut)
            self.triggered[bool].connect(self._triggered)

        def _triggered(self, checked=False):
            plot3d = self.getPlot3DWidget()
            if plot3d is None:
                _logger.error("Cannot copy widget, no associated Plot3DWidget")
            else:
                image = plot3d.grabGL()
                qt.QApplication.clipboard().setImage(image)

    class SaveAction(Plot3DAction):
        """QAction to provide save snapshot of a Plot3DWidget

        :param parent: See :class:`QAction`
        :param ~silx.gui.plot3d.Plot3DWidget.Plot3DWidget plot3d:
            Plot3DWidget the action is associated with
        """

        def __init__(self, parent, plot3d=None):
            super(SaveAction, self).__init__(parent, plot3d)

            self.setIcon(getQIcon("document-save"))
            self.setText("Save...")
            self.setToolTip("Save a snapshot of the 3D scene")
            self.setCheckable(False)
            self.setShortcut(qt.QKeySequence.Save)
            self.setShortcutContext(qt.Qt.WidgetShortcut)
            self.triggered[bool].connect(self._triggered)

        def _triggered(self, checked=False):
            plot3d = self.getPlot3DWidget()
            if plot3d is None:
                _logger.error("Cannot save widget, no associated Plot3DWidget")
            else:
                dialog = qt.QFileDialog(self.parent())
                dialog.setWindowTitle("Save snapshot as")
                dialog.setModal(True)
                dialog.setNameFilters(
                    ("Plot3D Snapshot PNG (*.png)", "Plot3D Snapshot JPEG (*.jpg)")
                )

                dialog.setFileMode(qt.QFileDialog.AnyFile)
                dialog.setAcceptMode(qt.QFileDialog.AcceptSave)

                if not dialog.exec():
                    return

                nameFilter = dialog.selectedNameFilter()
                filename = dialog.selectedFiles()[0]
                dialog.close()

                # Forces the filename extension to match the chosen filter
                extension = nameFilter.split()[-1][2:-1]
                if (
                    len(filename) <= len(extension)
                    or filename[-len(extension) :].lower() != extension.lower()
                ):
                    filename += extension

                image = plot3d.grabGL()
                if not image.save(filename):
                    _logger.error("Failed to save image as %s", filename)
                    qt.QMessageBox.critical(
                        self.parent(), "Save snapshot as", "Failed to save snapshot"
                    )

    class PrintAction(Plot3DAction):
        """QAction to provide printing of a Plot3DWidget

        :param parent: See :class:`QAction`
        :param ~silx.gui.plot3d.Plot3DWidget.Plot3DWidget plot3d:
            Plot3DWidget the action is associated with
        """

        def __init__(self, parent, plot3d=None):
            super(PrintAction, self).__init__(parent, plot3d)

            self.setIcon(getQIcon("document-print"))
            self.setText("Print...")
            self.setToolTip("Print a snapshot of the 3D scene")
            self.setCheckable(False)
            self.setShortcut(qt.QKeySequence.Print)
            self.setShortcutContext(qt.Qt.WidgetShortcut)
            self.triggered[bool].connect(self._triggered)

        def getPrinter(self):
            """Return the QPrinter instance used for printing.

            :rtype: QPrinter
            """
            return printer.getDefaultPrinter()

        def _triggered(self, checked=False):
            plot3d = self.getPlot3DWidget()
            if plot3d is None:
                _logger.error("Cannot print widget, no associated Plot3DWidget")
            else:
                printer = self.getPrinter()
                dialog = qt.QPrintDialog(printer, plot3d)
                dialog.setWindowTitle("Print Plot3D snapshot")
                if not dialog.exec():
                    return

                image = plot3d.grabGL()

                # Draw pixmap with painter
                painter = qt.QPainter()
                if not painter.begin(printer):
                    return

                pageRect = printer.pageRect(qt.QPrinter.DevicePixel)
                if pageRect.width() < image.width() or pageRect.height() < image.height():
                    # Downscale to page
                    xScale = pageRect.width() / image.width()
                    yScale = pageRect.height() / image.height()
                    scale = min(xScale, yScale)
                else:
                    scale = 1.0

                rect = qt.QRectF(0, 0, scale * image.width(), scale * image.height())
                painter.drawImage(rect, image)
                painter.end()

    class VideoAction(Plot3DAction):
        """This action triggers the recording of a video of the scene.

        The scene is rotated 360 degrees around a vertical axis.

        :param parent: Action parent see :class:`QAction`.
        :param ~silx.gui.plot3d.Plot3DWidget.Plot3DWidget plot3d:
            Plot3DWidget the action is associated with
        """

        PNG_SERIE_FILTER = "Serie of PNG files (*.png)"
        MNG_FILTER = "Multiple-image Network Graphics file (*.mng)"

        def __init__(self, parent, plot3d=None):
            super(VideoAction, self).__init__(parent, plot3d)
            self.setText("Record video..")
            self.setIcon(getQIcon("camera"))
            self.setToolTip("Record a video of a 360 degrees rotation of the 3D scene.")
            self.setCheckable(False)
            self.triggered[bool].connect(self._triggered)

        def _triggered(self, checked=False):
            """Action triggered callback"""
            plot3d = self.getPlot3DWidget()
            if plot3d is None:
                _logger.warning("Ignoring action triggered without Plot3DWidget set")
                return

            dialog = qt.QFileDialog(parent=plot3d)
            dialog.setWindowTitle("Save video as...")
            dialog.setModal(True)
            dialog.setNameFilters([self.PNG_SERIE_FILTER, self.MNG_FILTER])
            dialog.setFileMode(qt.QFileDialog.AnyFile)
            dialog.setAcceptMode(qt.QFileDialog.AcceptSave)

            if not dialog.exec():
                return

            nameFilter = dialog.selectedNameFilter()
            filename = dialog.selectedFiles()[0]

            # Forces the filename extension to match the chosen filter
            extension = nameFilter.split()[-1][2:-1]
            if (
                len(filename) <= len(extension)
                or filename[-len(extension) :].lower() != extension.lower()
            ):
                filename += extension

            nbFrames = int(4.0 * 25)  # 4 seconds, 25 fps

            if nameFilter == self.PNG_SERIE_FILTER:
                self._saveAsPNGSerie(filename, nbFrames)
            elif nameFilter == self.MNG_FILTER:
                self._saveAsMNG(filename, nbFrames)
            else:
                _logger.error("Unsupported file filter: %s", nameFilter)

        def _saveAsPNGSerie(self, filename, nbFrames):
            """Save video as serie of PNG files.

            It adds a counter to the provided filename before the extension.

            :param str filename: filename to use as template
            :param int nbFrames: Number of frames to generate
            """
            plot3d = self.getPlot3DWidget()
            assert plot3d is not None

            # Define filename template
            nbDigits = int(numpy.log10(nbFrames)) + 1
            indexFormat = "%%0%dd" % nbDigits
            extensionIndex = filename.rfind(".")
            filenameFormat = (
                filename[:extensionIndex] + indexFormat + filename[extensionIndex:]
            )

            try:
                for index, image in enumerate(self._video360(nbFrames)):
                    image.save(filenameFormat % index)
            except GeneratorExit:
                pass

        def _saveAsMNG(self, filename, nbFrames):
            """Save video as MNG file.

            :param str filename: filename to use
            :param int nbFrames: Number of frames to generate
            """
            plot3d = self.getPlot3DWidget()
            assert plot3d is not None

            frames = (convertQImageToArray(im) for im in self._video360(nbFrames))
            try:
                with open(filename, "wb") as file_:
                    for chunk in mng.convert(frames, nb_images=nbFrames):
                        file_.write(chunk)
            except GeneratorExit:
                os.remove(filename)  # Saving aborted, delete file

        def _video360(self, nbFrames):
            """Run the video and provides the images

            :param int nbFrames: The number of frames to generate for
            :return: Iterator of QImage of the video sequence
            """
            plot3d = self.getPlot3DWidget()
            assert plot3d is not None

            angleStep = 360.0 / nbFrames

            # Create progress bar dialog
            dialog = qt.QDialog(plot3d)
            dialog.setWindowTitle("Record Video")
            layout = qt.QVBoxLayout(dialog)
            progress = qt.QProgressBar()
            progress.setRange(0, nbFrames)
            layout.addWidget(progress)

            btnBox = qt.QDialogButtonBox(qt.QDialogButtonBox.Abort)
            btnBox.rejected.connect(dialog.reject)
            layout.addWidget(btnBox)

            dialog.setModal(True)
            dialog.show()

            qapp = qt.QApplication.instance()

            for frame in range(nbFrames):
                progress.setValue(frame)
                image = plot3d.grabGL()
                yield image
                plot3d.viewport.orbitCamera("left", angleStep)
                qapp.processEvents()
                if not dialog.isVisible():
                    break  # It as been rejected by the abort button
            else:
                dialog.accept()

            if dialog.result() == qt.QDialog.Rejected:
                raise GeneratorExit("Aborted")