pythonpython-3.xpyqt5qprinter

print a file with certain extension


I'd like to print a file selected with a file picker (or somehow) with certain extension, so that PyQt or printer will automatically recognize the format (e.g. pdf, ms word, excel, txt, html, jpg etc.)

So far, I have found here how to print the content of the TextEdit, but I'd like to print files with various formats.

Is it possible with PyQt5 or should I search elsewhere?


Solution

  • Printing a plain text document does not require a viewer, since the print_() function actually calls the internal QDocument's print_() function:

    filePath, filter = QFileDialog.getOpenFileName(self, 'Open file', '', 'Text (*.txt)')
    if not filePath:
        return
    doc = QtGui.QTextDocument()
    try:
        with open(filePath, 'r') as txtFile:
            doc.setPlainText(txtFile.read())
        printer = QtPrintSupport.QPrinter(QtPrintSupport.QPrinter.HighResolution)
        if not QtPrintSupport.QPrintDialog(printer, self).exec_():
            return
        doc.print_(printer)
    except Exception as e:
        print('Error trying to print: {}'.format(e))
    

    You might want to add some functionalities to set the page size, document margins, font sizes etc, before actually printing (just read the QTextDocument docs), I'll leave that to you.


    Printing from html files is almost similar, but you'll need to use the QWebEnginePage class from QtWebEngineWidgets. See this answer.
    Do not use QTextDocument.setHtml(), as Qt has limited support for html tags.

    The same is valid for PDF files too, the difference is that the file has to be loaded through setUrl() and the QWebEngineSettings.PluginsEnabled setting has to be enabled through page.settings().setAttribute(setting, bool) in case it's not.
    Read the documentation about PDF File Viewing.


    Printing images could be done through two approaches.

    The first and simpler, is to create a temporary html file that embeds the image and load to a webengine page as above (you could add controls for zoom/scaling).

    Alternatively, you could directly print using QPainter, but you'll have to relate to the printer resolution and image size, so you'd probably want to have a preview dialog before actually printing the image, otherwise it might be too small (or too big).

    While more complex than a plain <html><img src=""></html>, this allows a better control on the positioning and sizing of the image[s].

    class ImagePrintPreview(QtWidgets.QDialog):
        def __init__(self, parent, printer, pixmap):
            super().__init__(parent)
    
            self.printer = printer
            self.pixmap = pixmap
    
            layout = QtWidgets.QGridLayout(self)
    
            self.viewer = QtWidgets.QLabel()
            layout.addWidget(self.viewer, 0, 0, 1, 2)
    
            self.resoCombo = QtWidgets.QComboBox()
            layout.addWidget(self.resoCombo, 1, 0)
    
            self.zoom = QtWidgets.QSpinBox(minimum=50, maximum=200, suffix='%')
            self.zoom.setValue(100)
            self.zoom.setAccelerated(True)
            layout.addWidget(self.zoom, 1, 1)
            self.zoom.valueChanged.connect(self.updatePreview)
    
            self.buttonBox = QtWidgets.QDialogButtonBox(
                QtWidgets.QDialogButtonBox.Ok|QtWidgets.QDialogButtonBox.Cancel)
            layout.addWidget(self.buttonBox)
            self.buttonBox.accepted.connect(self.accept)
            self.buttonBox.rejected.connect(self.reject)
    
            default = printer.resolution()
            self.resoCombo.addItem(str(default), default)
            for dpi in (150, 300, 600, 1200):
                if dpi == default:
                    continue
                self.resoCombo.addItem(str(dpi), dpi)
            self.resoCombo.currentIndexChanged.connect(self.updatePreview)
    
            self.updatePreview()
    
        def updatePreview(self):
            # create a preview to show how the image will be printed
            self.printer.setResolution(self.resoCombo.currentData())
            paperRect = self.printer.paperRect(self.printer.DevicePixel)
            printRect = self.printer.pageRect(self.printer.DevicePixel)
    
            # a temporary pixmap that will use the printer's page size
            # note that page/paper are QRectF, they have a QSizeF which has to
            # be converted to a QSize
            pm = QtGui.QPixmap(paperRect.size().toSize())
            # new pixmap have allocated memory for their contents, which usually
            # result in some random pixels, just fill it with white
            pm.fill(QtCore.Qt.white)
            # start a qpainter on the pixmap
            qp = QtGui.QPainter(pm)
            # scale the pixmap to the wanted zoom value
            zoom = self.zoom.value() * .01
            scaled = self.pixmap.scaledToWidth(int(self.pixmap.width() * zoom), QtCore.Qt.SmoothTransformation)
            # paint the pixmap aligned to the printing margins
            qp.drawPixmap(printRect.topLeft(), scaled)
    
            # other possible alternatives:
    
            # Center the image:
            #   qp.translate(printRect.center())
            #   delta = QtCore.QPointF(scaled.rect().center())
            #   qp.drawPixmap(-delta, scaled)
    
            # To also rotate 90° clockwise, add this to the above:
            #   qp.rotate(90)
            # *after* qp.translate() and before qp.drawPixmap()
    
    
            # when painting to a non QWidget device, you always have to end the
            # painter before being able to use it
            qp.end()
            # scale the temporary pixmap to a fixed width
            self.viewer.setPixmap(pm.scaledToWidth(300, QtCore.Qt.SmoothTransformation))
    
        def exec_(self):
            if super().exec_():
                self.printer.setResolution(self.resoCombo.currentData())
                # do the same as above, but paint directly on the printer device
                printRect = self.printer.pageRect(self.printer.DevicePixel)
                qp = QtGui.QPainter(self.printer)
                zoom = self.zoom.value() * .01
                scaled = self.pixmap.scaledToWidth(int(self.pixmap.width() * zoom), QtCore.Qt.SmoothTransformation)
                qp.drawPixmap(printRect.topLeft(), scaled)
                # as above, that's important!
                qp.end()
    
    
    class ImagePrinter(QtWidgets.QWidget):
        def __init__(self):
            super().__init__()
            layout = QtWidgets.QVBoxLayout(self)
            selBtn = QtWidgets.QPushButton('Open image')
            layout.addWidget(selBtn)
            selBtn.clicked.connect(self.selectFile)
    
        def selectFile(self):
            filePath, filter = QtWidgets.QFileDialog.getOpenFileName(self, 'Open file', '/tmp', 'Images (*.jpg *.png)')
            if not filePath:
                return
            pixmap = QtGui.QPixmap(filePath)
            if pixmap.isNull():
                return
    
            printer = QtPrintSupport.QPrinter(QtPrintSupport.QPrinter.HighResolution)
            if QtPrintSupport.QPrintDialog(printer, self).exec_():
                ImagePrintPreview(self, printer, pixmap).exec_()
    

    Note that I couldn't test this under windows, so it might be necessary to change things related to the resolution (possibly by using printer.supportedResolutions()).


    As already explained in the comments, printing to other (and possibly proprietary) formats requires external modules.