Reportlab paragraph frame split doesn't work - python

I have the report lab document, which is a mix of regular strings drawed with drawstring method and frames. I will get the table data from request, and i need to have the possibility to split the table to another page, because i don't know the size of the page. It works fine when the table is not big, but when the amount of rows is too big, it dust doesn't display anyting on the page. As per the reportlab lib, I simple need to use frame.split(), but it doesn't work. The code:
def generate_pdf(request: Request):
buffer = io.BytesIO()
pdfmetrics.registerFont(TTFont("Poppins-bold", f"static/fonts/poppins-800.ttf"))
pdfmetrics.registerFont(TTFont("Poppins-medium", "static/fonts/poppins-medium.ttf"))
pdfmetrics.registerFont(TTFont("Poppins-semibold", "static/fonts/poppins-semibold.ttf"))
pdfmetrics.registerFont(TTFont("Poppins-regular", "static/fonts/poppins-regular.ttf"))
doc = canvas.Canvas(buffer)
customColor = colors.Color(red=(39.0 / 255), green=(71.0 / 255), blue=(114.0 / 255))
doc.setFillColor(customColor)
doc.setFont(psfontname="Poppins-bold", size=20)
if request.data["type"] == "Purchase Order":
doc.drawString(50, 780, "Purchase Order")
else:
doc.drawString(50, 780, "Work Order")
doc = add_image(doc)
doc = draw_invoice_start_end(doc, request)
doc = draw_order_by_order_to(doc, request)
table_data = []
for i in range(50):
table_data.append(["item"])
frame1 = Frame(width=800, height=300, showBoundary=1, x1=50, y1=200)
story = []
table = Table(data=table_data, colWidths=None, rowHeights=None, style=None, splitByRow=1, repeatRows=0,
repeatCols=0,
rowSplitRange=None, spaceBefore=None, spaceAfter=None, cornerRadii=None)
styles = getSampleStyleSheet()
styles.add(ParagraphStyle(name="Rectangles",
fontName="Poppins-semibold",
textColor=customColor,
fontSize=10,
leading=12,
backColor="white",
borderRadius=5)
)
styleN = styles["Rectangles"]
story.append(table)
paragraph = Paragraph(text="Lorem impsum" * 5000, style=styleN)
story.append(paragraph)
for el in story:
if frame1.add(el, doc) == 0:
frame1.split(el, doc)
doc.showPage()
frame1 = Frame(0.5 * inch, inch, 7 * inch, 10.5 * inch, showBoundary=1)
doc.save()
pdf = buffer.getvalue()
file_data = ContentFile(pdf)
buffer.close()
return file_data
The only way I can think of, is using the frame.split() method, but it didn't work. Also, it looks like the SimpleDocTemplate could help me, but I will need to change the whole code to make it work.
This is what i get with this code: outcome

Related

Reportlab - How to add margin between Tables?

So i am trying to create three tables per page, the following code will collide all three tables together with 0 margin between them. I would like some white space between two tables. Is there a configuration for that?
doc = SimpleDocTemplate("my.pdf", pagesize=A4)
elements = []
i = 0
for person in persons:
data = get_data()
t = Table(data, colWidths=col_widths, rowHeights=row_heights)
elements.append(t)
i = i + 1
if i % 3 == 0:
elements.append(PageBreak())
doc.build(elements)
You could try using the Spacer function to add space between the tables. An example of its use from the documentation is:
from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer
def go():
doc = SimpleDocTemplate("hello.pdf")
Story = [Spacer(1,2*inch)]
for i in range(100):
bogustext = ("This is Paragraph number %s. " % i) *20
p = Paragraph(bogustext, style)
Story.append(p)
Story.append(Spacer(1,0.2*inch))
doc.build(Story, onFirstPage=myFirstPage, onLaterPages=myLaterPages)

(Python) Change pagesize and format of PDF file generated with xtopdf

