Source code for silx.gui.widgets.PrintPreview

# /*##########################################################################
#
# Copyright (c) 2004-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 implements a print preview dialog.

The dialog provides methods to send images, pixmaps and SVG
items to the page to be printed.

The user can interactively move and resize the items.
"""
import sys
import logging
from silx.gui import qt, printer


__authors__ = ["V.A. Sole", "P. Knobel"]
__license__ = "MIT"
__date__ = "11/07/2017"


_logger = logging.getLogger(__name__)


[docs] class PrintPreviewDialog(qt.QDialog): """Print preview dialog widget.""" def __init__(self, parent=None, printer=None): qt.QDialog.__init__(self, parent) self.setWindowTitle("Print Preview") self.setModal(False) self.resize(400, 500) self.mainLayout = qt.QVBoxLayout(self) self.mainLayout.setContentsMargins(0, 0, 0, 0) self.mainLayout.setSpacing(0) self._buildToolbar() self.printer = printer # :class:`QPrinter` (paint device that paints on a printer). # :meth:`showEvent` has been reimplemented to enforce printer # setup. self.printDialog = None # :class:`QPrintDialog` (dialog for specifying the printer's # configuration) self.scene = None # :class:`QGraphicsScene` (surface for managing # 2D graphical items) self.page = None # :class:`QGraphicsRectItem` used as white background page on which # to display the print preview. self.view = None # :class:`QGraphicsView` widget for displaying :attr:`scene` self._svgItems = [] # List storing :class:`QSvgRenderer` items to be printed, added in # :meth:`addSvgItem`, cleared in :meth:`_clearAll`. # This ensures that there is a reference pointing to the items, # which ensures they are not destroyed before being printed. self._viewScale = 1.0 # Zoom level (1.0 is 100%) self._toBeCleared = False # Flag indicating that all items must be removed from :attr:`scene` # and from :attr:`_svgItems`. # Set to True after a successful printing. The widget is then hidden, # and it will be cleared the next time it is shown. # Reset to False after :meth:`_clearAll` has done its job. def _buildToolbar(self): toolBar = qt.QWidget(self) # a layout for the toolbar toolsLayout = qt.QHBoxLayout(toolBar) toolsLayout.setContentsMargins(0, 0, 0, 0) toolsLayout.setSpacing(0) hideBut = qt.QPushButton("Hide", toolBar) hideBut.setToolTip("Hide print preview dialog") hideBut.clicked.connect(self.hide) cancelBut = qt.QPushButton("Clear All", toolBar) cancelBut.setToolTip("Remove all items") cancelBut.clicked.connect(self._clearAll) removeBut = qt.QPushButton("Remove", toolBar) removeBut.setToolTip("Remove selected item (use left click to select)") removeBut.clicked.connect(self._remove) setupBut = qt.QPushButton("Setup", toolBar) setupBut.setToolTip("Select and configure a printer") setupBut.clicked.connect(self.setup) printBut = qt.QPushButton("Print", toolBar) printBut.setToolTip("Print page and close print preview") printBut.clicked.connect(self._print) zoomPlusBut = qt.QPushButton("Zoom +", toolBar) zoomPlusBut.clicked.connect(self._zoomPlus) zoomMinusBut = qt.QPushButton("Zoom -", toolBar) zoomMinusBut.clicked.connect(self._zoomMinus) toolsLayout.addWidget(hideBut) toolsLayout.addWidget(printBut) toolsLayout.addWidget(cancelBut) toolsLayout.addWidget(removeBut) toolsLayout.addWidget(setupBut) # toolsLayout.addStretch() # toolsLayout.addWidget(marginLabel) # toolsLayout.addWidget(self.marginSpin) toolsLayout.addStretch() # toolsLayout.addWidget(scaleLabel) # toolsLayout.addWidget(self.scaleCombo) toolsLayout.addWidget(zoomPlusBut) toolsLayout.addWidget(zoomMinusBut) # toolsLayout.addStretch() self.toolBar = toolBar self.mainLayout.addWidget(self.toolBar) def _buildStatusBar(self): """Create the status bar used to display the printer name or output file name.""" # status bar statusBar = qt.QStatusBar(self) self.targetLabel = qt.QLabel(statusBar) self._updateTargetLabel() statusBar.addWidget(self.targetLabel) self.mainLayout.addWidget(statusBar) def _updateTargetLabel(self): """Update printer name or file name shown in the status bar.""" if self.printer is None: self.targetLabel.setText("Undefined printer") return if self.printer.outputFileName(): self.targetLabel.setText("File:" + self.printer.outputFileName()) else: self.targetLabel.setText("Printer:" + self.printer.printerName()) def _updatePrinter(self): """Resize :attr:`page`, :attr:`scene` and :attr:`view` to :attr:`printer` width and height.""" printer = self.printer assert ( printer is not None ), "_updatePrinter should not be called unless a printer is defined" if self.scene is None: self.scene = qt.QGraphicsScene() self.scene.setBackgroundBrush(qt.QColor(qt.Qt.lightGray)) self.scene.setSceneRect(qt.QRectF(0, 0, printer.width(), printer.height())) if self.page is None: self.page = qt.QGraphicsRectItem(0, 0, printer.width(), printer.height()) self.page.setBrush(qt.QColor(qt.Qt.white)) self.scene.addItem(self.page) self.scene.setSceneRect(qt.QRectF(0, 0, printer.width(), printer.height())) self.page.setPos(qt.QPointF(0.0, 0.0)) self.page.setRect(qt.QRectF(0, 0, printer.width(), printer.height())) if self.view is None: self.view = qt.QGraphicsView(self.scene) self.mainLayout.addWidget(self.view) self._buildStatusBar() # self.view.scale(1./self._viewScale, 1./self._viewScale) self.view.fitInView(self.page.rect(), qt.Qt.KeepAspectRatio) self._viewScale = 1.00 self._updateTargetLabel() # Public methods
[docs] def addImage(self, image, title=None, comment=None, commentPosition=None): """Add an image to the print preview scene. :param QImage image: Image to be added to the scene :param str title: Title shown above (centered) the image :param str comment: Comment displayed below the image :param commentPosition: "CENTER" or "LEFT" """ self.addPixmap( qt.QPixmap.fromImage(image), title=title, comment=comment, commentPosition=commentPosition, )
[docs] def addPixmap(self, pixmap, title=None, comment=None, commentPosition=None): """Add a pixmap to the print preview scene :param QPixmap pixmap: Pixmap to be added to the scene :param str title: Title shown above (centered) the pixmap :param str comment: Comment displayed below the pixmap :param commentPosition: "CENTER" or "LEFT" """ if self._toBeCleared: self._clearAll() self.ensurePrinterIsSet() if self.printer is None: _logger.error("printer is not set, cannot add pixmap to page") return if title is None: title = " " * 88 if comment is None: comment = " " * 88 if commentPosition is None: commentPosition = "CENTER" rectItem = qt.QGraphicsRectItem(self.page) rectItem.setRect(qt.QRectF(1, 1, pixmap.width(), pixmap.height())) pen = rectItem.pen() color = qt.QColor(qt.Qt.red) color.setAlpha(1) pen.setColor(color) rectItem.setPen(pen) rectItem.setZValue(1) rectItem.setFlag(qt.QGraphicsItem.ItemIsSelectable, True) rectItem.setFlag(qt.QGraphicsItem.ItemIsMovable, True) rectItem.setFlag(qt.QGraphicsItem.ItemIsFocusable, False) rectItemResizeRect = _GraphicsResizeRectItem(rectItem, self.scene) rectItemResizeRect.setZValue(2) pixmapItem = qt.QGraphicsPixmapItem(rectItem) pixmapItem.setPixmap(pixmap) pixmapItem.setZValue(0) # I add the title textItem = qt.QGraphicsTextItem(title, rectItem) textItem.setTextInteractionFlags(qt.Qt.TextEditorInteraction) offset = 0.5 * textItem.boundingRect().width() textItem.moveBy(0.5 * pixmap.width() - offset, -20) textItem.setZValue(2) # I add the comment commentItem = qt.QGraphicsTextItem(comment, rectItem) commentItem.setTextInteractionFlags(qt.Qt.TextEditorInteraction) offset = 0.5 * commentItem.boundingRect().width() if commentPosition.upper() == "LEFT": x = 1 else: x = 0.5 * pixmap.width() - offset commentItem.moveBy(x, pixmap.height() + 20) commentItem.setZValue(2) rectItem.moveBy(20, 40)
[docs] def addSvgItem( self, item, title=None, comment=None, commentPosition=None, viewBox=None, keepRatio=True, ): """Add a SVG item to the scene. :param QSvgRenderer item: SVG item to be added to the scene. :param str title: Title shown above (centered) the SVG item. :param str comment: Comment displayed below the SVG item. :param str commentPosition: "CENTER" or "LEFT" :param QRectF viewBox: Bounding box for the item on the print page (xOffset, yOffset, width, height). If None, use original item size. :param bool keepRatio: If True, resizing the item will preserve its original aspect ratio. """ if not qt.HAS_SVG: raise RuntimeError("Missing QtSvg library.") if not isinstance(item, qt.QSvgRenderer): raise TypeError("addSvgItem: QSvgRenderer expected") if self._toBeCleared: self._clearAll() self.ensurePrinterIsSet() if self.printer is None: _logger.error("printer is not set, cannot add SvgItem to page") return if title is None: title = 50 * " " if comment is None: comment = 80 * " " if commentPosition is None: commentPosition = "CENTER" if viewBox is None: if hasattr(item, "_viewBox"): # PyMca compatibility: viewbox attached to item viewBox = item._viewBox else: # try the original item viewbox viewBox = item.viewBoxF() svgItem = _GraphicsSvgRectItem(viewBox, self.page) svgItem.setSvgRenderer(item) svgItem.setCacheMode(qt.QGraphicsItem.NoCache) svgItem.setZValue(0) svgItem.setFlag(qt.QGraphicsItem.ItemIsSelectable, True) svgItem.setFlag(qt.QGraphicsItem.ItemIsMovable, True) svgItem.setFlag(qt.QGraphicsItem.ItemIsFocusable, False) rectItemResizeRect = _GraphicsResizeRectItem( svgItem, self.scene, keepratio=keepRatio ) rectItemResizeRect.setZValue(2) self._svgItems.append(item) # Comment / legend dummyComment = 80 * "1" commentItem = qt.QGraphicsTextItem(dummyComment, svgItem) commentItem.setTextInteractionFlags(qt.Qt.TextEditorInteraction) # we scale the text to have the legend box have the same width as the graph scaleCalculationRect = qt.QRectF(commentItem.boundingRect()) scale = svgItem.boundingRect().width() / scaleCalculationRect.width() commentItem.setPlainText(comment) commentItem.setZValue(1) commentItem.setFlag(qt.QGraphicsItem.ItemIsMovable, True) commentItem.setScale(scale) # align if commentPosition.upper() == "CENTER": alignment = qt.Qt.AlignCenter elif commentPosition.upper() == "RIGHT": alignment = qt.Qt.AlignRight else: alignment = qt.Qt.AlignLeft commentItem.setTextWidth(commentItem.boundingRect().width()) center_format = qt.QTextBlockFormat() center_format.setAlignment(alignment) cursor = commentItem.textCursor() cursor.select(qt.QTextCursor.Document) cursor.mergeBlockFormat(center_format) cursor.clearSelection() commentItem.setTextCursor(cursor) if alignment == qt.Qt.AlignLeft: deltax = 0 else: deltax = ( svgItem.boundingRect().width() - commentItem.boundingRect().width() ) / 2.0 commentItem.moveBy( svgItem.boundingRect().x() + deltax, svgItem.boundingRect().y() + svgItem.boundingRect().height(), ) # Title textItem = qt.QGraphicsTextItem(title, svgItem) textItem.setTextInteractionFlags(qt.Qt.TextEditorInteraction) textItem.setZValue(1) textItem.setFlag(qt.QGraphicsItem.ItemIsMovable, True) title_offset = 0.5 * textItem.boundingRect().width() textItem.moveBy( svgItem.boundingRect().x() + 0.5 * svgItem.boundingRect().width() - title_offset * scale, svgItem.boundingRect().y(), ) textItem.setScale(scale)
[docs] def setup(self): """Open a print dialog to ensure the :attr:`printer` is set. If the setting fails or is cancelled, :attr:`printer` is reset to *None*. """ if self.printer is None: self.printer = printer.getDefaultPrinter() if self.printDialog is None: self.printDialog = qt.QPrintDialog(self.printer, self) if self.printDialog.exec(): if self.printer.width() <= 0 or self.printer.height() <= 0: self.message = qt.QMessageBox(self) self.message.setIcon(qt.QMessageBox.Critical) self.message.setText( "Unknown library error \non printer initialization" ) self.message.setWindowTitle("Library Error") self.message.setModal(0) self.printer = None return self.printer.setFullPage(True) self._updatePrinter() else: # printer setup cancelled, check for a possible previous configuration if self.page is None: # not initialized self.printer = None
[docs] def ensurePrinterIsSet(self): """If the printer is not already set, try to interactively setup the printer using a QPrintDialog. In case of failure, hide widget and log a warning. :return: True if printer was set. False if it failed or if the selection dialog was canceled. """ if self.printer is None: self.setup() if self.printer is None: self.hide() _logger.warning( "Printer setup failed or was cancelled, " + "but printer is required." ) return self.printer is not None
[docs] def setOutputFileName(self, name): """Set output filename. Setting a non-empty name enables printing to file. :param str name: File name (path)""" self.printer.setOutputFileName(name)
# overloaded methods
[docs] def exec(self): if self._toBeCleared: self._clearAll() return qt.QDialog.exec(self)
[docs] def exec_(self): # Qt5 compatibility return self.exec()
[docs] def raise_(self): if self._toBeCleared: self._clearAll() return qt.QDialog.raise_(self)
def showEvent(self, event): """Reimplemented to force printer setup. In case of failure, hide the widget.""" if self._toBeCleared: self._clearAll() self.ensurePrinterIsSet() return super(PrintPreviewDialog, self).showEvent(event) # button callbacks def _print(self): """Do the printing, hide the print preview dialog, set :attr:`_toBeCleared` flag to True to trigger clearing the next time the dialog is shown. If the printer is not setup, do it first.""" printer = self.printer painter = qt.QPainter() if not painter.begin(printer) or printer is None: _logger.error("Cannot initialize printer") return try: self.scene.render( painter, qt.QRectF(0, 0, printer.width(), printer.height()), qt.QRectF( self.page.rect().x(), self.page.rect().y(), self.page.rect().width(), self.page.rect().height(), ), qt.Qt.KeepAspectRatio, ) painter.end() self.hide() self.accept() self._toBeCleared = True except: # FIXME painter.end() qt.QMessageBox.critical( self, "ERROR", "Printing problem:\n %s" % sys.exc_info()[1] ) _logger.error("printing problem:\n %s" % sys.exc_info()[1]) return def _zoomPlus(self): self._viewScale *= 1.20 self.view.scale(1.20, 1.20) def _zoomMinus(self): self._viewScale *= 0.80 self.view.scale(0.80, 0.80) def _clearAll(self): """ Clear the print preview window, remove all items but keep the page. """ itemlist = self.scene.items() keep = self.page while len(itemlist) != 1: if itemlist.index(keep) == 0: self.scene.removeItem(itemlist[1]) else: self.scene.removeItem(itemlist[0]) itemlist = self.scene.items() self._svgItems = [] self._toBeCleared = False def _remove(self): """Remove selected item in :attr:`scene`.""" itemlist = self.scene.items() # this loop is not efficient if there are many items ... for item in itemlist: if item.isSelected(): self.scene.removeItem(item)
[docs] class SingletonPrintPreviewDialog(PrintPreviewDialog): """Singleton print preview dialog. All widgets in a program that instantiate this class will share a single print preview dialog. This enables sending multiple images to a single page to be printed. """ _instance = None def __new__(self, *var, **kw): if self._instance is None: self._instance = PrintPreviewDialog(*var, **kw) return self._instance
class _GraphicsSvgRectItem(qt.QGraphicsRectItem): """:class:`qt.QGraphicsRectItem` with an attached :class:`qt.QSvgRenderer`, and with a painter redefined to render the SVG item.""" def setSvgRenderer(self, renderer): """ :param QSvgRenderer renderer: svg renderer """ self._renderer = renderer def paint(self, painter, *var, **kw): self._renderer.render(painter, self.boundingRect()) class _GraphicsResizeRectItem(qt.QGraphicsRectItem): """Resizable QGraphicsRectItem.""" def __init__(self, parent=None, scene=None, keepratio=True): qt.QGraphicsRectItem.__init__(self, parent) rect = parent.boundingRect() x = rect.x() y = rect.y() w = rect.width() h = rect.height() self._newRect = None self.keepRatio = keepratio self.setRect(qt.QRectF(x + w - 40, y + h - 40, 40, 40)) self.setAcceptHoverEvents(True) pen = qt.QPen() color = qt.QColor(qt.Qt.white) color.setAlpha(0) pen.setColor(color) pen.setStyle(qt.Qt.NoPen) self.setPen(pen) self.setBrush(color) self.setFlag(qt.QGraphicsItem.ItemIsMovable, True) self.show() def hoverEnterEvent(self, event): if self.parentItem().isSelected(): self.parentItem().setSelected(False) if self.keepRatio: self.setCursor(qt.QCursor(qt.Qt.SizeFDiagCursor)) else: self.setCursor(qt.QCursor(qt.Qt.SizeAllCursor)) self.setBrush(qt.QBrush(qt.Qt.yellow, qt.Qt.SolidPattern)) return qt.QGraphicsRectItem.hoverEnterEvent(self, event) def hoverLeaveEvent(self, event): self.setCursor(qt.QCursor(qt.Qt.ArrowCursor)) pen = qt.QPen() color = qt.QColor(qt.Qt.white) color.setAlpha(0) pen.setColor(color) pen.setStyle(qt.Qt.NoPen) self.setPen(pen) self.setBrush(color) return qt.QGraphicsRectItem.hoverLeaveEvent(self, event) def mousePressEvent(self, event): if self._newRect is not None: self._newRect = None self._point0 = self.pos() parent = self.parentItem() scene = self.scene() # following line prevents dragging along the previously selected # item when resizing another one scene.clearSelection() rect = parent.boundingRect() self._x = rect.x() self._y = rect.y() self._w = rect.width() self._h = rect.height() self._ratio = self._w / self._h self._newRect = qt.QGraphicsRectItem(parent) self._newRect.setRect(qt.QRectF(self._x, self._y, self._w, self._h)) qt.QGraphicsRectItem.mousePressEvent(self, event) def mouseMoveEvent(self, event): point1 = self.pos() deltax = point1.x() - self._point0.x() deltay = point1.y() - self._point0.y() if self.keepRatio: r1 = (self._w + deltax) / self._w r2 = (self._h + deltay) / self._h if r1 < r2: self._newRect.setRect( qt.QRectF( self._x, self._y, self._w + deltax, (self._w + deltax) / self._ratio, ) ) else: self._newRect.setRect( qt.QRectF( self._x, self._y, (self._h + deltay) * self._ratio, self._h + deltay, ) ) else: self._newRect.setRect( qt.QRectF(self._x, self._y, self._w + deltax, self._h + deltay) ) qt.QGraphicsRectItem.mouseMoveEvent(self, event) def mouseReleaseEvent(self, event): point1 = self.pos() deltax = point1.x() - self._point0.x() deltay = point1.y() - self._point0.y() self.moveBy(-deltax, -deltay) parent = self.parentItem() # deduce scale from rectangle if self.keepRatio: scalex = self._newRect.rect().width() / self._w scaley = scalex else: scalex = self._newRect.rect().width() / self._w scaley = self._newRect.rect().height() / self._h # apply the scale to the previous transformation matrix previousTransform = parent.transform() parent.setTransform(previousTransform.scale(scalex, scaley)) self.scene().removeItem(self._newRect) self._newRect = None qt.QGraphicsRectItem.mouseReleaseEvent(self, event) def main(): """ """ if len(sys.argv) < 2: print("give an image file as parameter please.") sys.exit(1) if len(sys.argv) > 2: print("only one parameter please.") sys.exit(1) filename = sys.argv[1] w = PrintPreviewDialog() w.resize(400, 500) comment = "" for i in range(20): comment += "Line number %d: En un lugar de La Mancha de cuyo nombre ...\n" % i if filename[-3:] == "svg": item = qt.QSvgRenderer(filename, w.page) w.addSvgItem(item, title=filename, comment=comment, commentPosition="CENTER") else: w.addPixmap( qt.QPixmap.fromImage(qt.QImage(filename)), title=filename, comment=comment, commentPosition="CENTER", ) w.addImage(qt.QImage(filename), comment=comment, commentPosition="LEFT") sys.exit(w.exec()) if __name__ == "__main__": a = qt.QApplication(sys.argv) main() a.exec()