If your job involves generating PDF reports, invoices, etc. you have probably thought about automating that with Python. Python has some great libraries for working with PDF files, allowing you to read and write PDFs from scripts. But you can also use these libraries as the basic of simple GUI tools, giving you an easy way to auto-fill or edit PDF reports on the desktop.
In this tutorial we'll be using two libraries to create a custom PDF report filler. The data will be collected using a Qt form: just edit the fields, press "Generate" to get the filled out form in the folder. The two libraries we'll be using here are --
- reportlab which allows you to create PDFs using text and drawing primitives
- pdfrw a library for reading and extracting pages from existing PDFs
While we could use reportlab to draw the entire PDF, it's easier to design a template using external tools and then simply overlay the dynamic content on this. We can use pdfrw
to read our template PDF and then extract a page, onto which we can then draw using reportlab
. That allows us to overlay custom information (from our app) directly onto an existing PDF template, which we save under a new name.
In this example we're entering the fields manually, but you can modify the application to read the data for the PDF from an external CSV file & generate multiple PDFs from it.
Template PDF
For testing I've created a custom TPS report template using Google Docs and downloaded the page as PDF. The page contains a number of fields which are to be filled. In this tutorial, we'll write a PyQt form which a user can fill in and then write that data out onto the PDF at the correct place.
The template is in A4 format. Save it in the same folder as your script.
Create GUI Applications with Python & Qt5 by Martin Fitzpatrick — (PySide2 Edition) The hands-on guide to making apps with Python — Over 10,000 copies sold!
If you have another template you'd prefer to use, feel free to use that. Just remember that you'll need to adjust the positions of the form fields when writing it.
Laying out the Form view
Qt includes a QFormLayout
layout which simplifies the process of generating simple form layouts. It works similarly to a grid, but you can add rows of elements together and strings are converted automatically to QLabel
objects. Our skeleton application, including the full layout matching the template form (more or less) is shown below.
- PyQt5
- PySide2
- PyQt6
- PySide6
from PyQt5.QtWidgets import QPushButton, QLineEdit, QApplication, QFormLayout, QWidget, QTextEdit, QSpinBox
class Window(QWidget):
def __init__(self):
super().__init__()
self.name = QLineEdit()
self.program_type = QLineEdit()
self.product_code = QLineEdit()
self.customer = QLineEdit()
self.vendor = QLineEdit()
self.n_errors = QSpinBox()
self.n_errors.setRange(0, 1000)
self.comments = QTextEdit()
self.generate_btn = QPushButton("Generate PDF")
layout = QFormLayout()
layout.addRow("Name", self.name)
layout.addRow("Program Type", self.program_type)
layout.addRow("Product Code", self.product_code)
layout.addRow("Customer", self.customer)
layout.addRow("Vendor", self.vendor)
layout.addRow("No. of Errors", self.n_errors)
layout.addRow("Comments", self.comments)
layout.addRow(self.generate_btn)
self.setLayout(layout)
app = QApplication([])
w = Window()
w.show()
app.exec()
from PySide2.QtWidgets import QPushButton, QLineEdit, QApplication, QFormLayout, QWidget, QTextEdit, QSpinBox
class Window(QWidget):
def __init__(self):
super().__init__()
self.name = QLineEdit()
self.program_type = QLineEdit()
self.product_code = QLineEdit()
self.customer = QLineEdit()
self.vendor = QLineEdit()
self.n_errors = QSpinBox()
self.n_errors.setRange(0, 1000)
self.comments = QTextEdit()
self.generate_btn = QPushButton("Generate PDF")
layout = QFormLayout()
layout.addRow("Name", self.name)
layout.addRow("Program Type", self.program_type)
layout.addRow("Product Code", self.product_code)
layout.addRow("Customer", self.customer)
layout.addRow("Vendor", self.vendor)
layout.addRow("No. of Errors", self.n_errors)
layout.addRow("Comments", self.comments)
layout.addRow(self.generate_btn)
self.setLayout(layout)
app = QApplication([])
w = Window()
w.show()
app.exec_()
from PyQt6.QtWidgets import QPushButton, QLineEdit, QApplication, QFormLayout, QWidget, QTextEdit, QSpinBox
class Window(QWidget):
def __init__(self):
super().__init__()
self.name = QLineEdit()
self.program_type = QLineEdit()
self.product_code = QLineEdit()
self.customer = QLineEdit()
self.vendor = QLineEdit()
self.n_errors = QSpinBox()
self.n_errors.setRange(0, 1000)
self.comments = QTextEdit()
self.generate_btn = QPushButton("Generate PDF")
layout = QFormLayout()
layout.addRow("Name", self.name)
layout.addRow("Program Type", self.program_type)
layout.addRow("Product Code", self.product_code)
layout.addRow("Customer", self.customer)
layout.addRow("Vendor", self.vendor)
layout.addRow("No. of Errors", self.n_errors)
layout.addRow("Comments", self.comments)
layout.addRow(self.generate_btn)
self.setLayout(layout)
app = QApplication([])
w = Window()
w.show()
app.exec()
from PySide6.QtWidgets import QPushButton, QLineEdit, QApplication, QFormLayout, QWidget, QTextEdit, QSpinBox
class Window(QWidget):
def __init__(self):
super().__init__()
self.name = QLineEdit()
self.program_type = QLineEdit()
self.product_code = QLineEdit()
self.customer = QLineEdit()
self.vendor = QLineEdit()
self.n_errors = QSpinBox()
self.n_errors.setRange(0, 1000)
self.comments = QTextEdit()
self.generate_btn = QPushButton("Generate PDF")
layout = QFormLayout()
layout.addRow("Name", self.name)
layout.addRow("Program Type", self.program_type)
layout.addRow("Product Code", self.product_code)
layout.addRow("Customer", self.customer)
layout.addRow("Vendor", self.vendor)
layout.addRow("No. of Errors", self.n_errors)
layout.addRow("Comments", self.comments)
layout.addRow(self.generate_btn)
self.setLayout(layout)
app = QApplication([])
w = Window()
w.show()
app.exec_()
When writing tools to replace/automate paper forms, it's usually a good idea to try and mimic the layout of the paper form so it's familiar.
The above will give us the following layout in a window when run. You can already type things into the fields, but pressing the button won't do anything yet -- we haven't written the code to generate the PDF or hooked it up to the button.
Generating a PDF
For PDF generation using a base template, we'll be combining reportlab
and PdfReader
. The process is as follows --
- Read in the
template.pdf
file usingPdfReader
, and extract the first page only. - Create a reportlab
Canvas
object - Use
pdfrw.toreportlab.makerl
to generate a canvas object then add it to the Canvas withcanvas.doForm()
- Draw out custom bits on the Canvas
- Save the PDF to file
The code is shown below, this doesn't require Qt, you can save to a file and run as-is. When run the resulting PDF will be saved as result.pdf
in the same folder.
from reportlab.pdfgen.canvas import Canvas
from pdfrw import PdfReader
from pdfrw.buildxobj import pagexobj
from pdfrw.toreportlab import makerl
outfile = "result.pdf"
template = PdfReader("template.pdf", decompress=False).pages[0]
template_obj = pagexobj(template)
canvas = Canvas(outfile)
xobj_name = makerl(canvas, template_obj)
canvas.doForm(xobj_name)
ystart = 443
# Prepared by
canvas.drawString(170, ystart, "My name here")
canvas.save()
Since the process of generating the PDF is doing IO, it may take some time (e.g. if we loading files off network drives). Because of this, it is better to handle this in a separate thread. We'll define this custom thread runner next.
Running the generation in a separate thread
Since each generation is an isolated job, it makes sense to use Qt's QRunner
framework to handle the process -- this also makes it simple later to for example add customizable templates per job. We're using the same approach seen in the Multithreading tutorial where we use a subclass of QRunner
to hold our custom run code, and implement runner-specific signals on a separate subclass of QObject
.
- PyQt5
- PySide2
- PyQt6
- PySide6
from PyQt5.QtWidgets import QPushButton, QLineEdit, QApplication, QFormLayout, QWidget, QTextEdit, QMessageBox, QSpinBox
from PyQt5.QtCore import QObject, QRunnable, QThreadPool, pyqtSignal, pyqtSlot
from reportlab.pdfgen.canvas import Canvas
from pdfrw import PdfReader
from pdfrw.buildxobj import pagexobj
from pdfrw.toreportlab import makerl
class WorkerSignals(QObject):
"""
Defines the signals available from a running worker thread.
"""
error = pyqtSignal(str)
file_saved_as = pyqtSignal(str)
class Generator(QRunnable):
"""
Worker thread
Inherits from QRunnable to handle worker thread setup, signals
and wrap-up.
:param data: The data to add to the PDF for generating.
"""
def __init__(self, data):
super().__init__()
self.data = data
self.signals = WorkerSignals()
@pyqtSlot()
def run(self):
try:
outfile = "result.pdf"
template = PdfReader("template.pdf", decompress=False).pages[0]
template_obj = pagexobj(template)
canvas = Canvas(outfile)
xobj_name = makerl(canvas, template_obj)
canvas.doForm(xobj_name)
ystart = 443
# Prepared by
canvas.drawString(170, ystart, self.data['name'])
canvas.save()
except Exception as e:
self.signals.error.emit(str(e))
return
self.signals.file_saved_as.emit(outfile)
from PySide2.QtWidgets import QPushButton, QLineEdit, QApplication, QFormLayout, QWidget, QTextEdit, QMessageBox, QSpinBox
from PySide2.QtCore import QObject, QRunnable, QThreadPool, Signal, Slot
from reportlab.pdfgen.canvas import Canvas
from pdfrw import PdfReader
from pdfrw.buildxobj import pagexobj
from pdfrw.toreportlab import makerl
class WorkerSignals(QObject):
"""
Defines the signals available from a running worker thread.
"""
error = Signal(str)
file_saved_as = Signal(str)
class Generator(QRunnable):
"""
Worker thread
Inherits from QRunnable to handle worker thread setup, signals
and wrap-up.
:param data: The data to add to the PDF for generating.
"""
def __init__(self, data):
super().__init__()
self.data = data
self.signals = WorkerSignals()
@Slot()
def run(self):
try:
outfile = "result.pdf"
template = PdfReader("template.pdf", decompress=False).pages[0]
template_obj = pagexobj(template)
canvas = Canvas(outfile)
xobj_name = makerl(canvas, template_obj)
canvas.doForm(xobj_name)
ystart = 443
# Prepared by
canvas.drawString(170, ystart, self.data['name'])
canvas.save()
except Exception as e:
self.signals.error.emit(str(e))
return
self.signals.file_saved_as.emit(outfile)
from PyQt6.QtWidgets import QPushButton, QLineEdit, QApplication, QFormLayout, QWidget, QTextEdit, QMessageBox, QSpinBox
from PyQt6.QtCore import QObject, QRunnable, QThreadPool, pyqtSignal, pyqtSlot
from reportlab.pdfgen.canvas import Canvas
from pdfrw import PdfReader
from pdfrw.buildxobj import pagexobj
from pdfrw.toreportlab import makerl
class WorkerSignals(QObject):
"""
Defines the signals available from a running worker thread.
"""
error = pyqtSignal(str)
file_saved_as = pyqtSignal(str)
class Generator(QRunnable):
"""
Worker thread
Inherits from QRunnable to handle worker thread setup, signals
and wrap-up.
:param data: The data to add to the PDF for generating.
"""
def __init__(self, data):
super().__init__()
self.data = data
self.signals = WorkerSignals()
@pyqtSlot()
def run(self):
try:
outfile = "result.pdf"
template = PdfReader("template.pdf", decompress=False).pages[0]
template_obj = pagexobj(template)
canvas = Canvas(outfile)
xobj_name = makerl(canvas, template_obj)
canvas.doForm(xobj_name)
ystart = 443
# Prepared by
canvas.drawString(170, ystart, self.data['name'])
canvas.save()
except Exception as e:
self.signals.error.emit(str(e))
return
self.signals.file_saved_as.emit(outfile)
from PySide6.QtWidgets import QPushButton, QLineEdit, QApplication, QFormLayout, QWidget, QTextEdit, QMessageBox, QSpinBox
from PySide6.QtCore import QObject, QRunnable, QThreadPool, Signal, Slot
from reportlab.pdfgen.canvas import Canvas
from pdfrw import PdfReader
from pdfrw.buildxobj import pagexobj
from pdfrw.toreportlab import makerl
class WorkerSignals(QObject):
"""
Defines the signals available from a running worker thread.
"""
error = Signal(str)
file_saved_as = Signal(str)
class Generator(QRunnable):
"""
Worker thread
Inherits from QRunnable to handle worker thread setup, signals
and wrap-up.
:param data: The data to add to the PDF for generating.
"""
def __init__(self, data):
super().__init__()
self.data = data
self.signals = WorkerSignals()
@Slot()
def run(self):
try:
outfile = "result.pdf"
template = PdfReader("template.pdf", decompress=False).pages[0]
template_obj = pagexobj(template)
canvas = Canvas(outfile)
xobj_name = makerl(canvas, template_obj)
canvas.doForm(xobj_name)
ystart = 443
# Prepared by
canvas.drawString(170, ystart, self.data['name'])
canvas.save()
except Exception as e:
self.signals.error.emit(str(e))
return
self.signals.file_saved_as.emit(outfile)
We've defined two signals here:
file_saved_as
which emits the filename of the saved PDF file (on success)error
which emits errors as a string for debugging
We need a QThreadPool
to add run our custom runner on. We can add this onto our MainWindow
in the __init__
block.
class Window(QWidget):
def __init__(self):
super().__init__()
self.threadpool = QThreadPool()
Now we have the generator QRunner
defined, we just need to implement the generate
method to create the runner, pass it the data from our form fields and the start the generation running.
def generate(self):
self.generate_btn.setDisabled(True)
data = {
'name': self.name.text(),
'program_type': self.program_type.text(),
'product_code': self.product_code.text(),
'customer': self.customer.text(),
'vendor': self.vendor.text(),
'n_errors': str(self.n_errors.value()),
'comments': self.comments.toPlainText()
}
g = Generator(data)
g.signals.file_saved_as.connect(self.generated)
g.signals.error.connect(print) # Print errors to console.
self.threadpool.start(g)
def generated(self, outfile):
pass28
In this code we first disable the generate_btn
so the user can't press the button multiple times while the generation is taking place. We then construct a dictionary of data from our widgets, using the .text()
method to get the text from QLineEdit
widgets, .value()
to get the value from the QSpinBox
and .toPlainText()
to get the plain text representation of the QTextEdit
. We convert the numeric value to a string, since we are placing text.
To actually generate the PDF we create an instance of the Generator
runner we just defined, passing in the dictionary of data. We connect the file_saved_as
signal to our generated
method (defined at the bottom, but not doing anything yet) and the error
signal to the standard Python print
function: this will automatically print any errors to the console.
Finally, we take our Generator
instance and pass it to our threadpool's .start()
method to queue it to run (it should start immediately).
We can then hook this method up to our button in the __init__
of our main window e.g.
self.generate_btn.pressed.connect(self.generate)
If you run the app now, pressing the button will trigger the generation of the PDF and the result will be saved as result.pdf
in the same folder as you started the app.
So far we've only placed a single block of text on the page, so let's complete the generator to write all our fields in the correct place.
Completing the generator
Next we need to finish the text placement on the template. The trick here is to work out what the per-line spacing is for your template (depends on the font size etc.) and then calculate positions relative to the first line. The y coordinates increase up the page (so 0,0 is the bottom left) so in our code before, we define the ystart
for the top line and then subtract 28 for each line.
ystart = 443
# Prepared by
canvas.drawString(170, ystart, self.data['name'])
# Date: Todays date
today = datetime.today()
canvas.drawString(410, ystart, today.strftime('%F'))
# Device/Program Type
canvas.drawString(230, ystart-28, self.data['program_type'])
# Product code
canvas.drawString(175, ystart-(2*28), self.data['product_code'])
# Customer
canvas.drawString(315, ystart-(2*28), self.data['customer'])
# Vendor
canvas.drawString(145, ystart-(3*28), self.data['vendor'])
ystart = 250
# Program Language
canvas.drawString(210, ystart, "Python")
canvas.drawString(430, ystart, self.data['n_errors'])
Wrapping
For most of our form fields we can just output the text as-is, since there are no line breaks. If the text entered is too long, then it will overflow -- but if we wanted we can limit this on the fields themselves by setting a max length in characters, e.g.
field.setMaxLength(25)
For the comments field, things are a little more tricky. The field can be much longer, and lines need to be wrapped over multiple lines in the template. The field also accepts line breaks (by pressing Enter) which cause problems when written out to the PDF.
As you can see in the above screenshot, the line breaks appear as black squares in the text. The good news is that just removing the line breaks will make it easier to wrap: we can just wrap each line to a specified number of characters.
Since the characters are variable width this isn't perfect, but it shouldn't matter. If we wrap for a line-full of the widest characters (W) any real line will fit.
Python comes with the textwrap
library built in, which we can use to wrap our text, once we've stripped the newlines.
import textwrap
comments = comments.replace('\n', ' ')
lines = textwrap.wrap(comments, width=80)
But we need to account for the first line being shorter, which we can do by wrapping first to the shorter length, re-joining the remainder, and re-wrapping it, e.g.
import textwrap
comments = comments.replace('\n', ' ')
lines = textwrap.wrap(comments, width=65) # 45
first_line = lines[0]
remainder = ' '.join(lines[1:])
lines = textwrap.wrap(remainder, 75) # 55
lines = lines[:4] # max lines, not including the first.
The comment markers on the wrap lines (45 & 55) show the wrap length needed to fit a line of Ws into the space. This is the shortest possible line, but not realistic. The values used should work with most normal text.
To do this properly we should calculate the actual size of each length of text in the document font and use that to inform the wrapper.
Once we have the lines prepared, we can print them onto the PDF by iterating through the list and decrementing the y position for each time. The spacing between the lines in our template document is 28.
comments = self.data['comments'].replace('\n', ' ')
if comments:
lines = textwrap.wrap(comments, width=65) # 45
first_line = lines[0]
remainder = ' '.join(lines[1:])
lines = textwrap.wrap(remainder, 75) # 55
lines = lines[:4] # max lines, not including the first.
canvas.drawString(155, 223, first_line)
for n, l in enumerate(lines, 1):
canvas.drawString(80, 223 - (n*28), l)
This gives the following result with some sample lorem ipsum text.
Automatically showing the result
When the file is created our runner returns the filename of the created file in a signal (currently it is always the same). It would be nice to present the resulting PDF to the user automatically, so they can check if everything looks good. On Windows we can use os.startfile
to open a file with the default launcher for that type -- in this case opening the PDF with the default PDF viewer.
Since this isn't available on other platforms, we catch the error and instead show a QMessageBox
def generated(self, outfile):
self.generate_btn.setDisabled(False)
try:
os.startfile(outfile)
except Exception:
# If startfile not available, show dialog.
QMessageBox.information(self, "Finished", "PDF has been generated")
Complete code
The complete code for PyQt5, PySide2, PyQt6 or PySide6 is shown below.
- PyQt5
- PySide2
- PyQt6
- PySide6
from PyQt5.QtWidgets import QPushButton, QLineEdit, QApplication, QFormLayout, QWidget, QTextEdit, QMessageBox, QSpinBox
from PyQt5.QtCore import QObject, QRunnable, QThreadPool, pyqtSignal, pyqtSlot
from reportlab.pdfgen.canvas import Canvas
import os
import textwrap
from datetime import datetime
from pdfrw import PdfReader
from pdfrw.buildxobj import pagexobj
from pdfrw.toreportlab import makerl
class WorkerSignals(QObject):
"""
Defines the signals available from a running worker thread.
"""
error = pyqtSignal(str)
file_saved_as = pyqtSignal(str)
class Generator(QRunnable):
"""
Worker thread
Inherits from QRunnable to handle worker thread setup, signals
and wrap-up.
:param data: The data to add to the PDF for generating.
"""
def __init__(self, data):
super().__init__()
self.data = data
self.signals = WorkerSignals()
@pyqtSlot()
def run(self):
try:
outfile = "result.pdf"
template = PdfReader("template.pdf", decompress=False).pages[0]
template_obj = pagexobj(template)
canvas = Canvas(outfile)
xobj_name = makerl(canvas, template_obj)
canvas.doForm(xobj_name)
ystart = 443
# Prepared by
canvas.drawString(170, ystart, self.data['name'])
# Date: Todays date
today = datetime.today()
canvas.drawString(410, ystart, today.strftime('%F'))
# Device/Program Type
canvas.drawString(230, ystart-28, self.data['program_type'])
# Product code
canvas.drawString(175, ystart-(2*28), self.data['product_code'])
# Customer
canvas.drawString(315, ystart-(2*28), self.data['customer'])
# Vendor
canvas.drawString(145, ystart-(3*28), self.data['vendor'])
ystart = 250
# Program Language
canvas.drawString(210, ystart, "Python")
canvas.drawString(430, ystart, self.data['n_errors'])
comments = self.data['comments'].replace('\n', ' ')
if comments:
lines = textwrap.wrap(comments, width=65) # 45
first_line = lines[0]
remainder = ' '.join(lines[1:])
lines = textwrap.wrap(remainder, 75) # 55
lines = lines[:4] # max lines, not including the first.
canvas.drawString(155, 223, first_line)
for n, l in enumerate(lines, 1):
canvas.drawString(80, 223 - (n*28), l)
canvas.save()
except Exception as e:
self.signals.error.emit(str(e))
return
self.signals.file_saved_as.emit(outfile)
class Window(QWidget):
def __init__(self):
super().__init__()
self.threadpool = QThreadPool()
self.name = QLineEdit()
self.program_type = QLineEdit()
self.product_code = QLineEdit()
self.customer = QLineEdit()
self.vendor = QLineEdit()
self.n_errors = QSpinBox()
self.n_errors.setRange(0, 1000)
self.comments = QTextEdit()
self.generate_btn = QPushButton("Generate PDF")
self.generate_btn.pressed.connect(self.generate)
layout = QFormLayout()
layout.addRow("Name", self.name)
layout.addRow("Program Type", self.program_type)
layout.addRow("Product Code", self.product_code)
layout.addRow("Customer", self.customer)
layout.addRow("Vendor", self.vendor)
layout.addRow("No. of Errors", self.n_errors)
layout.addRow("Comments", self.comments)
layout.addRow(self.generate_btn)
self.setLayout(layout)
def generate(self):
self.generate_btn.setDisabled(True)
data = {
'name': self.name.text(),
'program_type': self.program_type.text(),
'product_code': self.product_code.text(),
'customer': self.customer.text(),
'vendor': self.vendor.text(),
'n_errors': str(self.n_errors.value()),
'comments': self.comments.toPlainText()
}
g = Generator(data)
g.signals.file_saved_as.connect(self.generated)
g.signals.error.connect(print) # Print errors to console.
self.threadpool.start(g)
def generated(self, outfile):
self.generate_btn.setDisabled(False)
try:
os.startfile(outfile)
except Exception:
# If startfile not available, show dialog.
QMessageBox.information(self, "Finished", "PDF has been generated")
app = QApplication([])
w = Window()
w.show()
app.exec_()
from PySide2.QtWidgets import QPushButton, QLineEdit, QApplication, QFormLayout, QWidget, QTextEdit, QMessageBox, QSpinBox
from PySide2.QtCore import QObject, QRunnable, QThreadPool, Signal, Slot
from reportlab.pdfgen.canvas import Canvas
import os
import textwrap
from datetime import datetime
from pdfrw import PdfReader
from pdfrw.buildxobj import pagexobj
from pdfrw.toreportlab import makerl
class WorkerSignals(QObject):
"""
Defines the signals available from a running worker thread.
"""
error = Signal(str)
file_saved_as = Signal(str)
class Generator(QRunnable):
"""
Worker thread
Inherits from QRunnable to handle worker thread setup, signals
and wrap-up.
:param data: The data to add to the PDF for generating.
"""
def __init__(self, data):
super().__init__()
self.data = data
self.signals = WorkerSignals()
@Slot()
def run(self):
try:
outfile = "result.pdf"
template = PdfReader("template.pdf", decompress=False).pages[0]
template_obj = pagexobj(template)
canvas = Canvas(outfile)
xobj_name = makerl(canvas, template_obj)
canvas.doForm(xobj_name)
ystart = 443
# Prepared by
canvas.drawString(170, ystart, self.data['name'])
# Date: Todays date
today = datetime.today()
canvas.drawString(410, ystart, today.strftime('%F'))
# Device/Program Type
canvas.drawString(230, ystart-28, self.data['program_type'])
# Product code
canvas.drawString(175, ystart-(2*28), self.data['product_code'])
# Customer
canvas.drawString(315, ystart-(2*28), self.data['customer'])
# Vendor
canvas.drawString(145, ystart-(3*28), self.data['vendor'])
ystart = 250
# Program Language
canvas.drawString(210, ystart, "Python")
canvas.drawString(430, ystart, self.data['n_errors'])
comments = self.data['comments'].replace('\n', ' ')
if comments:
lines = textwrap.wrap(comments, width=65) # 45
first_line = lines[0]
remainder = ' '.join(lines[1:])
lines = textwrap.wrap(remainder, 75) # 55
lines = lines[:4] # max lines, not including the first.
canvas.drawString(155, 223, first_line)
for n, l in enumerate(lines, 1):
canvas.drawString(80, 223 - (n*28), l)
canvas.save()
except Exception as e:
self.signals.error.emit(str(e))
return
self.signals.file_saved_as.emit(outfile)
class Window(QWidget):
def __init__(self):
super().__init__()
self.threadpool = QThreadPool()
self.name = QLineEdit()
self.program_type = QLineEdit()
self.product_code = QLineEdit()
self.customer = QLineEdit()
self.vendor = QLineEdit()
self.n_errors = QSpinBox()
self.n_errors.setRange(0, 1000)
self.comments = QTextEdit()
self.generate_btn = QPushButton("Generate PDF")
self.generate_btn.pressed.connect(self.generate)
layout = QFormLayout()
layout.addRow("Name", self.name)
layout.addRow("Program Type", self.program_type)
layout.addRow("Product Code", self.product_code)
layout.addRow("Customer", self.customer)
layout.addRow("Vendor", self.vendor)
layout.addRow("No. of Errors", self.n_errors)
layout.addRow("Comments", self.comments)
layout.addRow(self.generate_btn)
self.setLayout(layout)
def generate(self):
self.generate_btn.setDisabled(True)
data = {
'name': self.name.text(),
'program_type': self.program_type.text(),
'product_code': self.product_code.text(),
'customer': self.customer.text(),
'vendor': self.vendor.text(),
'n_errors': str(self.n_errors.value()),
'comments': self.comments.toPlainText()
}
g = Generator(data)
g.signals.file_saved_as.connect(self.generated)
g.signals.error.connect(print) # Print errors to console.
self.threadpool.start(g)
def generated(self, outfile):
self.generate_btn.setDisabled(False)
try:
os.startfile(outfile)
except Exception:
# If startfile not available, show dialog.
QMessageBox.information(self, "Finished", "PDF has been generated")
app = QApplication([])
w = Window()
w.show()
app.exec_()
from PyQt6.QtWidgets import QPushButton, QLineEdit, QApplication, QFormLayout, QWidget, QTextEdit, QMessageBox, QSpinBox
from PyQt6.QtCore import QObject, QRunnable, QThreadPool, pyqtSignal, pyqtSlot
from reportlab.pdfgen.canvas import Canvas
import os
import textwrap
from datetime import datetime
from pdfrw import PdfReader
from pdfrw.buildxobj import pagexobj
from pdfrw.toreportlab import makerl
class WorkerSignals(QObject):
"""
Defines the signals available from a running worker thread.
"""
error = pyqtSignal(str)
file_saved_as = pyqtSignal(str)
class Generator(QRunnable):
"""
Worker thread
Inherits from QRunnable to handle worker thread setup, signals
and wrap-up.
:param data:The data to add to the PDF for generating.
"""
def __init__(self, data):
super().__init__()
self.data = data
self.signals = WorkerSignals()
@pyqtSlot()
def run(self):
try:
outfile = "result.pdf"
template = PdfReader("template.pdf", decompress=False).pages[0]
template_obj = pagexobj(template)
canvas = Canvas(outfile)
xobj_name = makerl(canvas, template_obj)
canvas.doForm(xobj_name)
ystart = 443
# Prepared by
canvas.drawString(170, ystart, self.data['name'])
# Date: Todays date
today = datetime.today()
canvas.drawString(410, ystart, today.strftime('%F'))
# Device/Program Type
canvas.drawString(230, ystart-28, self.data['program_type'])
# Product code
canvas.drawString(175, ystart-(2*28), self.data['product_code'])
# Customer
canvas.drawString(315, ystart-(2*28), self.data['customer'])
# Vendor
canvas.drawString(145, ystart-(3*28), self.data['vendor'])
ystart = 250
# Program Language
canvas.drawString(210, ystart, "Python")
canvas.drawString(430, ystart, self.data['n_errors'])
comments = self.data['comments'].replace('\n', ' ')
if comments:
lines = textwrap.wrap(comments, width=65) # 45
first_line = lines[0]
remainder = ' '.join(lines[1:])
lines = textwrap.wrap(remainder, 75) # 55
lines = lines[:4] # max lines, not including the first.
canvas.drawString(155, 223, first_line)
for n, l in enumerate(lines, 1):
canvas.drawString(80, 223 - (n*28), l)
canvas.save()
except Exception as e:
self.signals.error.emit(str(e))
return
self.signals.file_saved_as.emit(outfile)
class Window(QWidget):
def __init__(self):
super().__init__()
self.threadpool = QThreadPool()
self.name = QLineEdit()
self.program_type = QLineEdit()
self.product_code = QLineEdit()
self.customer = QLineEdit()
self.vendor = QLineEdit()
self.n_errors = QSpinBox()
self.n_errors.setRange(0, 1000)
self.comments = QTextEdit()
self.generate_btn = QPushButton("Generate PDF")
self.generate_btn.pressed.connect(self.generate)
layout = QFormLayout()
layout.addRow("Name", self.name)
layout.addRow("Program Type", self.program_type)
layout.addRow("Product Code", self.product_code)
layout.addRow("Customer", self.customer)
layout.addRow("Vendor", self.vendor)
layout.addRow("No. of Errors", self.n_errors)
layout.addRow("Comments", self.comments)
layout.addRow(self.generate_btn)
self.setLayout(layout)
def generate(self):
self.generate_btn.setDisabled(True)
data = {
'name': self.name.text(),
'program_type': self.program_type.text(),
'product_code': self.product_code.text(),
'customer': self.customer.text(),
'vendor': self.vendor.text(),
'n_errors': str(self.n_errors.value()),
'comments': self.comments.toPlainText()
}
g = Generator(data)
g.signals.file_saved_as.connect(self.generated)
g.signals.error.connect(print) # Print errors to console.
self.threadpool.start(g)
def generated(self, outfile):
self.generate_btn.setDisabled(False)
try:
os.startfile(outfile)
except Exception:
# If startfile not available, show dialog.
QMessageBox.information(self, "Finished", "PDF has been generated")
app = QApplication([])
w = Window()
w.show()
app.exec()
from PySide6.QtWidgets import QPushButton, QLineEdit, QApplication, QFormLayout, QWidget, QTextEdit, QMessageBox, QSpinBox
from PySide6.QtCore import QObject, QRunnable, QThreadPool, Signal, Slot
from reportlab.pdfgen.canvas import Canvas
import os
import textwrap
from datetime import datetime
from pdfrw import PdfReader
from pdfrw.buildxobj import pagexobj
from pdfrw.toreportlab import makerl
class WorkerSignals(QObject):
"""
Defines the signals available from a running worker thread.
"""
error = Signal(str)
file_saved_as = Signal(str)
class Generator(QRunnable):
"""
Worker thread
Inherits from QRunnable to handle worker thread setup, signals
and wrap-up.
:param data:The data to add to the PDF for generating.
"""
def __init__(self, data):
super().__init__()
self.data = data
self.signals = WorkerSignals()
@Slot()
def run(self):
try:
outfile = "result.pdf"
template = PdfReader("template.pdf", decompress=False).pages[0]
template_obj = pagexobj(template)
canvas = Canvas(outfile)
xobj_name = makerl(canvas, template_obj)
canvas.doForm(xobj_name)
ystart = 443
# Prepared by
canvas.drawString(170, ystart, self.data['name'])
# Date: Todays date
today = datetime.today()
canvas.drawString(410, ystart, today.strftime('%F'))
# Device/Program Type
canvas.drawString(230, ystart-28, self.data['program_type'])
# Product code
canvas.drawString(175, ystart-(2*28), self.data['product_code'])
# Customer
canvas.drawString(315, ystart-(2*28), self.data['customer'])
# Vendor
canvas.drawString(145, ystart-(3*28), self.data['vendor'])
ystart = 250
# Program Language
canvas.drawString(210, ystart, "Python")
canvas.drawString(430, ystart, self.data['n_errors'])
comments = self.data['comments'].replace('\n', ' ')
if comments:
lines = textwrap.wrap(comments, width=65) # 45
first_line = lines[0]
remainder = ' '.join(lines[1:])
lines = textwrap.wrap(remainder, 75) # 55
lines = lines[:4] # max lines, not including the first.
canvas.drawString(155, 223, first_line)
for n, l in enumerate(lines, 1):
canvas.drawString(80, 223 - (n*28), l)
canvas.save()
except Exception as e:
self.signals.error.emit(str(e))
return
self.signals.file_saved_as.emit(outfile)
class Window(QWidget):
def __init__(self):
super().__init__()
self.threadpool = QThreadPool()
self.name = QLineEdit()
self.program_type = QLineEdit()
self.product_code = QLineEdit()
self.customer = QLineEdit()
self.vendor = QLineEdit()
self.n_errors = QSpinBox()
self.n_errors.setRange(0, 1000)
self.comments = QTextEdit()
self.generate_btn = QPushButton("Generate PDF")
self.generate_btn.pressed.connect(self.generate)
layout = QFormLayout()
layout.addRow("Name", self.name)
layout.addRow("Program Type", self.program_type)
layout.addRow("Product Code", self.product_code)
layout.addRow("Customer", self.customer)
layout.addRow("Vendor", self.vendor)
layout.addRow("No. of Errors", self.n_errors)
layout.addRow("Comments", self.comments)
layout.addRow(self.generate_btn)
self.setLayout(layout)
def generate(self):
self.generate_btn.setDisabled(True)
data = {
'name': self.name.text(),
'program_type': self.program_type.text(),
'product_code': self.product_code.text(),
'customer': self.customer.text(),
'vendor': self.vendor.text(),
'n_errors': str(self.n_errors.value()),
'comments': self.comments.toPlainText()
}
g = Generator(data)
g.signals.file_saved_as.connect(self.generated)
g.signals.error.connect(print) # Print errors to console.
self.threadpool.start(g)
def generated(self, outfile):
self.generate_btn.setDisabled(False)
try:
os.startfile(outfile)
except Exception:
# If startfile not available, show dialog.
QMessageBox.information(self, "Finished", "PDF has been generated")
app = QApplication([])
w = Window()
w.show()
app.exec_()
Generating from a CSV file
In the above example you need to type the data to fill in manually. This is fine if you don't have a lot of PDFs to generate, but not so much fun if you have an entire CSV file worth of data to generate reports for. In the example below, rather than present a list of form fields to the user we just ask for a source CSV file from which PDFs can be generated -- each row in the file generates a separate PDF file using the data in the file.
- PyQt5
- PySide2
- PyQt6
- PySide6
from PyQt5.QtWidgets import QPushButton, QLineEdit, QApplication, QFormLayout, QWidget, QTextEdit, QMessageBox, QSpinBox, QFileDialog
from PyQt5.QtCore import QObject, QRunnable, QThreadPool, pyqtSignal, pyqtSlot
from reportlab.pdfgen.canvas import Canvas
import os, csv
import textwrap
from datetime import datetime
from pdfrw import PdfReader
from pdfrw.buildxobj import pagexobj
from pdfrw.toreportlab import makerl
class WorkerSignals(QObject):
"""
Defines the signals available from a running worker thread.
"""
error = pyqtSignal(str)
finished = pyqtSignal()
class Generator(QRunnable):
"""
Worker thread
Inherits from QRunnable to handle worker thread setup, signals
and wrap-up.
:param data: The data to add to the PDF for generating.
"""
def __init__(self, data):
super().__init__()
self.data = data
self.signals = WorkerSignals()
@pyqtSlot()
def run(self):
try:
filename, _ = os.path.splitext(self.data['sourcefile'])
folder = os.path.dirname(self.data['sourcefile'])
template = PdfReader("template.pdf", decompress=False).pages[0]
template_obj = pagexobj(template)
with open(self.data['sourcefile'], 'r', newline='') as f:
reader = csv.DictReader(f)
for n, row in enumerate(reader, 1):
fn = f'{filename}-{n}.pdf'
outfile = os.path.join(folder, fn)
canvas = Canvas(outfile)
xobj_name = makerl(canvas, template_obj)
canvas.doForm(xobj_name)
ystart = 443
# Prepared by
canvas.drawString(170, ystart, row.get('name', ''))
# Date: Todays date
today = datetime.today()
canvas.drawString(410, ystart, today.strftime('%F'))
# Device/Program Type
canvas.drawString(230, ystart-28, row.get('program_type', ''))
# Product code
canvas.drawString(175, ystart-(2*28), row.get('product_code', ''))
# Customer
canvas.drawString(315, ystart-(2*28), row.get('customer', ''))
# Vendor
canvas.drawString(145, ystart-(3*28), row.get('vendor', ''))
ystart = 250
# Program Language
canvas.drawString(210, ystart, "Python")
canvas.drawString(430, ystart, row.get('n_errors', ''))
comments = row.get('comments', '').replace('\n', ' ')
if comments:
lines = textwrap.wrap(comments, width=65) # 45
first_line = lines[0]
remainder = ' '.join(lines[1:])
lines = textwrap.wrap(remainder, 75) # 55
lines = lines[:4] # max lines, not including the first.
canvas.drawString(155, 223, first_line)
for n, l in enumerate(lines, 1):
canvas.drawString(80, 223 - (n*28), l)
canvas.save()
except Exception as e:
self.signals.error.emit(str(e))
return
self.signals.finished.emit()
class Window(QWidget):
def __init__(self):
super().__init__()
self.threadpool = QThreadPool()
self.sourcefile = QLineEdit()
self.sourcefile.setDisabled(True) # must use the file finder to select a valid file.
self.file_select = QPushButton("Select CSV...")
self.file_select.pressed.connect(self.choose_csv_file)
self.generate_btn = QPushButton("Generate PDF")
self.generate_btn.pressed.connect(self.generate)
layout = QFormLayout()
layout.addRow(self.sourcefile, self.file_select)
layout.addRow(self.generate_btn)
self.setLayout(layout)
def choose_csv_file(self):
filename, _ = QFileDialog.getOpenFileName(self, "Select a file", filter="CSV files (*.csv)")
if filename:
self.sourcefile.setText(filename)
def generate(self):
if not self.sourcefile.text():
return # If the field is empty, ignore.
self.generate_btn.setDisabled(True)
data = {
'sourcefile': self.sourcefile.text(),
}
g = Generator(data)
g.signals.finished.connect(self.generated)
g.signals.error.connect(print) # Print errors to console.
self.threadpool.start(g)
def generated(self):
self.generate_btn.setDisabled(False)
QMessageBox.information(self, "Finished", "PDFs have been generated")
app = QApplication([])
w = Window()
w.show()
app.exec()
from PySide2.QtWidgets import QPushButton, QLineEdit, QApplication, QFormLayout, QWidget, QTextEdit, QMessageBox, QSpinBox, QFileDialog
from PySide2.QtCore import QObject, QRunnable, QThreadPool, Signal, Slot
from reportlab.pdfgen.canvas import Canvas
import os, csv
import textwrap
from datetime import datetime
from pdfrw import PdfReader
from pdfrw.buildxobj import pagexobj
from pdfrw.toreportlab import makerl
class WorkerSignals(QObject):
"""
Defines the signals available from a running worker thread.
"""
error = Signal(str)
finished = Signal()
class Generator(QRunnable):
"""
Worker thread
Inherits from QRunnable to handle worker thread setup, signals
and wrap-up.
:param data: The data to add to the PDF for generating.
"""
def __init__(self, data):
super().__init__()
self.data = data
self.signals = WorkerSignals()
@Slot()
def run(self):
try:
filename, _ = os.path.splitext(self.data['sourcefile'])
folder = os.path.dirname(self.data['sourcefile'])
template = PdfReader("template.pdf", decompress=False).pages[0]
template_obj = pagexobj(template)
with open(self.data['sourcefile'], 'r', newline='') as f:
reader = csv.DictReader(f)
for n, row in enumerate(reader, 1):
fn = f'{filename}-{n}.pdf'
outfile = os.path.join(folder, fn)
canvas = Canvas(outfile)
xobj_name = makerl(canvas, template_obj)
canvas.doForm(xobj_name)
ystart = 443
# Prepared by
canvas.drawString(170, ystart, row.get('name', ''))
# Date: Todays date
today = datetime.today()
canvas.drawString(410, ystart, today.strftime('%F'))
# Device/Program Type
canvas.drawString(230, ystart-28, row.get('program_type', ''))
# Product code
canvas.drawString(175, ystart-(2*28), row.get('product_code', ''))
# Customer
canvas.drawString(315, ystart-(2*28), row.get('customer', ''))
# Vendor
canvas.drawString(145, ystart-(3*28), row.get('vendor', ''))
ystart = 250
# Program Language
canvas.drawString(210, ystart, "Python")
canvas.drawString(430, ystart, row.get('n_errors', ''))
comments = row.get('comments', '').replace('\n', ' ')
if comments:
lines = textwrap.wrap(comments, width=65) # 45
first_line = lines[0]
remainder = ' '.join(lines[1:])
lines = textwrap.wrap(remainder, 75) # 55
lines = lines[:4] # max lines, not including the first.
canvas.drawString(155, 223, first_line)
for n, l in enumerate(lines, 1):
canvas.drawString(80, 223 - (n*28), l)
canvas.save()
except Exception as e:
self.signals.error.emit(str(e))
return
self.signals.finished.emit()
class Window(QWidget):
def __init__(self):
super().__init__()
self.threadpool = QThreadPool()
self.sourcefile = QLineEdit()
self.sourcefile.setDisabled(True) # must use the file finder to select a valid file.
self.file_select = QPushButton("Select CSV...")
self.file_select.pressed.connect(self.choose_csv_file)
self.generate_btn = QPushButton("Generate PDF")
self.generate_btn.pressed.connect(self.generate)
layout = QFormLayout()
layout.addRow(self.sourcefile, self.file_select)
layout.addRow(self.generate_btn)
self.setLayout(layout)
def choose_csv_file(self):
filename, _ = QFileDialog.getOpenFileName(self, "Select a file", filter="CSV files (*.csv)")
if filename:
self.sourcefile.setText(filename)
def generate(self):
if not self.sourcefile.text():
return # If the field is empty, ignore.
self.generate_btn.setDisabled(True)
data = {
'sourcefile': self.sourcefile.text(),
}
g = Generator(data)
g.signals.finished.connect(self.generated)
g.signals.error.connect(print) # Print errors to console.
self.threadpool.start(g)
def generated(self):
self.generate_btn.setDisabled(False)
QMessageBox.information(self, "Finished", "PDFs have been generated")
app = QApplication([])
w = Window()
w.show()
app.exec_()
from PyQt6.QtWidgets import QPushButton, QLineEdit, QApplication, QFormLayout, QWidget, QTextEdit, QMessageBox, QSpinBox, QFileDialog
from PyQt6.QtCore import QObject, QRunnable, QThreadPool, pyqtSignal, pyqtSlot
from reportlab.pdfgen.canvas import Canvas
import os, csv
import textwrap
from datetime import datetime
from pdfrw import PdfReader
from pdfrw.buildxobj import pagexobj
from pdfrw.toreportlab import makerl
class WorkerSignals(QObject):
"""
Defines the signals available from a running worker thread.
"""
error = pyqtSignal(str)
finished = pyqtSignal()
class Generator(QRunnable):
"""
Worker thread
Inherits from QRunnable to handle worker thread setup, signals
and wrap-up.
:param data: The data to add to the PDF for generating.
"""
def __init__(self, data):
super().__init__()
self.data = data
self.signals = WorkerSignals()
@pyqtSlot()
def run(self):
try:
filename, _ = os.path.splitext(self.data['sourcefile'])
folder = os.path.dirname(self.data['sourcefile'])
template = PdfReader("template.pdf", decompress=False).pages[0]
template_obj = pagexobj(template)
with open(self.data['sourcefile'], 'r', newline='') as f:
reader = csv.DictReader(f)
for n, row in enumerate(reader, 1):
fn = f'{filename}-{n}.pdf'
outfile = os.path.join(folder, fn)
canvas = Canvas(outfile)
xobj_name = makerl(canvas, template_obj)
canvas.doForm(xobj_name)
ystart = 443
# Prepared by
canvas.drawString(170, ystart, row.get('name', ''))
# Date: Todays date
today = datetime.today()
canvas.drawString(410, ystart, today.strftime('%F'))
# Device/Program Type
canvas.drawString(230, ystart-28, row.get('program_type', ''))
# Product code
canvas.drawString(175, ystart-(2*28), row.get('product_code', ''))
# Customer
canvas.drawString(315, ystart-(2*28), row.get('customer', ''))
# Vendor
canvas.drawString(145, ystart-(3*28), row.get('vendor', ''))
ystart = 250
# Program Language
canvas.drawString(210, ystart, "Python")
canvas.drawString(430, ystart, row.get('n_errors', ''))
comments = row.get('comments', '').replace('\n', ' ')
if comments:
lines = textwrap.wrap(comments, width=65) # 45
first_line = lines[0]
remainder = ' '.join(lines[1:])
lines = textwrap.wrap(remainder, 75) # 55
lines = lines[:4] # max lines, not including the first.
canvas.drawString(155, 223, first_line)
for n, l in enumerate(lines, 1):
canvas.drawString(80, 223 - (n*28), l)
canvas.save()
except Exception as e:
self.signals.error.emit(str(e))
return
self.signals.finished.emit()
class Window(QWidget):
def __init__(self):
super().__init__()
self.threadpool = QThreadPool()
self.sourcefile = QLineEdit()
self.sourcefile.setDisabled(True) # must use the file finder to select a valid file.
self.file_select = QPushButton("Select CSV...")
self.file_select.pressed.connect(self.choose_csv_file)
self.generate_btn = QPushButton("Generate PDF")
self.generate_btn.pressed.connect(self.generate)
layout = QFormLayout()
layout.addRow(self.sourcefile, self.file_select)
layout.addRow(self.generate_btn)
self.setLayout(layout)
def choose_csv_file(self):
filename, _ = QFileDialog.getOpenFileName(self, "Select a file", filter="CSV files (*.csv)")
if filename:
self.sourcefile.setText(filename)
def generate(self):
if not self.sourcefile.text():
return # If the field is empty, ignore.
self.generate_btn.setDisabled(True)
data = {
'sourcefile': self.sourcefile.text(),
}
g = Generator(data)
g.signals.finished.connect(self.generated)
g.signals.error.connect(print) # Print errors to console.
self.threadpool.start(g)
def generated(self):
self.generate_btn.setDisabled(False)
QMessageBox.information(self, "Finished", "PDFs have been generated")
app = QApplication([])
w = Window()
w.show()
app.exec()
from PySide6.QtWidgets import QPushButton, QLineEdit, QApplication, QFormLayout, QWidget, QTextEdit, QMessageBox, QSpinBox, QFileDialog
from PySide6.QtCore import QObject, QRunnable, QThreadPool, Signal, Slot
from reportlab.pdfgen.canvas import Canvas
import os, csv
import textwrap
from datetime import datetime
from pdfrw import PdfReader
from pdfrw.buildxobj import pagexobj
from pdfrw.toreportlab import makerl
class WorkerSignals(QObject):
"""
Defines the signals available from a running worker thread.
"""
error = Signal(str)
finished = Signal()
class Generator(QRunnable):
"""
Worker thread
Inherits from QRunnable to handle worker thread setup, signals
and wrap-up.
:param data: The data to add to the PDF for generating.
"""
def __init__(self, data):
super().__init__()
self.data = data
self.signals = WorkerSignals()
@Slot()
def run(self):
try:
filename, _ = os.path.splitext(self.data['sourcefile'])
folder = os.path.dirname(self.data['sourcefile'])
template = PdfReader("template.pdf", decompress=False).pages[0]
template_obj = pagexobj(template)
with open(self.data['sourcefile'], 'r', newline='') as f:
reader = csv.DictReader(f)
for n, row in enumerate(reader, 1):
fn = f'{filename}-{n}.pdf'
outfile = os.path.join(folder, fn)
canvas = Canvas(outfile)
xobj_name = makerl(canvas, template_obj)
canvas.doForm(xobj_name)
ystart = 443
# Prepared by
canvas.drawString(170, ystart, row.get('name', ''))
# Date: Todays date
today = datetime.today()
canvas.drawString(410, ystart, today.strftime('%F'))
# Device/Program Type
canvas.drawString(230, ystart-28, row.get('program_type', ''))
# Product code
canvas.drawString(175, ystart-(2*28), row.get('product_code', ''))
# Customer
canvas.drawString(315, ystart-(2*28), row.get('customer', ''))
# Vendor
canvas.drawString(145, ystart-(3*28), row.get('vendor', ''))
ystart = 250
# Program Language
canvas.drawString(210, ystart, "Python")
canvas.drawString(430, ystart, row.get('n_errors', ''))
comments = row.get('comments', '').replace('\n', ' ')
if comments:
lines = textwrap.wrap(comments, width=65) # 45
first_line = lines[0]
remainder = ' '.join(lines[1:])
lines = textwrap.wrap(remainder, 75) # 55
lines = lines[:4] # max lines, not including the first.
canvas.drawString(155, 223, first_line)
for n, l in enumerate(lines, 1):
canvas.drawString(80, 223 - (n*28), l)
canvas.save()
except Exception as e:
self.signals.error.emit(str(e))
return
self.signals.finished.emit()
class Window(QWidget):
def __init__(self):
super().__init__()
self.threadpool = QThreadPool()
self.sourcefile = QLineEdit()
self.sourcefile.setDisabled(True) # must use the file finder to select a valid file.
self.file_select = QPushButton("Select CSV...")
self.file_select.pressed.connect(self.choose_csv_file)
self.generate_btn = QPushButton("Generate PDF")
self.generate_btn.pressed.connect(self.generate)
layout = QFormLayout()
layout.addRow(self.sourcefile, self.file_select)
layout.addRow(self.generate_btn)
self.setLayout(layout)
def choose_csv_file(self):
filename, _ = QFileDialog.getOpenFileName(self, "Select a file", filter="CSV files (*.csv)")
if filename:
self.sourcefile.setText(filename)
def generate(self):
if not self.sourcefile.text():
return # If the field is empty, ignore.
self.generate_btn.setDisabled(True)
data = {
'sourcefile': self.sourcefile.text(),
}
g = Generator(data)
g.signals.finished.connect(self.generated)
g.signals.error.connect(print) # Print errors to console.
self.threadpool.start(g)
def generated(self):
self.generate_btn.setDisabled(False)
QMessageBox.information(self, "Finished", "PDFs have been generated")
app = QApplication([])
w = Window()
w.show()
app.exec_()
You can run this app using the template.pdf
and this example CSV file to generate a few TPS reports.
Things to notice --
- We now generate multiple files, so it doesn't make much sense to open them when they're finished. Instead, we always show the "complete" message, and only once. The signal
file_saved_as
has been renamed tofinished
and we've removed the filenamestr
since it's no longer used. - The
QLineEdit
to get the filename is disabled so it's not possible to edit directly: the only way to set a source CSV file is to select the file directly, ensuring it's there. - We auto-generate the output filenames, based on the import filename and the current row number. The
filename
is taken from the input CSV: with a CSV namedtps.csv
files will be namedtps-1.pdf
,tps-2.pdf
etc. Files are written out to the folder the source CSV is in. - Since some rows/files might miss required fields, we use
.get()
on the row dictionary with a default empty string.
Possible improvements
If you feel like improving on this code, there are a few things you could try
- Make the template and output file location configurable -- use a Qt file dialogs
- Load the field positions from a file alongside the template (JSON) so you can use the same form with multiple templates
- Make the fields configurable -- this gets quite tricky, but you particular types (
str
,datetime
,int
, etc.) can have specific widgets assigned to them