Skip to main content
`Ctrl`+`K`

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

    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

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

        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)

        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)

        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

        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

        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

        def exec(self):
            if self._toBeCleared:
                self._clearAll()
            return qt.QDialog.exec(self)

        def exec_(self):  # Qt5 compatibility
            return self.exec()

        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)

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