Split a table across more than 1 page in Reportlab - python

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().

Related

Reportlab paragraph frame split doesn't work

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

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-PPTX: Changing table style or adding borders to cells

I've started putting together some code to take Pandas data and put it into a PowerPoint slide. The template I'm using defaults to Medium Style 2 - Accent 1 which would be fine as changing the font and background are fairly easy, but there doesn't appear to be an implemented portion to python-pptx that allows for changing cell borders. Below is my code, open to any solution. (Altering the XML or changing the template default to populate a better style would be good options for me, but haven't found good documentation on how to do either). Medium Style 4 would be ideal for me as it has exactly the borders I'm looking for.
import pandas
import numpy
from pptx import Presentation
from pptx.util import Inches, Pt
from pptx.dml.color import RGBColor
#Template Location
tmplLoc = 'C:/Desktop/'
#Read in Template
prs = Presentation(tmplLoc+'Template.pptx')
#Import data as Pandas Dataframe - dummy data for now
df = pandas.DataFrame(numpy.random.randn(10,10),columns=list('ABCDEFGHIJ'))
#Determine Table Header
header = list(df.columns.values)
#Determine rows and columns
in_rows = df.shape[0]
in_cols = df.shape[1]
#Insert table from C1 template
slide_layout = prs.slide_layouts[11]
slide = prs.slides.add_slide(slide_layout)
#Set slide title
title_placeholder = slide.shapes.title
title_placeholder.text = "Slide Title"
#Augment placeholder to be a table
placeholder = slide.placeholders[1]
graphic_frame = placeholder.insert_table(rows = in_rows+1, cols = in_cols)
table = graphic_frame.table
#table.apply_style = 'MediumStyle4'
#table.apply_style = 'D7AC3CCA-C797-4891-BE02-D94E43425B78'
#Set column widths
table.columns[0].width = Inches(2.23)
table.columns[1].width = Inches(0.9)
table.columns[2].width = Inches(0.6)
table.columns[3].width = Inches(2)
table.columns[4].width = Inches(0.6)
table.columns[5].width = Inches(0.6)
table.columns[6].width = Inches(0.6)
table.columns[7].width = Inches(0.6)
table.columns[8].width = Inches(0.6)
table.columns[9].width = Inches(0.6)
#total_width = 2.23+0.9+0.6+2+0.6*6
#Insert data into table
for rows in xrange(in_rows+1):
for cols in xrange(in_cols):
#Write column titles
if rows == 0:
table.cell(rows, cols).text = header[cols]
table.cell(rows, cols).text_frame.paragraphs[0].font.size=Pt(14)
table.cell(rows, cols).text_frame.paragraphs[0].font.color.rgb = RGBColor(255, 255, 255)
table.cell(rows, cols).fill.solid()
table.cell(rows, cols).fill.fore_color.rgb=RGBColor(0, 58, 111)
#Write rest of table entries
else:
table.cell(rows, cols).text = str("{0:.2f}".format(df.iloc[rows-1,cols]))
table.cell(rows, cols).text_frame.paragraphs[0].font.size=Pt(10)
table.cell(rows, cols).text_frame.paragraphs[0].font.color.rgb = RGBColor(0, 0, 0)
table.cell(rows, cols).fill.solid()
table.cell(rows, cols).fill.fore_color.rgb=RGBColor(255, 255, 255)
#Write Table to File
prs.save('C:/Desktop/test.pptx')
Maybe not really clean code but allowed me to adjust all borders of all cells in a table:
from pptx.oxml.xmlchemy import OxmlElement
def SubElement(parent, tagname, **kwargs):
element = OxmlElement(tagname)
element.attrib.update(kwargs)
parent.append(element)
return element
def _set_cell_border(cell, border_color="000000", border_width='12700'):
tc = cell._tc
tcPr = tc.get_or_add_tcPr()
for lines in ['a:lnL','a:lnR','a:lnT','a:lnB']:
ln = SubElement(tcPr, lines, w=border_width, cap='flat', cmpd='sng', algn='ctr')
solidFill = SubElement(ln, 'a:solidFill')
srgbClr = SubElement(solidFill, 'a:srgbClr', val=border_color)
prstDash = SubElement(ln, 'a:prstDash', val='solid')
round_ = SubElement(ln, 'a:round')
headEnd = SubElement(ln, 'a:headEnd', type='none', w='med', len='med')
tailEnd = SubElement(ln, 'a:tailEnd', type='none', w='med', len='med')
Based on this post: https://groups.google.com/forum/#!topic/python-pptx/UTkdemIZICw
In case someone else comes across this issue again, some changes should be made to the solution posted by JuuLes87 to avoid that Microsoft Office PowerPoint requires to repair the generated presentation.
After carefully inspecting the xml string of the table generated by pptx, I found that the requirement to repair the presentation seemed to be due to the duplicated nodes of 'a:lnL' or 'a:lnR' or 'a:lnT' or 'a:lnB' in the children elements of 'a:tcPr'. So we only need to remove nodes of ['a:lnL','a:lnR','a:lnT','a:lnB'] before these nodes are inserted as below.
from pptx.oxml.xmlchemy import OxmlElement
def SubElement(parent, tagname, **kwargs):
element = OxmlElement(tagname)
element.attrib.update(kwargs)
parent.append(element)
return element
def _set_cell_border(cell, border_color="000000", border_width='12700'):
tc = cell._tc
tcPr = tc.get_or_add_tcPr()
for lines in ['a:lnL','a:lnR','a:lnT','a:lnB']:
# Every time before a node is inserted, the nodes with the same tag should be removed.
tag = lines.split(":")[-1]
for e in tcPr.getchildren():
if tag in str(e.tag):
tcPr.remove(e)
# end
ln = SubElement(tcPr, lines, w=border_width, cap='flat', cmpd='sng', algn='ctr')
solidFill = SubElement(ln, 'a:solidFill')
srgbClr = SubElement(solidFill, 'a:srgbClr', val=border_color)
prstDash = SubElement(ln, 'a:prstDash', val='solid')
round_ = SubElement(ln, 'a:round')
headEnd = SubElement(ln, 'a:headEnd', type='none', w='med', len='med')
tailEnd = SubElement(ln, 'a:tailEnd', type='none', w='med', len='med')
I had a hard time figuring out why this wasn't working. For anyone else struggling with this, I had to add the following to the end of the function:
return cell
When using, you want to use the function as such:
cell = _set_cell_border(cell)

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.

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