Booklet page layout with ReportLab - python

I have a Python program that generates a PDF file with ReportLab, and I'd like to generate another PDF that is formatted to have each page cut in half, then folded and stapled together into a booklet.
For example, if my current document contains pages A, B, C, D, E, F, G, and H, I want the new document to have two pages that I can print double sided to come out like this:
BG|HA
DE|FC
I have seen this order referred to as 4-up or imposition.
My printer has an option for printing four pages to each sheet, but it doesn't reorder the pages. If I print the current document with that setting and double sided, it comes out like this:
AB|EF
CD|GH
My first preference is to generate a PDF with four pages per sheet as shown. If I can't figure that out, the next best thing would be to generate a PDF with reordered pages so that my printer can print them four pages to each sheet. In other words, reorder the pages to B, G, D, E, H, A, F, and C.
Here's a code example that prints eight pages:
from subprocess import call
from reportlab.lib import pagesizes
from reportlab.lib.styles import getSampleStyleSheet
from reportlab.lib.units import inch
from reportlab.platypus import SimpleDocTemplate, Paragraph
def main():
pdf_path = 'booklet.pdf'
doc = SimpleDocTemplate(pdf_path,
pagesize=pagesizes.letter,
topMargin=0.625*inch,
bottomMargin=0.625*inch)
styles = getSampleStyleSheet()
paragraph_style = styles['Normal']
print(paragraph_style.fontSize, paragraph_style.leading)
paragraph_style.fontSize = 300
paragraph_style.leading = 360
story = []
for text in 'ABCDEFGH':
flowable = Paragraph(text, paragraph_style)
story.append(flowable)
doc.build(story)
# call(["evince", pdf_path]) # launch PDF viewer
main()

Thanks to this question, I could see how to collect canvas pages before actually writing them into the document. I added some code to reorder them, and now I can print them with my extra printer option for 4 pages to each sheet.
from subprocess import call
from reportlab.lib import pagesizes
from reportlab.lib.styles import getSampleStyleSheet
from reportlab.lib.units import inch
from reportlab.pdfgen.canvas import Canvas
from reportlab.platypus import SimpleDocTemplate, Paragraph
class BookletCanvas(Canvas):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.pages = []
def showPage(self):
self.pages.append(dict(self.__dict__))
self._startPage()
def save(self):
while len(self.pages) % 8 != 0:
self.showPage()
original_pages = self.pages[:]
reordered_pages = []
while original_pages:
reordered_pages.append(original_pages.pop(1))
reordered_pages.append(original_pages.pop(-2))
reordered_pages.append(original_pages.pop(2))
reordered_pages.append(original_pages.pop(-3))
reordered_pages.append(original_pages.pop(-1))
reordered_pages.append(original_pages.pop(0))
reordered_pages.append(original_pages.pop(-1))
reordered_pages.append(original_pages.pop(0))
for page in reordered_pages:
self.__dict__.update(page)
super().showPage()
super().save()
def main():
pdf_path = 'booklet.pdf'
doc = SimpleDocTemplate(pdf_path,
pagesize=pagesizes.letter,
topMargin=0.625*inch,
bottomMargin=0.625*inch)
styles = getSampleStyleSheet()
paragraph_style = styles['Normal']
print(paragraph_style.fontSize, paragraph_style.leading)
paragraph_style.fontSize = 300
paragraph_style.leading = 360
story = []
for text in 'ABCDEFGH':
flowable = Paragraph(text, paragraph_style)
story.append(flowable)
doc.build(story, canvasmaker=BookletCanvas)
# call(["evince", pdf_path]) # launch PDF viewer
main()
I'd still prefer to do the 4 pages to each sheet inside the PDF, but this will do for now.

Related

Pass id parameter into imported class in Django

