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

# coding: utf-8
# /*##########################################################################
#
# Copyright (c) 2016-2021 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.
"""

from __future__ import absolute_import, division

__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__)


[docs]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)
[docs]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')
[docs]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)
[docs] 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. rect = qt.QRectF(0, 0, scale * image.width(), scale * image.height()) painter.drawImage(rect, image) painter.end()
[docs]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(dialog.AnyFile) dialog.setAcceptMode(dialog.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. * 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. / 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')