Combining QPainter and QTextDocument on QPdfWriter

971 Views Asked by At

In another question, I learned about QTextDocument, and I was told that you can use a QPainter and a QTextDocument on the same page of a PDF. However, when I tried to do that, they each restart the document, wiping out the other's content.

from PySide6.QtGui import QPdfWriter, QPainter, QPageSize, QTextDocument, Qt
from PySide6.QtWidgets import QApplication


def main():
    app = QApplication()
    pdf = QPdfWriter('example.pdf')
    pdf.setPageSize(QPageSize.Letter)

    # Whichever of these goes second, overwrites the first.
    draw_diagram(pdf)
    print_document(pdf)

    app.exit(0)


def draw_diagram(pdf: QPdfWriter):
    painter = QPainter(pdf)
    painter.drawArc(painter.window().width()//4,
                    painter.window().height()//2 - painter.window().width()//4,
                    painter.window().width()//2,
                    painter.window().width()//2,
                    0,
                    5760)
    painter.drawText(0,
                     painter.window().height()//2,
                     painter.window().width(),
                     painter.window().height()//10,
                     Qt.AlignHCenter | Qt.AlignTop,
                     'https://donkirkby.github.io')
    print(pdf.newPage())
    painter.drawText(painter.window().width()//2,
                     painter.window().height()//2,
                     'Bar')
    print(pdf.newPage())
    painter.end()


def print_document(pdf: QPdfWriter):
    html = "<a href='https://donkirkby.github.io'>donkirkby.github.io</a>"

    document = QTextDocument()
    document.setHtml(html)
    document.print_(pdf)


main()

Ideally, I'd like text and drawing on the same page, but this code tries to keep them separate so they don't overwrite each other. It doesn't work either way.

How can I combine drawings with a text document? Is QTextCursor helpful?

2

There are 2 best solutions below

0
eyllanesc On BEST ANSWER

The problem is that every time a QPainter is set then the QPdfWriter is reset. A possible solution is to use the same QPainter and instead of the print method you should use drawContents, you will also have to handle the paging manually.

from PySide6.QtGui import QPdfWriter, QPainter, QPageSize, QTextDocument, Qt
from PySide6.QtWidgets import QApplication


def main():
    app = QApplication()
    pdf = QPdfWriter("example.pdf")
    pdf.setPageSize(QPageSize.Letter)

    painter = QPainter(pdf)

    draw_diagram(painter, pdf)
    print_document(painter, pdf)

    painter.end()


def draw_diagram(painter: QPainter, pdf: QPdfWriter):
    painter.drawArc(
        painter.window().width() // 4,
        painter.window().height() // 2 - painter.window().width() // 4,
        painter.window().width() // 2,
        painter.window().width() // 2,
        0,
        5760,
    )
    painter.drawText(
        0,
        painter.window().height() // 2,
        painter.window().width(),
        painter.window().height() // 10,
        Qt.AlignHCenter | Qt.AlignTop,
        "https://donkirkby.github.io",
    )
    print(pdf.newPage())
    painter.drawText(
        painter.window().width() // 2, painter.window().height() // 2, "Bar"
    )
    print(pdf.newPage())


def print_document(painter: QPainter, pdf: QPdfWriter):
    document = QTextDocument()
    document.documentLayout().setPaintDevice(pdf)

    html = "<a href='https://donkirkby.github.io'>donkirkby.github.io</a>"
    document.setHtml(html)

    document.drawContents(painter)


if __name__ == "__main__":
    main()
0
Don Kirkby On

Although I ended up using eyllanesc's answer of switching from document.print_() to document.drawContents(), I also experimented with QTextCursor and QPyTextObject. This TextObject example gives a good introduction. It would be useful if I needed to spread over multiple pages, I think.

Here's my example from the question converted to use QPyTextObject:

from PySide6.QtCore import QSizeF, QRectF
from PySide6.QtGui import (QPdfWriter, QPainter, QPageSize, QTextDocument, Qt, QPyTextObject, QTextFormat, QTextCursor,
                           QTextCharFormat)
from PySide6.QtWidgets import QApplication

DIAGRAM_TEXT_FORMAT = QTextFormat.UserObject + 1
DIAGRAM_DATA = 1
OBJECT_REPLACEMENT = chr(0xfffc)


def main():
    app = QApplication()
    pdf = QPdfWriter('example.pdf')
    pdf.setPageSize(QPageSize.Letter)

    print_document(pdf)

    app.exit(0)


class Diagram(QPyTextObject):
    def __init__(self, parent=None):
        super().__init__(parent)

    # noinspection PyPep8Naming,PyShadowingBuiltins
    def intrinsicSize(self,
                      doc: QTextDocument,
                      posInDocument: int,
                      format: QTextFormat) -> QSizeF:
        diameter = doc.textWidth()/4
        return QSizeF(doc.textWidth(), diameter)

    # noinspection PyPep8Naming,PyShadowingBuiltins
    def drawObject(self,
                   painter: QPainter,
                   rect: QRectF,
                   doc: QTextDocument,
                   posInDocument: int,
                   format: QTextFormat):
        diameter = rect.height()
        message = format.property(DIAGRAM_DATA)
        painter.drawArc((rect.width() - diameter) // 2,
                        rect.y(),
                        diameter,
                        diameter,
                        0,
                        5760)
        painter.drawText(rect,
                         Qt.AlignHCenter | Qt.AlignVCenter,
                         message)


def print_document(pdf: QPdfWriter):
    html = """\
<h1>Title</h1>
<p>Lorem ipsum
<a href='https://donkirkby.github.io'>donkirkby.github.io</a>
dolores sit amet.</p>
"""

    document = QTextDocument()
    document.setPageSize(QSizeF(pdf.width(), pdf.height()))
    font = document.defaultFont()
    font.setPixelSize(pdf.height()//60)
    document.setDefaultFont(font)
    diagram_handler = Diagram()
    doc_layout = document.documentLayout()
    doc_layout.registerHandler(DIAGRAM_TEXT_FORMAT, diagram_handler)

    document.setHtml(html)
    cursor = QTextCursor(document)
    cursor.movePosition(cursor.End)
    cursor.insertText('\n')
    
    diagram_format = QTextCharFormat()
    diagram_format.setObjectType(DIAGRAM_TEXT_FORMAT)

    for i in range(1, 11):
        cursor.insertHtml(f'<h3>Heading {i}</h3>')
        cursor.insertText('\n')
        diagram_format.setProperty(DIAGRAM_DATA, f'Message {i} in a circle')
        cursor.insertText(OBJECT_REPLACEMENT, diagram_format)

    document.print_(pdf)


main()

The thing I still don't like about this approach is that a page break can be added between the heading and the diagram. I'll ask a follow up question about that.