In Django I have a function based view responsible of printing the details (actually only the name) of all the registered users on a pdf file.
def test_pdf(request, id):
# Create the HttpResponse object with the appropriate PDF headers.
response = HttpResponse(content_type='application/pdf')
response['Content-Disposition'] = 'attachment; filename="My Users.pdf"'
buffer = io.BytesIO()
report = MyPrint(buffer, 'Letter', id)
pdf = report.print_users()
response.write(pdf)
return response
This function works because I imported in the views.py file a class I built in another file, responsible of drawing the pdf, MyPrint:
from reportlab.lib.pagesizes import letter, A4
from reportlab.platypus import SimpleDocTemplate, Paragraph
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
from reportlab.lib.enums import TA_CENTER
from django.contrib.auth.models import User
class MyPrint:
def __init__(self, buffer, pagesize):
self.buffer = buffer
if pagesize == 'A4':
self.pagesize = A4
elif pagesize == 'Letter':
self.pagesize = letter
self.width, self.height = self.pagesize
def print_users(self):
buffer = self.buffer
doc = SimpleDocTemplate(buffer,
rightMargin=72,
leftMargin=72,
topMargin=72,
bottomMargin=72,
pagesize=self.pagesize)
# Our container for 'Flowable' objects
elements = []
# A large collection of style sheets pre-made for us
styles = getSampleStyleSheet()
styles.add(ParagraphStyle(name='centered', alignment=TA_CENTER))
# Draw things on the PDF. Here's where the PDF generation happens.
# See the ReportLab documentation for the full list of functionality.
users = User.objects.all()
elements.append(Paragraph('My User Names', styles['Heading1']))
for i, user in enumerate(users):
elements.append(Paragraph(user.get_full_name(), styles['Normal']))
doc.build(elements)
# Get the value of the BytesIO buffer and write it to the response.
pdf = buffer.getvalue()
buffer.close()
return pdf
Now, How can I make the function and the class specific to a user if I pass in the relative pk into the function? Apart from updating the urlpattern, should I pass the id into the class and / or into the function?
If you want to have the existing function work with one or more users, and continue to work if you don't pass in an id, I think the simplest way of changing it would be as follows:
def print_users(self, id=None):
buffer = self.buffer
doc = SimpleDocTemplate(buffer,
rightMargin=72,
leftMargin=72,
topMargin=72,
bottomMargin=72,
pagesize=self.pagesize)
# Our container for 'Flowable' objects
elements = []
# A large collection of style sheets pre-made for us
styles = getSampleStyleSheet()
styles.add(ParagraphStyle(name='centered', alignment=TA_CENTER))
# Draw things on the PDF. Here's where the PDF generation happens.
# See the ReportLab documentation for the full list of functionality.
users = User.objects.all()
if id:
users = users.filter(id__in=id)
elements.append(Paragraph('My User Names', styles['Heading1']))
for i, user in enumerate(users):
elements.append(Paragraph(user.get_full_name(), styles['Normal']))
doc.build(elements)
# Get the value of the BytesIO buffer and write it to the response.
pdf = buffer.getvalue()
buffer.close()
return pdf
Then change how you call it to:
report = MyPrint(buffer, 'Letter')
pdf = report.print_users(id)
or, if you want to print all users, just call it as:
report = MyPrint(buffer, 'Letter')
pdf = report.print_users()

Python reportLab platypus: bottom image

