Home » Python » python – Print items to pdf-Exceptionshub

python – Print items to pdf-Exceptionshub

Posted by: admin February 24, 2020 Leave a comment

Questions:

I have a window with a QGraphicsScene as painter, and i want to render its elements to a pdf file on press of a button.

def generateReport(self):
        lineList = {}
        for i in self.circleList:
            for j,k in i.lineItems:
                if j not in lineList:
                    lineList[j] = [i, k]
        printed = QPdfWriter("Output.pdf")
        printed.setPageSize(QPagedPaintDevice.A4)
        printer = QPainter(printed)
        self.painter.render(printer)
        for i,j in enumerate(lineList):
            # j.scene().render(printer)
            # lineList[j][0].scene().render(printer)
            # lineList[j][1].scene().render(printer)
            printer.drawText(0, self.painter.height() + i*200, f'{j.nameItem.toPlainText()}: {lineList[j][0].m_items[4].toPlainText()}, {lineList[j][1].m_items[4].toPlainText()}')
        printer.end()

nameItem on j is the name label for the line, m_items[4] is the name label for each circle.

My issue is that i cant seem to get the exact height of the rendered scene, moreover I have zero clue as to how i could overflow the text to the next page should the contents not fit in one.

it would be lovely if i could somehow render every line and its corresponding circles seperately for each connection, stored in lineList

note: the line is a child of every circle , and the names of every line and circle are children of theirs, implemented much in the same way as the answer to my previous question where in lies my final issue of the grip items also being rendered.

I have discovered that I can create a new scene, move each item one by one and render it out to the pdf but this raises two separate issues

  1. I cant add a line break and avoid overdrawing the new render over the previous one, and
  2. I cant position the text as addText doesnt take positional arguments.

MRE:

import random
from fbs_runtime.application_context.PyQt5 import ApplicationContext
from PyQt5.QtCore import Qt
from PyQt5.QtGui import QPainter, QPdfWriter, QBrush, QPagedPaintDevice
from PyQt5.QtWidgets import (QDialog, QGraphicsScene,
                             QGraphicsView, QGridLayout,
                             QPushButton, QGraphicsEllipseItem)

class gui(QDialog):
    def __init__(self, parent=None):
        super(gui, self).__init__(parent)
        self.resize(1280, 720)
        self.painter = QGraphicsScene(0, 0, self.width() - 50, self.height() - 70)
        self.painter.setBackgroundBrush(QBrush(Qt.white))
        self.canvas = QGraphicsView(self.painter)
        mainLayout = QGridLayout()
        mainLayout.addWidget(self.canvas, 0, 0, -1, -1)
        self.setLayout(mainLayout)

    @property
    def circleList(self):
        return [item for item in self.painter.items() if isinstance(item, QGraphicsEllipseItem)]

    def newCircle(self):
        self.painter.addEllipse( random.randint(100, 400), random.randint(100, 400), 50 + random.random() * 200, 50 + random.random() * 200)

    def generateReport(self):
        printed = QPdfWriter("Output.pdf")
        printed.setPageSize(QPagedPaintDevice.A4)
        printer = QPainter(printed)
        self.painter.render(printer)
        for i,j in enumerate(self.circleList):
            printer.drawText(0, printer.viewport().height() + i*200, 'test')
        printer.end()


if __name__ == "__main__":
    app = ApplicationContext()
    test = gui()
    test.newCircle()
    test.newCircle()
    test.newCircle()
    test.generateReport()
    test.show()
    exit(app.app.exec_())

if possible , the ability to print, test then circle for all circles would be decent enough for me.

Incorrect output example:

enter image description here

How to&Answers:

To understand what painting is like, you have to understand how QGraphicsScene::render() method works:

void QGraphicsScene::render(QPainter *painter, const QRectF &target = QRectF(), const QRectF &source = QRectF(), Qt::AspectRatioMode aspectRatioMode = Qt::KeepAspectRatio)

Renders the source rect from scene into target, using painter. This
function is useful for capturing the contents of the scene onto a
paint device, such as a QImage (e.g., to take a screenshot), or for
printing with QPrinter. For example:

QGraphicsScene scene;
scene.addItem(...
...
QPrinter printer(QPrinter::HighResolution);
printer.setPaperSize(QPrinter::A4);

QPainter painter(&printer);
scene.render(&painter);

If source is a null rect, this function will use sceneRect() to
determine what to render. If target is a null rect, the dimensions of
painter’s paint device will be used.

The source rect contents will be transformed according to
aspectRatioMode to fit into the target rect. By default, the aspect
ratio is kept, and source is scaled to fit in target.

See also QGraphicsView::render().

In your case, if the source is not passed, the entire sceneRect (0, 0, 1230, 650) will be copied and painted on the pdf page, if the sizes do not match, the sizes will be scaled. So from the above it follows that if you want to print an item then you must pass as source the space it occupies in the scene and hide the other items, and the target is the place where you want to paint, which involves calculating the new position based on where the previous item was printed.

Considering the above, a possible solution is the following:

import random

from PyQt5 import QtCore, QtGui, QtWidgets


class Gui(QtWidgets.QDialog):
    def __init__(self, parent=None):
        super(Gui, self).__init__(parent)
        self.resize(1280, 720)
        self.scene = QtWidgets.QGraphicsScene(
            0, 0, self.width() - 50, self.height() - 70
        )
        self.scene.setBackgroundBrush(QtGui.QBrush(QtCore.Qt.white))
        self.canvas = QtWidgets.QGraphicsView(self.scene)
        mainLayout = QtWidgets.QGridLayout(self)
        mainLayout.addWidget(self.canvas)

    @property
    def circleList(self):
        return [
            item
            for item in self.scene.items()
            if isinstance(item, QtWidgets.QGraphicsEllipseItem)
        ]

    def newCircle(self):
        self.scene.addEllipse(
            random.randint(100, 400),
            random.randint(100, 400),
            50 + random.random() * 200,
            50 + random.random() * 200,
        )

    def generateReport(self):
        printer = QtGui.QPdfWriter("Output.pdf")
        printer.setPageSize(QtGui.QPagedPaintDevice.A4)
        printer.setResolution(100)
        painter = QtGui.QPainter(printer)
        delta = 20
        f = painter.font()
        f.setPixelSize(delta)
        painter.setFont(f)
        # hide all items
        last_states = []
        for item in self.scene.items():
            last_states.append(item.isVisible())
            item.setVisible(False)

        target = QtCore.QRectF(0, 0, printer.width(), 0)

        for i, item in enumerate(self.circleList):
            item.setVisible(True)
            source = item.mapToScene(item.boundingRect()).boundingRect()
            target.setHeight(source.height())
            if target.bottom() > printer.height():
                printer.newPage()
                target.moveTop(0)
            self.scene.render(painter, target, source)
            f = painter.font()
            f.setPixelSize(delta)
            painter.drawText(
                QtCore.QRectF(
                    target.bottomLeft(), QtCore.QSizeF(printer.width(), delta + 5)
                ),
                "test",
            )
            item.setVisible(False)
            target.setTop(target.bottom() + delta + 20)
        # restore visibility
        for item, state in zip(self.scene.items(), last_states):
            item.setVisible(state)
        painter.end()


if __name__ == "__main__":
    import sys

    app = QtWidgets.QApplication(sys.argv)
    w = Gui()
    for _ in range(200):
        w.newCircle()
    w.generateReport()
    w.show()
    sys.exit(app.exec_())