I want to convert an xlsx with Python. I used the modules tablib and xtopdf to build a well structured table. Works excellent! Unfortunately the content does not fit on one pdf page. So I wanted to change the pagesize and format to horizontal A3. But I don't know how that could work. My code:
import random
import tablib
from openpyxl import load_workbook
from xtopdf import PDFWriter
from pyPdf import PdfFileWriter, PdfFileReader
workbook = load_workbook('C:/Users/user1/Testexcel.xlsx', guess_types=True, data_only=True)
worksheet = workbook.get_sheet_by_name('Testsheet')
ws_range = worksheet.iter_rows('A4:H6')
# Helper function to output a string to both screen and PDF.
def print_and_write(pw, strng):
print strng
pw.writeLine(strng)
# Create an empty Dataset and set its headers.
data = tablib.Dataset()
data.headers = ['col1', 'col2', 'col3', 'col4']
widths = [30, 20, 10, 20] # Display widths for columns.
for row in ws_range:
col1 = str(row[0].value)
col2 = str(row[1].value)
col3 = str(row[2].value)
col4 = str(row[3].value)
columns = [col1, col2, col3, col4]
row = [ str(col).center(widths[idx]) for idx, col in enumerate(columns) ]
data.append(row)
# Set up the PDFWriter.
pw = PDFWriter('C:/Users/user1/Test.pdf')
pw.setFont('Courier', 10)
pw.setHeader('Test')
pw.setFooter('Test')
# Generate header and data rows as strings; output them to screen and PDF.
separator = '-' * sum(widths)
print_and_write(pw, separator)
# Output headers
header_strs = [ header.center(widths[idx]) for idx, header in enumerate(data.headers) ]
print_and_write(pw, ''.join(header_strs))
print_and_write(pw, separator)
# Output data
for row in data:
print_and_write(pw, ''.join(row))
print_and_write(pw, separator)
pw.close()
Found out that the PDFWriter from xtopdf itself instanciates an canvas object of the reportlab library. In the canvas class an attribute pagesize is declared which is setted by default to 'A4'. But if I change the entry to 'A3' the result pdf still is in 'A4'.
class Canvas(textobject._PDFColorSetter):
from reportlab.pdfgen import canvas
c = canvas.Canvas("hello.pdf")
from reportlab.lib.units import inch
# move the origin up and to the left
c.translate(inch,inch)
# define a large font
c.setFont("Helvetica", 80)
# choose some colors
c.setStrokeColorRGB(0.2,0.5,0.3)
c.setFillColorRGB(1,0,1)
# draw a rectangle
c.rect(inch,inch,6*inch,9*inch, fill=1)
# make text go straight up
c.rotate(90)
# change color
c.setFillColorRGB(0,0,0.77)
# say hello (note after rotate the y coord needs to be negative!)
c.drawString(3*inch, -3*inch, "Hello World")
c.showPage()
c.save()
"""
def __init__(self,filename,
pagesize='A3',
bottomup = 1,
pageCompression=None,
encoding = None,
invariant = None,
verbosity=0):
"""Create a canvas of a given size. etc.
You may pass a file-like object to filename as an alternative to
a string.
Most of the attributes are private - we will use set/get methods
as the preferred interface. Default page size is A4."""
if pagesize is None: pagesize = 'A3'
if encoding is None: encoding = rl_config.defaultEncoding
if invariant is None: invariant = rl_config.invariant
self._filename = filename
self._encodingName = encoding
self._doc = pdfdoc.PDFDocument(encoding,
compression=pageCompression,
invariant=invariant, filename=filename)
#this only controls whether it prints 'saved ...' - 0 disables
self._verbosity = verbosity
#this is called each time a page is output if non-null
self._onPage = None
self._pagesize = pagesize
self._pageRotation = 0
#self._currentPageHasImages = 0
self._pageTransition = None
self._pageDuration = None
self._destinations = {} # dictionary of destinations for cross indexing.
self.setPageCompression(pageCompression)
self._pageNumber = 1 # keep a count
#self3 = [] #where the current page's marking operators accumulate
# when we create a form we need to save operations not in the form
self._codeStack = []
self._restartAccumulators() # restart all accumulation state (generalized, arw)
self._annotationCount = 0
self._outlines = [] # list for a name tree
self._psCommandsBeforePage = [] #for postscript tray/font commands
self._psCommandsAfterPage = [] #for postscript tray/font commands
#PostScript has the origin at bottom left. It is easy to achieve a top-
#down coord system by translating to the top of the page and setting y
#scale to -1, but then text is inverted. So self.bottomup is used
#to also set the text matrix accordingly. You can now choose your
#drawing coordinates.
self.bottomup = bottomup
self.imageCaching = rl_config.defaultImageCaching
self._make_preamble()
self.init_graphics_state()
self.state_stack = []
edit: I think the changes in the reportlab module are not accepted by the system. Tried to remove the dictionary reportlab and tried to import it then in the commandline. Ironically it works ylthough python should not find that module anymore.
try this
from reportlab.pdfgen import canvas
from reportlab.lib.units import mm
c = canvas.Canvas("hello.pdf", pagesize = (297 * mm, 420 * mm))
# or (420 * mm, 297 * mm) if you want it in portrait format
# values for inch: 11.69 * inch , 16.53 * inch
#the following would create an empty page
c.showPage()
c.save()
Just forked a project named xtopdf at bitbucket and made the following change:
##------------------------ PDFWriter.__init__ ----------------------------
- def __init__(self, pdf_fn):
+ def __init__(self, pdf_fn, pagesize='A4'):
'''
Constructor.
"pdf_fn" arg is the name of the PDF file to be created.
'''
self.__pdf_fn = pdf_fn # file name of PDF file
- self.__canv = canvas.Canvas(pdf_fn) # canvas to write on
+ self.__canv = canvas.Canvas(pdf_fn, pagesize) # canvas to write on
self.__font_name = None # font name
self.__font_size = None # font size
self.__header_str = None # header string (partial)
Can you try it? use pw = PDFWriter('C:/Users/user1/Test.pdf', 'A3').

How to add multiple lines at bottom (footer) of PDF?

I have to create PDF file in which need to add lines at bottom left like footer.
Following code is working:
import StringIO
from reportlab.pdfgen import canvas
import uuid
def test(pdf_file_name="abc.pdf", pdf_size=(432, 648), font_details=("Times-Roman", 9)):
# create a new PDF with Reportla
text_to_add = "I am writing here.."
new_pdf = "test_%s.pdf"%(str(uuid.uuid4()))
packet = StringIO.StringIO()
packet.seek(0)
c = canvas.Canvas(pdf_file_name, pagesize = pdf_size)
#- Get the length of text in a PDF.
text_len = c.stringWidth(text_to_add, font_details[0], font_details[1])
#- take margin 20 and 20 in both axis
#- Adjust starting point on x axis according to text_len
x = pdf_size[0]-20 - text_len
y = 20
#- set font.
c.setFont(font_details[0], font_details[1])
#- write text,
c.drawString(x, y, text_to_add)
c.showPage()
c.save()
return pdf_file_name
Now if text have multiple lines then this is not working because length of text is greater than width of Page size. Understood.
I try with Frame and paragraph but still can not write text in correct position in a PDF
Following is code:
from reportlab.lib.pagesizes import letter
from reportlab.lib.styles import getSampleStyleSheet
from reportlab.platypus import BaseDocTemplate, Frame, PageTemplate, Paragraph
styles = getSampleStyleSheet()
styleN = styles['Normal']
styleH = styles['Heading1']
def footer(canvas, doc):
canvas.saveState()
P = Paragraph("This is a multi-line footer. It goes on every page. " * 10, styleN)
w, h = P.wrap(doc.width, doc.bottomMargin)
print "w, h:", w, h
print "doc.leftMargin:", doc.leftMargin
P.drawOn(canvas, 10, 30)
canvas.restoreState()
def test():
doc = BaseDocTemplate('test.pdf', pagesize=(432, 648))
print "doc.leftMargin:", doc.leftMargin
print "doc.bottomMargin:", doc.bottomMargin
print "doc.width:", doc.width
print "doc.height:", doc.height
frame = Frame(10, 50, 432, 648, id='normal')
template = PageTemplate(id='test', frames=frame, onPage=footer)
doc.addPageTemplates([template])
text = []
for i in range(1):
text.append(Paragraph("", styleN))
doc.build(text)
Not understand why size of page change, because I set (432, 648) but is show (288.0, 504.0)
doc.leftMargin: 72.0
doc.bottomMargin: 72.0
doc.width: 288.0
doc.height: 504.0
Also Frame size:
w, h: 288.0 96
doc.leftMargin: 72.0
Do not know how to fix this issue.
I refer this link
First the mystery regarding the doc.width, the doc.width isn't the actual width of the document. It is the width of the area between the margins so in this case doc.width + doc.leftMargin + doc.rightMargin equals the width of the actual page.
Now back to why the footer did not span the entire width of the page as you wanted. This is because of the same issue as described above, namely doc.width isn't the actual paper width.
Assuming you want the footer to span the entire page
def footer(canvas, doc):
canvas.saveState()
P = Paragraph("This is a multi-line footer. It goes on every page. " * 10, styleN)
# Notice the letter[0] which is the width of the letter page size
w, h = P.wrap(letter[0] - 20, doc.bottomMargin)
P.drawOn(canvas, 10, 10)
canvas.restoreState()
Assuming you want the footer to span the width of the writable area
Note: the margins on the default setting are pretty big so that is why there is so much empty space on the sides.
def footer(canvas, doc):
canvas.saveState()
P = Paragraph("This is a multi-line footer. It goes on every page. " * 10, styleN)
w, h = P.wrap(doc.width, doc.bottomMargin)
print "w, h:", w, h
print "doc.leftMargin:", doc.leftMargin
P.drawOn(canvas, doc.leftMargin, 10)
canvas.restoreState()
EDIT:
As it might be useful to know where the normal text should start. We need to figure out the height of our footer. Under normal circumstances we cannot use P.height as it depends on the width of the text, calling it will raise a AttributeError.
In our case we actually are able to get the height of the footer either directly from P.wrap (the h) or by calling P.height after we have called P.wrap.
By starting our Frame at the height of the footer we will never have overlapping text. Yet it is important to remember to set the height of the Frame to doc.height - footer.height to ensure the text won't be placed outside the page.

Split a table across more than 1 page in Reportlab

I'm trying to split a "full-width" table across 2 pages or even more. I use the Platypus library of ReportLab and the BaseDocTemplate class.
I've a "full width" table of elements and this should be drawn into a frame of the first page, if the table has enough rows It should be continued in the second page. My problem is that the frame of the first page has a different height and position than the others, because at the top of the first page I need to show more information (Yes... I'm talking about an invoice or order).
After thousands attempts, all that I've got is a pdf with a unique page with only 8 items/rows, It's exactly the space that they require at the first page, but if the table has more than 8 rows, then I get a pdf with only 1 page and without the table (that means an empty frame, although I see all data in the log).
I've used the methods split() and wrap() but probably in the wrong way, because I'm new with ReportLab. I show you the last version of my code:
from django.http import HttpResponse
from reportlab.pdfgen import canvas
from reportlab.lib.units import mm
from reportlab.lib import colors
from reportlab.lib.pagesizes import A4
from reportlab.platypus import BaseDocTemplate, PageTemplate, Table, Spacer, Frame, TableStyle,\
NextPageTemplate, PageBreak, FrameBreak
PAGE_WIDTH = A4[0]
PAGE_HEIGHT = A4[1]
MARGIN = 10*mm
class ThingPDF(BaseDocTemplate):
def header(self, canvas, subheader=True):
# print 'header()'
data = [('AAAA', 'Thing %s' % (self.thing.name)), ]
s = []
t = Table(data, colWidths=[95 * mm, 95 * mm], rowHeights=None, style=None, splitByRow=1,
repeatRows=0, repeatCols=0)
t.setStyle(TableStyle([
('BACKGROUND', (0, 0), (0, 0), colors.red),
('BACKGROUND', (1, 0), (1, 0), colors.blue),
('ALIGN', (1, 0), (1, 0), 'RIGHT'),
]))
s.append(t)
# if subheader:
# print 'subheader'
self.head.addFromList(s, canvas)
def data_table(self, canvas, items):
# print 'data_table()'
d = [[u'col0', u'col1', u'col2', u'col3', u'col4', ],]
for item in items:
d.append([item.col0, item.col1, item.col2, item.col3, item.col4])
s = []
t = Table(d, colWidths=[20*mm, 100*mm, 20*mm, 20*mm, 30*mm], rowHeights=20*mm, style=None,\
splitByRow=1, repeatRows=0, repeatCols=0)
t.setStyle([('BACKGROUND', (0,0), (-1,0), ('#eeeeee'))])
h=187*mm #TODO
w=A4[0] - (2*MARGIN)
splitter = t.split(w, h)
# print '\n\nresult of splitting: ', len(splitter)
for i in splitter:
print 'i: ', i
self.dataframeX.addFromList(s, canvas)
s.append(t)
self.dataframe0.addFromList(s, canvas)
def on_first_page(self, canvas, doc):
canvas.saveState()
self.header(canvas)
self.data_table(canvas, self.items)
canvas.restoreState()
def on_next_pages(self, canvas, doc):
canvas.saveState()
self.header(canvas, subheader=False)
canvas.restoreState()
def build_pdf(self, thing=None, items=None, user=None):
self.thing = thing
self.items = items
self.doc = BaseDocTemplate('%s.pdf' % (thing.name),
pagesize=A4,
pageTemplates=[self.first_page, self.next_pages,],
showBoundary=1,
rightMargin=MARGIN,
leftMargin=MARGIN,
bottomMargin=MARGIN,
topMargin=MARGIN,
allowSplitting=1,
title='%s' % 'title')
self.story.append(Spacer(0*mm, 2*mm))
self.doc.build(self.story)
response = HttpResponse(mimetype='application/pdf')
response['Content-Disposition'] = 'attachment; filename=%s.pdf' % (reference)
return response
def __init__(self):
self.thing = None
self.items = None
self.story = []
#========== FRAMES ==========
self.head = Frame(x1=MARGIN, y1=A4[1] - (2*MARGIN), width=A4[0] - (2*MARGIN), height=10*mm,
leftPadding=0, bottomPadding=0, rightPadding=0, topPadding=0, id='header',
showBoundary=1)#, overlapAttachedSpace=None, _debug=None)
self.dataframe0 = Frame(x1=MARGIN, y1=10*mm, width=A4[0] - (2*MARGIN), height=187*mm,
leftPadding=0, bottomPadding=0, rightPadding=0, topPadding=0,
id='body', showBoundary=1)
self.dataframeX = Frame(x1=MARGIN, y1=MARGIN, width=A4[0] - (2*MARGIN), height=257*mm,
leftPadding=0, bottomPadding=0, rightPadding=0, topPadding=0,
id='body', showBoundary=1)
#========== PAGES ==========
self.first_page = PageTemplate(id='firstpage', frames=[self.head, self.dataframe0], onPage=self.on_first_page)
self.next_pages = PageTemplate(id='nextpages', frames=[self.head, self.dataframeX], onPage=self.on_next_pages)
Thank you in advance!!
The code that you posted is lacking some data and is not by itself runnable, so i can't really tell if my answer will be correct. Please extend your code if this doesn't work!
First of all, you don't have to use the wrap and split methods at all! The table will split itself when doc.build consumes the story. Also, split doesn't split the table inline, but returns just a list of tables. So in your case splitter is a list of tables over which you iterate and then append an empty list to the frame. I would suggest that you skip that part. You are adding the different elements to individual frames. Because of this, you would add the splitted table to the dataframeX but dataframeX might never be used, because you are never using the next_pages PageTemplate. For this you have to add an NextPageTemplate() to the story, after you are finished with your first page. I would let platypus do that stuff for you: Just let your methods return lists with the generated elements and concatenate them before passing them to doc.build().

How do I continue a content to a next page in Reportlabs - Python

I'm making a table, where the table can be either small or large depending upon the data being received.
While I was providing a huge data set, I noticed that although the table is being made but my all content is not there, since it occupies only 1 page for that.
So, my question is How do I continue a content to a next page in Reportlabs without using showpage() , since I wont be able to know when to hit showpage or when not, because the content is being dynamically generated?
Code
def plot_table(pie_labels, pie_data, city_devices):
styles = getSampleStyleSheet()
styleN = styles["BodyText"]
styleN.alignment = TA_LEFT
styleBH = styles["Normal"]
styleBH.alignment = TA_CENTER
city_name = Paragraph('''<b>City Name</b>''', styleBH)
meter_name = Paragraph('''<b>Meter Name</b>''', styleBH)
consumption = Paragraph('''<b>Total Consumption</b>''', styleBH)
data= [[city_name, meter_name, consumption]]
# Texts
for label,record,device in zip(pie_labels,pie_data,city_devices):
label = Paragraph(label, styleN)
record = Paragraph(str(record), styleN)
device_list = ""
for d in device:
device_list += str(d) + ", "
device = Paragraph(device_list, styleN)
data.append([label, device, record])
table = Table(data, colWidths=[5.05 * cm, 5.7 * cm, 3* cm ])
table.setStyle(TableStyle([('INNERGRID', (0,0), (-1,-1), 0.25, colors.black),
('BOX', (0,0), (-1,-1), 0.25, colors.black),
]))
return table
table = plot_table(pie_labels, pie_data, city_devices)
table.wrapOn(the_canvas, width, height)
table.drawOn(the_canvas, *coord(2, 59.6, cm))
I'd advice using the more high-level primitives of reportlab, that is, document templates, frames and flowables. That way, you get splitting for "free". An example from the related questions
Use table.split():
from reportlab.lib.pagesizes import A4 # im using A4
width, height = A4
table_pieces = table.split(width, height)
for table_piece in table_pieces:
table_piece.drawOn(the_canvas, *coordinates)
the_canvas.show_page()
the_canvas.save()
Tell me if it helped :)

Categories