I'm working with the reportlab library and I have doubts about adding an image using a SimpleDocTemplate.
I have dynamic content and I don't know how much space it occupies. What happens is that I want to add a logo at the bottom of the page (always in the same place). The way I'm doing it is to add things to a list: e.g [text, spacer, table, spacer, logo] and then build it. The place of the logo depends on other variables.
Could you help me to accomplish this behavior?
I know that this can be done using absolute positioning (e.g using drawImage in a canvas class) but I don't know how to combine the way I'm doing it with this.
Thanks in advance
The chances are you want to put the image in a footer (closer to axel_ande's answer). That way the image will go in the same place on every page, but only be defined once.
If you want to put an image at the bottom of a page but not in the footer, you could try the TopPadder wrapper object:
from reportlab.platypus.doctemplate import SimpleDocTemplate
from reportlab.platypus.flowables import TopPadder
from reportlab.platypus import Table, Paragraph
from reportlab.lib.styles import getSampleStyleSheet
from reportlab.lib import colors
import numpy as np
document = SimpleDocTemplate('padding_test.pdf')
table = Table(np.random.rand(2,2).tolist(),
style=[('GRID', (0, 0), (-1, -1), 0.5, colors.black)])
styles = getSampleStyleSheet()
paragraph = Paragraph('Some paragraphs', style=styles['Normal'])
document.build([
paragraph,
TopPadder(table),
])
# This should generate a single pdf page with text at the top and a table at the bottom.
I stumbled across this when looking through the code, the only documentation I could find on it was in the release notes. In my example I wrap a table simply so the example code is stand-alone.
Hope this helps!
I got a heading for the reports that I generate that I produce like this code (where the PageTemplate generates the heading for each paper.
from reportlab.platypus import Table, TableStyle, Paragraph
from reportlab.platypus.frames import Frame
from reportlab.platypus.doctemplate import PageTemplate, BaseDocTemplate
class MyDocTemplate(BaseDocTemplate):
def __init__(self, filename, tr, param1, param2, plugin_dir, **kw):
self.allowSplitting = 0
BaseDocTemplate.__init__(self, filename, **kw)
self.tr = tr
self.plugin_dir = plugin_dir
frame = Frame(self.leftMargin, self.bottomMargin, self.width, self.height - 2 * cm, id='normal')
template = PageTemplate(id='test', frames=frame, onPage=partial(self.header, param1=param1, param2=param2))
self.addPageTemplates(template)
def header(self, canvas, doc, param1, param2):
canvas.saveState()
canvas.drawString(30, 750, self.tr('Simple report from GeoDataFarm'))
canvas.drawString(30, 733, self.tr('For the growing season of ') + str(param1))
canvas.drawImage(self.plugin_dir + '\\img\\icon.png', 500, 765, width=50, height=50)
canvas.drawString(500, 750, 'Generated:')
canvas.drawString(500, 733, param2)
canvas.line(30, 723, 580, 723)
#w, h = content.wrap(doc.width, doc.topMargin)
#content.drawOn(canvas, doc.leftMargin, doc.height + doc.topMargin - h)
canvas.restoreState()
doc = MyDocTemplate(report_name, self.tr, self.plugin_dir, '2018', '2018-09-21')
story = []
data_tbl = [['col1', 'col2'],[1, 2],[3, 4]]
table = Table(data_tbl, repeatRows=1, hAlign='LEFT', colWidths=[380/l_heading] * l_heading)
table.setStyle(TableStyle([('FONTSIZE', (0, 0), (l_heading, 0), 16)]))
story.append(table)
doc.build(story)

Is there a way to speed up PDF page merging (basically watermarking one with the other), when the base page is used repeatedly?

Clarification: I don't want to add pages to a PDF file. I want to add content to a very big PDF page. The page changes sometimes and the content is different every time.
I'm using pypdf2 and reportlab to make small additions to big PDF pages (~10MB). This takes 30 seconds and more and the majority of that time is spend parsing the original.
Usually the page also needs to be turned using mergeRotatedTranslatedPage.
My idea was to generate the content array of the original once and then copy it every time I want to add something. So I modified PageObject._merge to do just that. It worked... kind of. I'm now down to 18 sec.
Is there a better way to speed up this process? 18 sec for one page is still pretty slow.
If you want to use 100% capacity of all the cores of your processor, you can do it with "multiprocessing", as follows:
We count the number of pages in the PDF and the number of nuclei that your processor has, in order to calculate how many pages have to work each nucleus has.
The pages that must work are sent to each nucleus and at the end all the PDF's are joined.
# -*- coding: utf-8 -*-
from io import BytesIO
from PyPDF2 import PdfFileWriter, PdfFileReader, PdfFileMerger
from reportlab.pdfgen import canvas
from reportlab.lib.pagesizes import A4
from reportlab.lib.colors import Color
from webcolors import hex_to_rgb
import base64
import multiprocessing
import math
class SkyMark:
self.mainPdf=''
self.mask=''
#beginning, call from your api rest y pass request
def begin(self,request):
stringPdfBase64 = str(request.json['pdfBase4'])
blob = self.workMultiprocess(stringPdfBase64,'MyWaterMark text')
pdfbase64 = blob.decode('utf-8')
return pdfbase64
def workMultiprocess(self,stringPdfBase64,message='SkyMark'):
try:
#get pdf PdfFileReader objeto with your message
self.mask = self.getMaskWaterMark(message)
#Convert main pdfB64 to PdfFileReader object
sanitizedPdf = stringPdfBase64.rsplit(',', 1)[-1]
data = base64.b64decode(sanitizedPdf)
stream = BytesIO(data)
self.mainPdf = PdfFileReader(stream , strict=False)
numPaginas = self.mainPdf .getNumPages()
#count your cores of your processor
coresProcessor = int( multiprocessing.cpu_count() ) or 22
manager = multiprocessing.Manager()
return_dict = manager.dict()
jobs = []
#calculate how many pages each processor has
byPage= int( math.ceil( numPaginas/coresProcessor ))
pagesFrom=0
pagesTo=0
#Send task for every core
for i in range(coresProcessor):
pagesFrom = pagesTo
pagesTo = pagesFrom + byPage
if pagesTo>numPaginas:
pagesTo=numPaginas
p = multiprocessing.Process(target=self.doByPage, args=(pagesFrom,pagesTo,i, return_dict))
jobs.append(p)
p.start()
if pagesTo>=numPaginas:
break
for proc in jobs:
proc.join()
#Order single PDF's for merge
randData = return_dict.values()
ascArray = sorted(randData, key=lambda k: k['procnum'])
singlePdfsArray = []
for pdfs in ascArray:
singlePdfsArray.append(pdfs['dataB64'])
#merge task
return self.mergePdfsArray(singlePdfsArray)
except Exception as e:
print(f'Error {e}')
#Explotamos los cores del procesador
def doByPage(self,fromPage,toPage,procnum,return_dict):
output = PdfFileWriter()
waterMark = self.mask.getPage(0)
for i in range(fromPage,toPage):
#print(f'WaterMark page: {i}, Core: {procnum}')
page = self.mainPdf.getPage(i)
page.mergePage(waterMark)
page.compressContentStreams()
output.addPage(page)
letter_data = BytesIO()
output.write(letter_data)
letter_data.seek(0)
dataB64 = base64.b64encode(letter_data.read())
return_dict[procnum] = {'procnum':procnum,'dataB64':dataB64}
#Single Pdf with your watermark
def getMaskWaterMark(self,texto):
font_name='Helvetica'
font_size=22
color='#000000'
opacity=0.08
x=1
y=840
filename=''
bgTexto='';
for i in range(1, 6):
bgTexto+= ' '+texto;
cantidadRenglones=100
mask_stream = BytesIO()
watermark_canvas = canvas.Canvas(mask_stream, pagesize=A4)
watermark_canvas.setFont(font_name, font_size)
r, g, b = hex_to_rgb(color)
c = Color(r, g, b, alpha=opacity)
watermark_canvas.setFillColor(c)
print(watermark_canvas)
for i in range(1, cantidadRenglones):
watermark_canvas.drawString(x, y-(i * 25), bgTexto)
watermark_canvas.save()
mask_stream.seek(0)
mask = PdfFileReader(mask_stream , strict=False)
return mask
#Merge all pdf in only one pdf
def mergePdfsArray(self,arrayPdfsBase64):
merge = PdfFileMerger()
for f in arrayPdfsBase64:
nada = base64.b64decode(f)
stre = BytesIO(nada)
src = PdfFileReader(stre , strict=False)
merge.append(src)
letter_data = BytesIO()
merge.write(letter_data)
letter_data.seek(0)
data = base64.b64encode(letter_data.read())
return data

Two different pages with reportlab SimpleDocTemplate and Django

I'm using django and generating reports following this example, I need to generate a last page but without headers or footers and different content.
I'm trying to do this:
def print_example(self):
buffer = self.buffer
doc = SimpleDocTemplate(buffer,
rightMargin=72,
leftMargin=72,
topMargin=72,
bottomMargin=72,
pagesize=self.pagesize)
elements = []
elements.append(Paragraph('Content for all pages'), my_custom_style)
# ...
doc.build(elements, onFirstPage=self._header_footer, onLaterPages=self._header_footer,
canvasmaker=NumberedCanvas)
doc2 = SimpleDocTemplate(buffer,
rightMargin=72,
leftMargin=72,
topMargin=72,
bottomMargin=72,
pagesize=self.pagesize)
elements2 = []
elements2.append(Paragraph('Content for the last page only'), my_custom_style)
doc2.build(elements2, canvasmaker=NumberedCanvas)
# Get the value of the BytesIO buffer and write it to the response.
pdf = buffer.getvalue()
buffer.close()
return pdf
Then only the last content appears and previous content dissapears.
How can I generate the last page with different content?
I don't think it's possible using SimpleDocTemplate but you can achieve this by using BaseDocTemplate and defining your own templates.
Basic example
from reportlab.platypus import PageTemplate, BaseDocTemplate, NextPageTemplate, PageBreak
def headerFooterLayout(canvas, doc):
canvas.saveState()
canvas.setPageSize(self.pagesize)
# add header/footer
canvas.restoreState()
def emptyLayout(canvas, doc):
canvas.saveState()
canvas.setPageSize(self.pagesize)
canvas.restoreState()
pHeight, pWidth = self.pagesize
myFrame = Frame(0, 0, pHeight, pWidth, id='myFrame')
headerFooterTemplate = PageTemplate(id='headerFooterTemplate',
frames=[myFrame],
onPage=headerFooterLayout)
emptyTemplate = PageTemplate(id='emptyTemplate',
frames=[myFrame],
onPage=emptyLayout)
elements = []
elements.append(Paragraph('blah', style))
elements.append(NextPageTemplate('emptyTemplate'))
elements.append(PageBreak())
elements.append(Paragraph('last page', style))
doc = BaseDocTemplate(buffer,
rightMargin=72,
leftMargin=72,
topMargin=72,
bottomMargin=72)
doc.addPageTemplates([headerFooterTemplate, emptyTemplate])
doc.build(elements)
It's been quite a while since I used this so there may well be some issues but comment if something doesn't work.
This is all in the user guide but can be hard to find what you're looking for.

Python Reportlab PDF - Centering Text on page

I am using ReportLab to generate a pdf dynamically with python.
I would like a line of text to be centered on a page. Here is the specific code I currently have, but do not know how to center the text horizontally.
header = p.beginText(190, 740)
header.textOut("Title of Page Here")
# I know i can use TextLine etc in place of textOut
p.drawText(header)
The text displays and I can manually move the left position so the text looks centered, but I need this to be centered programmatically since the text will be dynamic and I don't know how much text there will be.
The reportlab canvas has a drawCentredString method. And yes, they spell it like that.
We’re British, dammit, and proud of
our spelling!
Edit:
As for text objects, I'm afraid you don't. You can do something along those lines, though:
from reportlab.pdfbase.pdfmetrics import stringWidth
from reportlab.rl_config import defaultPageSize
PAGE_WIDTH = defaultPageSize[0]
PAGE_HEIGHT = defaultPageSize[1]
text = "foobar foobar foobar"
text_width = stringWidth(text)
y = 1050 # wherever you want your text to appear
pdf_text_object = canvas.beginText((PAGE_WIDTH - text_width) / 2.0, y)
pdf_text_object.textOut(text) # or: pdf_text_object.textLine(text) etc.
You can use other page sizes, obviously.
I just needed this too, and wrote this:
def createTextObject(canv, x, y, text, style, centered=False):
font = (style.fontName, style.fontSize, style.leading)
lines = text.split("\n")
offsets = []
if centered:
maxwidth = 0
for line in lines:
offsets.append(canv.stringWidth(line, *font[:2]))
maxwidth = max(*offsets)
offsets = [(maxwidth - i)/2 for i in offsets]
else:
offsets = [0] * len(lines)
tx = canv.beginText(x, y)
tx.setFont(*font)
for offset, line in zip(offsets, lines):
tx.setXPos(offset)
tx.textLine(line)
tx.setXPos(-offset)
return tx
You can use Flowable object like Paragraph and assign alignment value to 1:
styles = getSampleStyleSheet()
title_style = styles['Heading1']
title_style.alignment = 1
title = Paragraph("Hello Reportlab", title_style)
story.append(title)
This example will create a pdf document with centered text:
from flask import make_response
import io
from reportlab.platypus import SimpleDocTemplate, Paragraph
from reportlab.lib.styles import getSampleStyleSheet
story=[]
pdf_doc = io.BytesIO()
doc = SimpleDocTemplate(pdf_doc)
styles = getSampleStyleSheet()
title_style = styles['Heading1']
title_style.alignment = 1
title = Paragraph("Hello Reportlab", title_style)
story.append(title)
doc.build(story)
content = pdf_doc.getvalue()
#output to browser
response = make_response(content)
response.mimetype = 'application/pdf'
return response
If you want the text to be floaten to the left, you need to change alignment to 0:
title_style.alignment = 0
If you want the text to be floaten to the right, you need to change alignment to 2:
title_style.alignment = 2
Try:
<para alignment="center">
As Per Reference : http://two.pairlist.net/pipermail/reportlab-users/2006-June/005092.html
In your case:
header.textOut("<"para alignment='center'>"Title of Page Here")
from reportlab.lib.pagesizes import letter
from reportlab.platypus import SimpleDocTemplate, Paragraph
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
doc = SimpleDocTemplate("form_letter.pdf",pagesize=letter,
rightMargin=72,leftMargin=72,
topMargin=72,bottomMargin=18)
Story = []
styles = getSampleStyleSheet()
title_style = styles['Heading1']
title_style.alignment = 1
title = Paragraph("Welcome to india", title_style)
Story.append(title)
doc.build(Story)

Categories

Resources