diff options
Diffstat (limited to 'src/silx/gui/plot/actions/io.py')
-rw-r--r-- | src/silx/gui/plot/actions/io.py | 874 |
1 files changed, 874 insertions, 0 deletions
diff --git a/src/silx/gui/plot/actions/io.py b/src/silx/gui/plot/actions/io.py new file mode 100644 index 0000000..1ff95f3 --- /dev/null +++ b/src/silx/gui/plot/actions/io.py @@ -0,0 +1,874 @@ +# /*########################################################################## +# +# Copyright (c) 2004-2023 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. +# +# ###########################################################################*/ +""" +:mod:`silx.gui.plot.actions.io` provides a set of QAction relative of inputs +and outputs for a :class:`.PlotWidget`. + +The following QAction are available: + +- :class:`CopyAction` +- :class:`PrintAction` +- :class:`SaveAction` +""" + +__authors__ = ["V.A. Sole", "T. Vincent", "P. Knobel"] +__license__ = "MIT" +__date__ = "25/09/2020" + +from io import BytesIO +import logging +import sys +import os.path +import traceback +import numpy +from fabio.TiffIO import TiffIO +from fabio.edfimage import EdfImage + +from silx.gui import qt, printer +from silx.gui.dialog.GroupDialog import GroupDialog +from silx.io.utils import save1D, savespec, NEXUS_HDF5_EXT +from silx.io.nxdata import save_NXdata + +from . import PlotAction +from ...utils.image import convertArrayToQImage + + +_logger = logging.getLogger(__name__) + +_NEXUS_HDF5_EXT_STR = " ".join(["*" + ext for ext in NEXUS_HDF5_EXT]) + + +def selectOutputGroup(h5filename): + """Open a dialog to prompt the user to select a group in + which to output data. + + :param str h5filename: name of an existing HDF5 file + :rtype: str + :return: Name of output group, or None if the dialog was cancelled + """ + dialog = GroupDialog() + dialog.addFile(h5filename) + dialog.setWindowTitle("Select an output group") + if not dialog.exec(): + return None + return dialog.getSelectedDataUrl().data_path() + + +class SaveAction(PlotAction): + """QAction for saving Plot content. + + It opens a Save as... dialog. + + :param plot: :class:`.PlotWidget` instance on which to operate. + :param parent: See :class:`QAction`. + """ + + SNAPSHOT_FILTER_SVG = "Plot Snapshot as SVG (*.svg)" + SNAPSHOT_FILTER_PNG = "Plot Snapshot as PNG (*.png)" + + DEFAULT_ALL_FILTERS = (SNAPSHOT_FILTER_PNG, SNAPSHOT_FILTER_SVG) + + # Dict of curve filters with CSV-like format + # Using ordered dict to guarantee filters order + # Note: '%.18e' is numpy.savetxt default format + CURVE_FILTERS_TXT = dict( + ( + ( + "Curve as Raw ASCII (*.txt)", + {"fmt": "%.18e", "delimiter": " ", "header": False}, + ), + ( + 'Curve as ";"-separated CSV (*.csv)', + {"fmt": "%.18e", "delimiter": ";", "header": True}, + ), + ( + 'Curve as ","-separated CSV (*.csv)', + {"fmt": "%.18e", "delimiter": ",", "header": True}, + ), + ( + "Curve as tab-separated CSV (*.csv)", + {"fmt": "%.18e", "delimiter": "\t", "header": True}, + ), + ( + "Curve as OMNIC CSV (*.csv)", + {"fmt": "%.7E", "delimiter": ",", "header": False}, + ), + ( + "Curve as SpecFile (*.dat)", + {"fmt": "%.10g", "delimiter": "", "header": False}, + ), + ) + ) + + CURVE_FILTER_NPY = "Curve as NumPy binary file (*.npy)" + + CURVE_FILTER_NXDATA = "Curve as NXdata (%s)" % _NEXUS_HDF5_EXT_STR + + DEFAULT_CURVE_FILTERS = list(CURVE_FILTERS_TXT.keys()) + [ + CURVE_FILTER_NPY, + CURVE_FILTER_NXDATA, + ] + + DEFAULT_ALL_CURVES_FILTERS = ("All curves as SpecFile (*.dat)",) + + IMAGE_FILTER_EDF = "Image data as EDF (*.edf)" + IMAGE_FILTER_TIFF = "Image data as TIFF (*.tif)" + IMAGE_FILTER_NUMPY = "Image data as NumPy binary file (*.npy)" + IMAGE_FILTER_ASCII = "Image data as ASCII (*.dat)" + IMAGE_FILTER_CSV_COMMA = "Image data as ,-separated CSV (*.csv)" + IMAGE_FILTER_CSV_SEMICOLON = "Image data as ;-separated CSV (*.csv)" + IMAGE_FILTER_CSV_TAB = "Image data as tab-separated CSV (*.csv)" + IMAGE_FILTER_RGB_PNG = "Image as PNG (*.png)" + IMAGE_FILTER_NXDATA = "Image as NXdata (%s)" % _NEXUS_HDF5_EXT_STR + + DEFAULT_IMAGE_FILTERS = ( + IMAGE_FILTER_EDF, + IMAGE_FILTER_TIFF, + IMAGE_FILTER_NUMPY, + IMAGE_FILTER_ASCII, + IMAGE_FILTER_CSV_COMMA, + IMAGE_FILTER_CSV_SEMICOLON, + IMAGE_FILTER_CSV_TAB, + IMAGE_FILTER_RGB_PNG, + IMAGE_FILTER_NXDATA, + ) + + SCATTER_FILTER_NXDATA = "Scatter as NXdata (%s)" % _NEXUS_HDF5_EXT_STR + DEFAULT_SCATTER_FILTERS = (SCATTER_FILTER_NXDATA,) + + # filters for which we don't want an "overwrite existing file" warning + DEFAULT_APPEND_FILTERS = ( + CURVE_FILTER_NXDATA, + IMAGE_FILTER_NXDATA, + SCATTER_FILTER_NXDATA, + ) + + def __init__(self, plot, parent=None): + self._filters = { + "all": {}, + "curve": {}, + "curves": {}, + "image": {}, + "scatter": {}, + } + + self._appendFilters = list(self.DEFAULT_APPEND_FILTERS) + + # Initialize filters + for nameFilter in self.DEFAULT_ALL_FILTERS: + self.setFileFilter( + dataKind="all", nameFilter=nameFilter, func=self._saveSnapshot + ) + + for nameFilter in self.DEFAULT_CURVE_FILTERS: + self.setFileFilter( + dataKind="curve", nameFilter=nameFilter, func=self._saveCurve + ) + + for nameFilter in self.DEFAULT_ALL_CURVES_FILTERS: + self.setFileFilter( + dataKind="curves", nameFilter=nameFilter, func=self._saveCurves + ) + + for nameFilter in self.DEFAULT_IMAGE_FILTERS: + self.setFileFilter( + dataKind="image", nameFilter=nameFilter, func=self._saveImage + ) + + for nameFilter in self.DEFAULT_SCATTER_FILTERS: + self.setFileFilter( + dataKind="scatter", nameFilter=nameFilter, func=self._saveScatter + ) + + super(SaveAction, self).__init__( + plot, + icon="document-save", + text="Save as...", + tooltip="Save curve/image/plot snapshot dialog", + triggered=self._actionTriggered, + checkable=False, + parent=parent, + ) + self.setShortcut(qt.QKeySequence.Save) + self.setShortcutContext(qt.Qt.WidgetShortcut) + + @staticmethod + def _errorMessage(informativeText="", parent=None): + """Display an error message.""" + # TODO issue with QMessageBox size fixed and too small + msg = qt.QMessageBox(parent) + msg.setIcon(qt.QMessageBox.Critical) + msg.setInformativeText(informativeText + " " + str(sys.exc_info()[1])) + msg.setDetailedText(traceback.format_exc()) + msg.exec() + + def _saveSnapshot(self, plot, filename, nameFilter): + """Save a snapshot of the :class:`PlotWindow` widget. + + :param str filename: The name of the file to write + :param str nameFilter: The selected name filter + :return: False if format is not supported or save failed, + True otherwise. + """ + if nameFilter == self.SNAPSHOT_FILTER_PNG: + fileFormat = "png" + elif nameFilter == self.SNAPSHOT_FILTER_SVG: + fileFormat = "svg" + else: # Format not supported + _logger.error("Saving plot snapshot failed: format not supported") + return False + + plot.saveGraph(filename, fileFormat=fileFormat) + return True + + def _getAxesLabels(self, item): + # If curve has no associated label, get the default from the plot + xlabel = item.getXLabel() or self.plot.getXAxis().getLabel() + ylabel = item.getYLabel() or self.plot.getYAxis().getLabel() + return xlabel, ylabel + + def _get1dData(self, item): + "provide xdata, [ydata], xlabel, [ylabel] and manages error bars" + xlabel, ylabel = self._getAxesLabels(item) + x_data = item.getXData(copy=False) + y_data = item.getYData(copy=False) + x_err = item.getXErrorData(copy=False) + y_err = item.getYErrorData(copy=False) + labels = [ylabel] + data = [y_data] + + if x_err is not None: + if numpy.isscalar(x_err): + data.append(numpy.zeros_like(y_data) + x_err) + labels.append(xlabel + "_errors") + elif x_err.ndim == 1: + data.append(x_err) + labels.append(xlabel + "_errors") + elif x_err.ndim == 2: + data.append(x_err[0]) + labels.append(xlabel + "_errors_below") + data.append(x_err[1]) + labels.append(xlabel + "_errors_above") + + if y_err is not None: + if numpy.isscalar(y_err): + data.append(numpy.zeros_like(y_data) + y_err) + labels.append(ylabel + "_errors") + elif y_err.ndim == 1: + data.append(y_err) + labels.append(ylabel + "_errors") + elif y_err.ndim == 2: + data.append(y_err[0]) + labels.append(ylabel + "_errors_below") + data.append(y_err[1]) + labels.append(ylabel + "_errors_above") + return x_data, data, xlabel, labels + + @staticmethod + def _selectWriteableOutputGroup(filename, parent): + if ( + os.path.exists(filename) + and os.path.isfile(filename) + and os.access(filename, os.W_OK) + ): + entryPath = selectOutputGroup(filename) + if entryPath is None: + _logger.info("Save operation cancelled") + return None + return entryPath + elif not os.path.exists(filename): + # create new entry in new file + return "/entry" + else: + SaveAction._errorMessage("Save failed (file access issue)\n", parent=parent) + return None + + def _saveCurveAsNXdata(self, curve, filename): + entryPath = self._selectWriteableOutputGroup(filename, parent=self.plot) + if entryPath is None: + return False + + xlabel, ylabel = self._getAxesLabels(curve) + + return save_NXdata( + filename, + nxentry_name=entryPath, + signal=curve.getYData(copy=False), + axes=[curve.getXData(copy=False)], + signal_name="y", + axes_names=["x"], + signal_long_name=ylabel, + axes_long_names=[xlabel], + signal_errors=curve.getYErrorData(copy=False), + axes_errors=[curve.getXErrorData(copy=True)], + title=self.plot.getGraphTitle(), + ) + + def _saveCurve(self, plot, filename, nameFilter): + """Save a curve from the plot. + + :param str filename: The name of the file to write + :param str nameFilter: The selected name filter + :return: False if format is not supported or save failed, + True otherwise. + """ + if nameFilter not in self.DEFAULT_CURVE_FILTERS: + return False + + # Check if a curve is to be saved + curve = plot.getActiveCurve() + # before calling _saveCurve, if there is no selected curve, we + # make sure there is only one curve on the graph + if curve is None: + curves = plot.getAllCurves() + if not curves: + self._errorMessage("No curve to be saved", parent=self.plot) + return False + curve = curves[0] + + if nameFilter in self.CURVE_FILTERS_TXT: + filter_ = self.CURVE_FILTERS_TXT[nameFilter] + fmt = filter_["fmt"] + csvdelim = filter_["delimiter"] + autoheader = filter_["header"] + else: + # .npy or nxdata + fmt, csvdelim, autoheader = ("", "", False) + + if nameFilter == self.CURVE_FILTER_NXDATA: + return self._saveCurveAsNXdata(curve, filename) + + xdata, data, xlabel, labels = self._get1dData(curve) + + try: + save1D( + filename, + xdata, + data, + xlabel, + labels, + fmt=fmt, + csvdelim=csvdelim, + autoheader=autoheader, + ) + except IOError: + self._errorMessage("Save failed\n", parent=self.plot) + return False + + return True + + def _saveCurves(self, plot, filename, nameFilter): + """Save all curves from the plot. + + :param str filename: The name of the file to write + :param str nameFilter: The selected name filter + :return: False if format is not supported or save failed, + True otherwise. + """ + if nameFilter not in self.DEFAULT_ALL_CURVES_FILTERS: + return False + + curves = plot.getAllCurves() + if not curves: + self._errorMessage("No curves to be saved", parent=self.plot) + return False + + curve = curves[0] + scanno = 1 + try: + xdata, data, xlabel, labels = self._get1dData(curve) + + specfile = savespec( + filename, + xdata, + data, + xlabel, + labels, + fmt="%.7g", + scan_number=1, + mode="w", + write_file_header=True, + close_file=False, + ) + except IOError: + self._errorMessage("Save failed\n", parent=self.plot) + return False + + for curve in curves[1:]: + try: + scanno += 1 + xdata, data, xlabel, labels = self._get1dData(curve) + specfile = savespec( + specfile, + xdata, + data, + xlabel, + labels, + fmt="%.7g", + scan_number=scanno, + write_file_header=False, + close_file=False, + ) + except IOError: + self._errorMessage("Save failed\n", parent=self.plot) + return False + specfile.close() + + return True + + def _saveImage(self, plot, filename, nameFilter): + """Save an image from the plot. + + :param str filename: The name of the file to write + :param str nameFilter: The selected name filter + :return: False if format is not supported or save failed, + True otherwise. + """ + if nameFilter not in self.DEFAULT_IMAGE_FILTERS: + return False + + image = plot.getActiveImage() + if image is None: + qt.QMessageBox.warning(plot, "No Data", "No image to be saved") + return False + + data = image.getData(copy=False) + + # TODO Use silx.io for writing files + if nameFilter == self.IMAGE_FILTER_EDF: + EdfImage(data=data, header={}).write(filename) + return True + + elif nameFilter == self.IMAGE_FILTER_TIFF: + tiffFile = TiffIO(filename, mode="w") + tiffFile.writeImage(data, software="silx") + return True + + elif nameFilter == self.IMAGE_FILTER_NUMPY: + try: + numpy.save(filename, data) + except IOError: + self._errorMessage("Save failed\n", parent=self.plot) + return False + return True + + elif nameFilter == self.IMAGE_FILTER_NXDATA: + entryPath = self._selectWriteableOutputGroup(filename, parent=self.plot) + if entryPath is None: + return False + xorigin, yorigin = image.getOrigin() + xscale, yscale = image.getScale() + xaxis = xorigin + xscale * numpy.arange(data.shape[1]) + yaxis = yorigin + yscale * numpy.arange(data.shape[0]) + xlabel, ylabel = self._getAxesLabels(image) + interpretation = "image" if len(data.shape) == 2 else "rgba-image" + + return save_NXdata( + filename, + nxentry_name=entryPath, + signal=data, + axes=[yaxis, xaxis], + signal_name="image", + axes_names=["y", "x"], + axes_long_names=[ylabel, xlabel], + title=plot.getGraphTitle(), + interpretation=interpretation, + ) + + elif nameFilter in ( + self.IMAGE_FILTER_ASCII, + self.IMAGE_FILTER_CSV_COMMA, + self.IMAGE_FILTER_CSV_SEMICOLON, + self.IMAGE_FILTER_CSV_TAB, + ): + csvdelim, filetype = { + self.IMAGE_FILTER_ASCII: (" ", "txt"), + self.IMAGE_FILTER_CSV_COMMA: (",", "csv"), + self.IMAGE_FILTER_CSV_SEMICOLON: (";", "csv"), + self.IMAGE_FILTER_CSV_TAB: ("\t", "csv"), + }[nameFilter] + + height, width = data.shape + rows, cols = numpy.mgrid[0:height, 0:width] + try: + save1D( + filename, + rows.ravel(), + (cols.ravel(), data.ravel()), + filetype=filetype, + xlabel="row", + ylabels=["column", "value"], + csvdelim=csvdelim, + autoheader=True, + ) + + except IOError: + self._errorMessage("Save failed\n", parent=self.plot) + return False + return True + + elif nameFilter == self.IMAGE_FILTER_RGB_PNG: + # Get displayed image + rgbaImage = image.getRgbaImageData(copy=False) + # Convert RGB QImage + qimage = convertArrayToQImage(rgbaImage[:, :, :3]) + + if qimage.save(filename, "PNG"): + return True + else: + _logger.error("Failed to save image as %s", filename) + qt.QMessageBox.critical( + self.parent(), "Save image as", "Failed to save image" + ) + + return False + + def _saveScatter(self, plot, filename, nameFilter): + """Save an image from the plot. + + :param str filename: The name of the file to write + :param str nameFilter: The selected name filter + :return: False if format is not supported or save failed, + True otherwise. + """ + if nameFilter not in self.DEFAULT_SCATTER_FILTERS: + return False + + if nameFilter == self.SCATTER_FILTER_NXDATA: + entryPath = self._selectWriteableOutputGroup(filename, parent=self.plot) + if entryPath is None: + return False + scatter = plot.getScatter() + + x = scatter.getXData(copy=False) + y = scatter.getYData(copy=False) + z = scatter.getValueData(copy=False) + + xerror = scatter.getXErrorData(copy=False) + if isinstance(xerror, float): + xerror = xerror * numpy.ones(x.shape, dtype=numpy.float32) + + yerror = scatter.getYErrorData(copy=False) + if isinstance(yerror, float): + yerror = yerror * numpy.ones(x.shape, dtype=numpy.float32) + + xlabel = plot.getGraphXLabel() + ylabel = plot.getGraphYLabel() + + return save_NXdata( + filename, + nxentry_name=entryPath, + signal=z, + axes=[x, y], + signal_name="values", + axes_names=["x", "y"], + axes_long_names=[xlabel, ylabel], + axes_errors=[xerror, yerror], + title=plot.getGraphTitle(), + ) + + def setFileFilter(self, dataKind, nameFilter, func, index=None, appendToFile=False): + """Set a name filter to add/replace a file format support + + :param str dataKind: + The kind of data for which the provided filter is valid. + One of: 'all', 'curve', 'curves', 'image', 'scatter' + :param str nameFilter: The name filter in the QFileDialog. + See :meth:`QFileDialog.setNameFilters`. + :param callable func: The function to call to perform saving. + Expected signature is: + bool func(PlotWidget plot, str filename, str nameFilter) + :param bool appendToFile: True to append the data into the selected + file. + :param integer index: Index of the filter in the final list (or None) + """ + assert dataKind in ("all", "curve", "curves", "image", "scatter") + + if appendToFile: + self._appendFilters.append(nameFilter) + + # first append or replace the new filter to prevent colissions + self._filters[dataKind][nameFilter] = func + if index is None: + # we are already done + return + + # get the current ordered list of keys + keyList = list(self._filters[dataKind].keys()) + + # deal with negative indices + if index < 0: + index = len(keyList) + index + if index < 0: + index = 0 + + if index >= len(keyList): + # nothing to be done, already at the end + txt = "Requested index %d impossible, already at the end" % index + _logger.info(txt) + return + + # get the new ordered list + oldIndex = keyList.index(nameFilter) + del keyList[oldIndex] + keyList.insert(index, nameFilter) + + # build the new filters + newFilters = {} + for key in keyList: + newFilters[key] = self._filters[dataKind][key] + + # and update the filters + self._filters[dataKind] = newFilters + return + + def getFileFilters(self, dataKind): + """Returns the nameFilter and associated function for a kind of data. + + :param str dataKind: + The kind of data for which the provided filter is valid. + On of: 'all', 'curve', 'curves', 'image', 'scatter' + :return: {nameFilter: function} associations. + :rtype: dict + """ + assert dataKind in ("all", "curve", "curves", "image", "scatter") + + return self._filters[dataKind].copy() + + def _actionTriggered(self, checked=False): + """Handle save action.""" + # Set-up filters + filters = {} + + # Add image filters if there is an active image + if self.plot.getActiveImage() is not None: + filters.update(self._filters["image"].items()) + + # Add curve filters if there is a curve to save + if self.plot.getActiveCurve() is not None or len(self.plot.getAllCurves()) == 1: + filters.update(self._filters["curve"].items()) + if len(self.plot.getAllCurves()) >= 1: + filters.update(self._filters["curves"].items()) + + # Add scatter filters if there is a scatter + # todo: CSV + if self.plot.getScatter() is not None: + filters.update(self._filters["scatter"].items()) + + filters.update(self._filters["all"].items()) + + # Create and run File dialog + dialog = qt.QFileDialog(self.plot) + dialog.setOption(qt.QFileDialog.DontUseNativeDialog) + dialog.setWindowTitle("Output File Selection") + dialog.setModal(1) + dialog.setNameFilters(list(filters.keys())) + + dialog.setFileMode(qt.QFileDialog.AnyFile) + dialog.setAcceptMode(qt.QFileDialog.AcceptSave) + + def onFilterSelection(filt_): + # disable overwrite confirmation for NXdata types, + # because we append the data to existing files + if filt_ in self._appendFilters: + dialog.setOption(qt.QFileDialog.DontConfirmOverwrite) + else: + dialog.setOption(qt.QFileDialog.DontConfirmOverwrite, False) + + dialog.filterSelected.connect(onFilterSelection) + + if not dialog.exec(): + return False + + nameFilter = dialog.selectedNameFilter() + filename = dialog.selectedFiles()[0] + dialog.close() + + if "(" in nameFilter and ")" == nameFilter.strip()[-1]: + # Check for correct file extension + # Extract file extensions as .something + extensions = [ + ext[ext.find(".") :] + for ext in nameFilter[nameFilter.find("(") + 1 : -1].split() + ] + for ext in extensions: + if ( + len(filename) > len(ext) + and filename[-len(ext) :].lower() == ext.lower() + ): + break + else: # filename has no extension supported in nameFilter, add one + if len(extensions) >= 1: + filename += extensions[0] + + # Handle save + func = filters.get(nameFilter, None) + if func is not None: + return func(self.plot, filename, nameFilter) + else: + _logger.error("Unsupported file filter: %s", nameFilter) + return False + + +def _plotAsPNG(plot): + """Save a :class:`Plot` as PNG and return the payload. + + :param plot: The :class:`Plot` to save + """ + pngFile = BytesIO() + plot.saveGraph(pngFile, fileFormat="png") + pngFile.flush() + pngFile.seek(0) + data = pngFile.read() + pngFile.close() + return data + + +class PrintAction(PlotAction): + """QAction for printing the plot. + + It opens a Print dialog. + + Current implementation print a bitmap of the plot area and not vector + graphics, so printing quality is not great. + + :param plot: :class:`.PlotWidget` instance on which to operate. + :param parent: See :class:`QAction`. + """ + + def __init__(self, plot, parent=None): + super(PrintAction, self).__init__( + plot, + icon="document-print", + text="Print...", + tooltip="Open print dialog", + triggered=self.printPlot, + checkable=False, + parent=parent, + ) + self.setShortcut(qt.QKeySequence.Print) + self.setShortcutContext(qt.Qt.WidgetShortcut) + + def getPrinter(self): + """The QPrinter instance used by the PrintAction. + + :rtype: QPrinter + """ + return printer.getDefaultPrinter() + + def printPlotAsWidget(self): + """Open the print dialog and print the plot. + + Use :meth:`QWidget.render` to print the plot + + :return: True if successful + """ + dialog = qt.QPrintDialog(self.getPrinter(), self.plot) + dialog.setWindowTitle("Print Plot") + if not dialog.exec(): + return False + + # Print a snapshot of the plot widget at the top of the page + widget = self.plot.centralWidget() + + painter = qt.QPainter() + if not painter.begin(self.getPrinter()): + return False + + pageRect = self.getPrinter().pageRect(qt.QPrinter.DevicePixel) + xScale = pageRect.width() / widget.width() + yScale = pageRect.height() / widget.height() + scale = min(xScale, yScale) + + painter.translate(pageRect.width() / 2.0, 0.0) + painter.scale(scale, scale) + painter.translate(-widget.width() / 2.0, 0.0) + widget.render(painter) + painter.end() + + return True + + def printPlot(self): + """Open the print dialog and print the plot. + + Use :meth:`Plot.saveGraph` to print the plot. + + :return: True if successful + """ + # Init printer and start printer dialog + dialog = qt.QPrintDialog(self.getPrinter(), self.plot) + dialog.setWindowTitle("Print Plot") + if not dialog.exec(): + return False + + # Save Plot as PNG and make a pixmap from it with default dpi + pngData = _plotAsPNG(self.plot) + + pixmap = qt.QPixmap() + pixmap.loadFromData(pngData, "png") + + pageRect = self.getPrinter().pageRect(qt.QPrinter.DevicePixel) + xScale = pageRect.width() / pixmap.width() + yScale = pageRect.height() / pixmap.height() + scale = min(xScale, yScale) + + # Draw pixmap with painter + painter = qt.QPainter() + if not painter.begin(self.getPrinter()): + return False + + painter.drawPixmap( + 0, 0, pixmap.width() * scale, pixmap.height() * scale, pixmap + ) + painter.end() + + return True + + +class CopyAction(PlotAction): + """QAction to copy :class:`.PlotWidget` content to clipboard. + + :param plot: :class:`.PlotWidget` instance on which to operate + :param parent: See :class:`QAction` + """ + + def __init__(self, plot, parent=None): + super(CopyAction, self).__init__( + plot, + icon="edit-copy", + text="Copy plot", + tooltip="Copy a snapshot of the plot into the clipboard", + triggered=self.copyPlot, + checkable=False, + parent=parent, + ) + self.setShortcut(qt.QKeySequence.Copy) + self.setShortcutContext(qt.Qt.WidgetShortcut) + + def copyPlot(self): + """Copy plot content to the clipboard as a bitmap.""" + # Save Plot as PNG and make a QImage from it with default dpi + pngData = _plotAsPNG(self.plot) + image = qt.QImage.fromData(pngData, "png") + qt.QApplication.clipboard().setImage(image) |