A project I’ve been working on recently needs to generate pdfs from user created content. There are a few Python libraries for pdf creation (PyPDF2, pdfkit, ReportLab etc…), but none seemed to be the king. I needed to have a high amount of stylistic control for my project and ReportLab looked like it gave you the most control.
The downside to ReportLab is that it was founded in 2000 and lacks documentation up to today’s standards. There are not too many blog posts on it, it uses camel case (non-standard for python) as you’ll see in some example, and British spellings for some terms. Hence I’m hoping this post acts a resource for people playing around with ReportLab.
Here are some resources I found useful:
Onto using ReportLab! You can install it with pip install reportlab . Luckily they’ve created some high level components for us to use in our pdf contained in a module called platypus. I’m going to import SimpleDocTemplate from it.
from reportlab.platypus import SimpleDocTemplate
SimpleDocTemplate takes a buffer as a required argument. A buffer here is acting as a place to output the the contents of the file as it’s being written. For example you can point it to a filename e.g.
my_doc = SimpleDocTemplate('myfile.pdf')
This will write to a file called myfile.pdf . Or if you want to hold onto it in python, you could also use a BytesIO buffer.
from io import BytesIOpdf_buffer = BytesIO()
my_doc = SimpleDocTemplate(pdf_buffer)
In this case, we can hold onto my_doc and doing things like return it in a request later.
Now if we just wanted to create the file, or add to the pdf_buffer , we could use the build method on SimpleDocTemplate .
flowables = []my_doc.build(flowables)
Think of flowables as a list of elements/content that we want to add to the pdf. It’s a required argument for build. In this case we haven’t added any content yet, so it’ll output an empty array.
Let’s start to add some basic text to our pdf! Again, we’re going to use classes that already exist in platypus, Paragraph and PageBreak .
A Paragraph takes two arguments, text and style. There’s not a whole lot of info on styles, so I’d suggest digging around with ipdb . However, there is a function that builds sample styles for us, getSampleStyleSheet which you can then modify.
from reportlab.lib.styles import getSampleStyleSheetsample_style_sheet = getSampleStyleSheet()# if you want to see all the sample styles, this prints them
sample_style_sheet.list()
We have some styles now. We can reference them like such: sample_style_sheet['BodyText'] or sample_style_sheet['Heading1'] . To see all the options use sample_style_sheet.list() .
Back to creating our paragraph and adding it to flowables:
from reportlab.platypus import Paragraphparagraph_1 = Paragraph("A title", sample_style_sheet['Heading1'])
paragraph_2 = Paragraph(
"Some normal body text",
sample_style_sheet['BodyText']
)flowables.append(paragraph_1)
flowables.append(paragraph_2)
Now that we have flowables, let’s build!
my_doc.build(flowables)
If you were using a file as the buffer, you should be able to open it and see the title and body outputted like such.
You can iterate through content and add paragraphs, headers etc. as you’d like. Another platypus object I found useful was PageBreak , which starts a new page, something that might be useful for a new chapter.
from reportlab.platypus import PageBreakflowables.append(PageBreak())
A specific requirement I had was the page size of the pdf. Luckily this is parameter you can pass in as a tuple to the pagesize argument, but you’ll have to multiply by a measurement (e.g. mm or inch ) constant that they provide. Here is how a custom sized pdf looks.
from reportlab.lib.units import mm, inchpagesize = (140 * mm, 216 * mm) # width, heightmy_doc = SimpleDocTemplate(
pdf_buffer,
pagesize=pagesize
)
If you want non-default margins, you can also pass those in with topMargin , leftMargin , rightMargin , and bottomMargin .
my_doc = SimpleDocTemplate(
pdf_buffer,
pagesize=pagesize,
topMargin=1*inch,
leftMargin=1*inch,
rightMargin=1*inch,
bottomMargin=1*inch
)
There are a number of fonts that come with ReportLab. You can see this by calling getAvailableFonts your doc’s canvas object.
my_doc.canv.getAvailableFonts()
Note: If you want to include your own, you have to register and embed them, but I won’t go into that now.
How do we apply these to our text? We can modify some of those sample styles that we had got with getSampleStyleSheet previously and use those when we create our Paragraph . For example:
custom_body_style = sample_style_sheet['BodyText']
custom_body_style.fontName = 'ZapfDingbats'
custom_body_style.fontSize = 25paragraph_3 = Paragraph("Dingbat paragraph", custom_body_style)
flowables.append(paragraph_3)
To show all the style’s attributes use listAttrs like such custom_body_style.listAttrs() and that will give you a sense of what’s modifiable.
To add bold, italics, underlines, and linebreaks, ReportLab appears to take accept HTML-like markup tags as RML. You could wrap some text as such.
Paragraph("A bold word.
An italic word.", custom_body_style)
I also wanted to add page numbers at the bottom of the pages. This can be done via the build method with the optional arguments: onFirstPage and onLaterPages . These take a callback with the pdf’s canvas and doc passed in. I’ve written one called add_page_number , which draws a string with the page number on each page.
def add_page_number(canvas, doc):
canvas.saveState()
canvas.setFont('Times-Roman', 10)
page_number_text = "%d" % (doc.page)
canvas.drawCentredString(
0.75 * inch,
0.75 * inch,
page_number_text
)
canvas.restoreState()
Since I wanted numbers on all the pages, my build call looked something like this:
my_doc.build(
flowables,
onFirstPage=add_page_number,
onLaterPages=add_page_number,
)
Django’s docs show you how to return a pdf. In our case, we can just use the buffer that we created, pdf_buffer .
def view_that_returns_pdf(request):
. # all the other stuff pdf_value = pdf_buffer.getvalue()
pdf_buffer.close() response = HttpResponse(content_type='application/pdf')
response['Content-Disposition'] = 'attachment; filename="some_file.pdf"'
response.write(pdf_value)
return response
I’m not a fan of the documentation that’s out there, but as with most things you just have to dig around. There’s a lot more power to ReportLab, which I haven’t fully uncovered yet. The canvas grants a significant amount of control, so I’d suggest looking into that for more complex pdfs.
from io import BytesIO
from reportlab.platypus import SimpleDocTemplate, Paragraph, PageBreak
from reportlab.lib.styles import getSampleStyleSheet
from reportlab.lib.units import mm, inchPAGESIZE = (140 * mm, 216 * mm)
BASE_MARGIN = 5 * mm
class PdfCreator: def add_page_number(self, canvas, doc):
canvas.saveState()
canvas.setFont('Times-Roman', 10)
page_number_text = "%d" % (doc.page)
canvas.drawCentredString(
0.75 * inch,
0.75 * inch,
page_number_text
)
canvas.restoreState() def get_body_style(self):
sample_style_sheet = getSampleStyleSheet()
body_style = sample_style_sheet['BodyText']
body_style.fontSize = 18
return body_style def build_pdf(self):
pdf_buffer = BytesIO()
my_doc = SimpleDocTemplate(
pdf_buffer,
pagesize=PAGESIZE,
topMargin=BASE_MARGIN,
leftMargin=BASE_MARGIN,
rightMargin=BASE_MARGIN,
bottomMargin=BASE_MARGIN
) body_style = self.get_body_style() flowables = [
Paragraph("First paragraph", body_style),
Paragraph("Second paragraph", body_style)
] my_doc.build(
flowables,
onFirstPage=self.add_page_number,
onLaterPages=self.add_page_number,
) pdf_value = pdf_buffer.getvalue()
pdf_buffer.close()
return pdf_value