3

I'd like to print pdf document to a printer. So far, I have this code.

def printDialog(self):
        filePath, filter = QFileDialog.getOpenFileName(self, 'Open file', '', 'Text (*.txt);;PDF (*.pdf)')
        if not filePath:
          return
        file_extension = os.path.splitext(filePath)[1]
        if file_extension == ".txt":
            doc = QtGui.QTextDocument()
            try:
                with open(filePath, 'r') as txtFile:
                    doc.setPlainText(txtFile.read())
                printer = QPrinter(QPrinter.HighResolution)
                if not QPrintDialog(printer, self).exec_():
                    return
                doc.print_(printer)
            except Exception as e:
                print('Error trying to print: {}'.format(e))
        elif file_extension == ".pdf":
            printer = QPrintDialog(QPrinter.HighResolution)
            printer.setOutputFormat(QPrinter.PdfFormat)
            printer.setOutputFileName(filePath)
            # TODO
        else:
            pass

I don't know how to continue in the TODO section.

xralf
  • 3,312
  • 45
  • 129
  • 200
  • Have you tried to use QWebEnginePage, as I already told you *twice*? – musicamante Feb 26 '20 at 20:51
  • @musicamante No, I have no idea now, how to do it. – xralf Feb 26 '20 at 21:00
  • @musicamante Can I print all documents in certain directory with it (select multiple documents)? And watch the progress of printing? e.g. show message boxes with "printing doc1", "printing doc2", etc. ? – xralf Feb 26 '20 at 21:05
  • In the answer to your previous question I linked to [this](https://stackoverflow.com/questions/54715978/how-to-print-html-page-with-image-in-qprinter-pyqt5/54716438) other answer that explains how to load and print html content. As I told you, just use `setUrl()`. The answer about multiple printing is yes, but it can only be addressed in another question, as soon as you understand how to print pdf files. – musicamante Feb 26 '20 at 21:23
  • @musicamante I tried index.html for the beginning, but I'm getting error `self._page.print(self._printer, self.print_completed) AttributeError: 'Window' object has no attribute 'print_completed'` . I'm sending pastebin with the code, if you could look at it https://pastebin.com/x582VjRe – xralf Feb 26 '20 at 21:56
  • `self.print_completed` in that example is clearly a reference to a method that the OP didn't include in the question code. [`print()`](https://doc.qt.io/qt-5/qwebenginepage.html#print) requires a callback parameter, which will receive a bool value to be notified whether the printing has been successful or not (printing is asynchronous, and could take some time until completion and shouldn't block the main loop, that's why a callback is required). You'll have to provide a method that reacts to that and use that method as callback parameter. – musicamante Feb 26 '20 at 22:15
  • @musicamante OK, I added `def print_completed(): print ("print completed")` to the code. It printed a page with logo, but "Hello" in my index.html wasn't there. Also, I have this error `TypeError: print_completed() takes 0 positional arguments but 2 were given` – xralf Feb 26 '20 at 22:39
  • I wrote "callback parameter, which will receive a **bool** value to be notified whether the printing has been successful or not". The callback needs an argument, the printing might not be successful. It's also written in the linked documentation. Please, read more carefully. – musicamante Feb 26 '20 at 22:44
  • @musicamante I tried `def print_completed(completed): if completed: print ("print completed") else: print("print not complete") ` Now it writes `TypeError: print_completed() takes 1 positional argument but 2 were given` But I have only one parameter – xralf Feb 26 '20 at 22:54
  • @musicamante I've got it, I forgot `self`. It works now, but I don't know what to add, to print pdf, instead of html – xralf Feb 26 '20 at 23:33
  • Have you used setUrl? Have you enabled the plugins? – musicamante Feb 26 '20 at 23:45
  • @musicamante Why should i setUrl if I only want to print pdf from the harddrive? I don't understand this logic. I'm googling and can't find nothing about this. – xralf Feb 27 '20 at 00:01
  • An url is just an address, it doesn't mean that's "on the web": it could be a local file too. QWebEnginePage is the abstract representation of a browser "view", and you can also load local files in your browser as soon as you type in the path to that file in the address field. Since you need to load a local PDF file, use [`url = QUrl.fromLocalFile(pathToTheFile)`](https://doc.qt.io/qt-5/qurl.html#fromLocalFile) to get the QUrl object and then `setUrl(url)`. Note that pdf file viewing has been introduced with Qt 5.13. – musicamante Feb 27 '20 at 00:21
  • @musicamante This is my code now https://pastebin.com/j5D7c4JY . I don't know how to enable that plugins. It prints empty page. – xralf Feb 27 '20 at 00:55
  • Unfortunately I cannot help you much with that, as I'm not able to further test it, I'm sorry. And, now that it comes to mind, it's possible that the rendering would create some issues anyway, so I'd suggest to look for third party libraries, such poppler. There's a pyqt binding https://pypi.org/project/python-poppler-qt5/ that's been updated just a couple of months ago, you could give it a try. – musicamante Feb 27 '20 at 07:01

1 Answers1

2

One possibility would be to use pdf2image which is

A python (3.5+) module that wraps pdftoppm and pdftocairo to convert PDF to a PIL Image object

see https://pypi.org/project/pdf2image/

Under the link above you will find installation instructions for poppler, which is a dependency of the module.

Steps

  • PDF file is readed
  • it is converted into images with help of pdf2image
  • each image is painted using QPaint

A simple version of the code would look like this:

images = convert_from_path(filePath, dpi=300, output_folder=path)
painter = QPainter()
painter.begin(printer)
for i, image in enumerate(images):
    if i > 0:
        printer.newPage()
    rect = painter.viewport()
    qtImage = ImageQt(image)
    qtImageScaled = qtImage.scaled(rect.size(), Qt.KeepAspectRatio, Qt.SmoothTransformation)
    painter.drawImage(rect, qtImageScaled)
painter.end()

The second parameter of convert_from_path describes the output resolution in dpi. The target rectangle in the device coordinate system can be determined by calling viewport on the QPainter instance. Finally, the image can then be smoothly scaled to the target rectangle using a bilinear filter algorithm.

Self-contained Sample Program

I have slightly modified your code and created a completely self-contained example.

Topics such as paper sizes, resolutions, optimization regarding memory requirements and error handling etc. are not handled. The intention is rather to give a minimal example.

import os
import sys

from PIL.ImageQt import ImageQt
from PyQt5 import QtWidgets
from PyQt5.QtCore import QSize, Qt
from PyQt5.QtGui import QPainter
from PyQt5.QtPrintSupport import QPrintDialog, QPrinter
from PyQt5.QtWidgets import QMainWindow, QFileDialog, QPushButton
import tempfile

from pdf2image import convert_from_path


class PrintDemo(QMainWindow):

    def __init__(self):
        QMainWindow.__init__(self)

        self.setMinimumSize(QSize(192, 128))
        self.setWindowTitle("Print Demo")

        printButton = QPushButton('Print', self)
        printButton.clicked.connect(self.onPrint)
        printButton.resize(128, 32)
        printButton.move(32, 48)

    def onPrint(self):
        self.printDialog()

    def printDialog(self):
        filePath, filter = QFileDialog.getOpenFileName(self, 'Open file', '', 'PDF (*.pdf)')
        if not filePath:
            return
        file_extension = os.path.splitext(filePath)[1]

        if file_extension == ".pdf":
            printer = QPrinter(QPrinter.HighResolution)
            dialog = QPrintDialog(printer, self)
            if dialog.exec_() == QPrintDialog.Accepted:
                with tempfile.TemporaryDirectory() as path:
                    images = convert_from_path(filePath, dpi=300, output_folder=path)
                    painter = QPainter()
                    painter.begin(printer)
                    for i, image in enumerate(images):
                        if i > 0:
                            printer.newPage()
                        rect = painter.viewport()
                        qtImage = ImageQt(image)
                        qtImageScaled = qtImage.scaled(rect.size(), Qt.KeepAspectRatio, Qt.SmoothTransformation)
                        painter.drawImage(rect, qtImageScaled)
                    painter.end()
        else:
            pass


if __name__ == "__main__":
    app = QtWidgets.QApplication(sys.argv)
    mainWin = PrintDemo()
    mainWin.show()
    sys.exit(app.exec_())
Stephan Schlecht
  • 26,556
  • 1
  • 33
  • 47
  • Thank you. And can it convert to larger images? How to preserve the size, to be the same as in input pdf? – xralf Mar 01 '20 at 15:31
  • I noticed that I can add second parameter to convert_from_bytes, dpi. But don't know what is the right value to preserve the size of pdf. – xralf Mar 01 '20 at 17:05
  • And there is a size parameter [yet](https://github.com/Belval/pdf2image), but don't know the right size. – xralf Mar 01 '20 at 17:13
  • The dpi parameter determines the quality of the PDF to image conversion. To fill the complete page the target rectangle in the device coordinate system can be determined by calling viewport on the painter. The image can then be smoothly scaled to the target rectangle using a bilinear filter algorithm. I have updated the answer and the sample code accordingly. – Stephan Schlecht Mar 01 '20 at 19:26
  • Thank you. And do you think it's usable in the environment where we print about 50 documents daily, every has about 100 pages. Or should I rather use [gsprint](https://stackoverflow.com/questions/27195594/python-silent-print-pdf-to-specific-printer). Speed is important for us. – xralf Mar 01 '20 at 19:43
  • Now, I tried to print 300 pages long pdf and I got MemoryError and whole laptop frozen for a minute. – xralf Mar 01 '20 at 19:57
  • 1
    At the bottom of the page https://pypi.org/project/pdf2image/ it says: _A relatively large PDF file will use up all your memory and cause the process to abort (unless you use an output folder)._ I have updated the answer to use the output folder variant. You can also find further performance tips under this link. – Stephan Schlecht Mar 01 '20 at 20:19
  • Unfortunately I have never used gsprint, it is quite possible that it is much more efficient. It's probably worth a try. – Stephan Schlecht Mar 01 '20 at 20:25
  • Now, it printed little longer, but still ended with `QImage: out of memory, returning null` and MemoryError. I'm now on Linux environment. I will try it tommorrow on Windows and if it won't be working I will try gsprint (I hope there won't be the same problem with memory) – xralf Mar 01 '20 at 20:43
  • Interesting: I made an attempt with a large PDF file with many pages and with convert_from_path the image conversion is no longer the problem. The memory requirement now arises in the actual print handling (printer, painter). The gsprint approach actually seems to be the more promising approach for large PDF files. – Stephan Schlecht Mar 02 '20 at 07:11