# coding: utf-8 # /*########################################################################## # # Copyright (c) 2016-2017 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 from silx.gui.plot.actions.io import PrintAction as _PrintAction from silx.gui.icons import getQIcon from .Plot3DAction import Plot3DAction from ..utils import mng from ..._utils import convertQImageToArray _logger = logging.getLogger(__name__) class CopyAction(Plot3DAction): """QAction to provide copy of a Plot3DWidget :param parent: See :class:`QAction` :param 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 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 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: qt.QPrinter """ # TODO This is a hack to sync with silx plot PrintAction # This needs to be centralized if _PrintAction._printer is None: _PrintAction._printer = qt.QPrinter() return _PrintAction._printer 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 if (printer.pageRect().width() < image.width() or printer.pageRect().height() < image.height()): # Downscale to page xScale = printer.pageRect().width() / image.width() yScale = printer.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() 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`. """ 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')