I tried to generate a shipping list with reportlab in Python.
I was trying to put all parts (like senders address, receivers address, a table) in place by using Platypus Frames.
The first problem that I ran into was that I needed a lot of Frames to position everything the right way, is there a better way using Platypus?
Because I want the senders address and my address to be on the same height and if I just add them to my story = [] they get aligned one below the other.
The next problem is that the table I'm drawing is dynamic in size and when I reach the end of the Frame ( space I want the table to go) it just does a FrameBreak and continuous in the next frame. So how can I make the Frame (space for my table ) dynamic?
Your use case is a really common one, so Reportlab has a system to help you out.
If you read the user guide about platypus it will introduce you to 4 main concepts:
DocTemplates the outermost container for the document;
PageTemplates specifications for layouts of pages of various kinds;
Frames specifications of regions in pages that can contain flowing text or graphics.
Flowables
Using PageTemplates you can combine "static" content with dynamic on a page in a sensible way like for example logo's, addresses and such.
You already discovered the Flowables and Frames but propably you did not start on fancyPageTemplates or DocTemplates yet. This makes sense because it isn't necessary for most simple documents. Saddly a shippinglist isn't a simple document, it holds address, logos and important info that has to be on every page. This is where PageTemplates come in.
So how do you use these templates? The concept is simple each page has a certain structure that might differ between pages, for example on page one you want to put the addresses and then start the table while on the second page you only want the table. This would be something like this:
Page 1:
Page 2:
A example would look like this:
(This would be perfect for the SO Documentation if there was one for Reportlab)
from reportlab.lib.pagesizes import A4
from reportlab.lib.styles import getSampleStyleSheet
from reportlab.lib.units import cm
from reportlab.lib import colors
from reportlab.platypus import BaseDocTemplate, Frame, PageTemplate, NextPageTemplate, Paragraph, PageBreak, Table, \
TableStyle
class ShippingListReport(BaseDocTemplate):
def __init__(self, filename, their_adress, objects, **kwargs):
super().__init__(filename, page_size=A4, _pageBreakQuick=0, **kwargs)
self.their_adress = their_adress
self.objects = objects
self.page_width = (self.width + self.leftMargin * 2)
self.page_height = (self.height + self.bottomMargin * 2)
styles = getSampleStyleSheet()
# Setting up the frames, frames are use for dynamic content not fixed page elements
first_page_table_frame = Frame(self.leftMargin, self.bottomMargin, self.width, self.height - 6 * cm, id='small_table')
later_pages_table_frame = Frame(self.leftMargin, self.bottomMargin, self.width, self.height, id='large_table')
# Creating the page templates
first_page = PageTemplate(id='FirstPage', frames=[first_page_table_frame], onPage=self.on_first_page)
later_pages = PageTemplate(id='LaterPages', frames=[later_pages_table_frame], onPage=self.add_default_info)
self.addPageTemplates([first_page, later_pages])
# Tell Reportlab to use the other template on the later pages,
# by the default the first template that was added is used for the first page.
story = [NextPageTemplate(['*', 'LaterPages'])]
table_grid = [["Product", "Quantity"]]
# Add the objects
for shipped_object in self.objects:
table_grid.append([shipped_object, "42"])
story.append(Table(table_grid, repeatRows=1, colWidths=[0.5 * self.width, 0.5 * self.width],
style=TableStyle([('GRID',(0,1),(-1,-1),0.25,colors.gray),
('BOX', (0,0), (-1,-1), 1.0, colors.black),
('BOX', (0,0), (1,0), 1.0, colors.black),
])))
self.build(story)
def on_first_page(self, canvas, doc):
canvas.saveState()
# Add the logo and other default stuff
self.add_default_info(canvas, doc)
canvas.drawString(doc.leftMargin, doc.height, "My address")
canvas.drawString(0.5 * doc.page_width, doc.height, self.their_adress)
canvas.restoreState()
def add_default_info(self, canvas, doc):
canvas.saveState()
canvas.drawCentredString(0.5 * (doc.page_width), doc.page_height - 2.5 * cm, "Company Name")
canvas.restoreState()
if __name__ == '__main__':
ShippingListReport('example.pdf', "Their address", ["Product", "Product"] * 50)
Related
I’m having some issues with reportlab and writing a PDF. When the PDF is written it only consumes a little less than 2/3 of the page width (letter). The heading, for example, wraps and never makes it past the halfway point of the document.
I’m at a loss for how to get my tables and paragraph to use the full width of the page.
Any insight is greatly appreciated.
Thank you in advance.
import io
import os
from django.core.files.base import ContentFile
from jsignature.utils import draw_signature
from reportlab.lib.pagesizes import letter
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
from reportlab.lib.enums import TA_RIGHT, TA_CENTER, TA_LEFT
from reportlab.lib.units import inch
from reportlab.pdfgen import canvas
from reportlab.platypus import SimpleDocTemplate, Paragraph, Table
from PIL import Image
# create pdf with table and paragraphs
def create_pdf(participant):
# create a file-like buffer to receive PDF data
buffer = io.BytesIO()
# define styles
styles = getSampleStyleSheet()
style_general = ParagraphStyle(
name='left',
parent=styles['Normal'],
fontSize=12,
fontName='Helvetica',
alignment=TA_LEFT)
style_image = ParagraphStyle(
name='left',
fontSize=30,
parent=styles['Normal'],
alignment=TA_LEFT)
style_heading = ParagraphStyle(
name='center',
fontSize=18,
fontName='Helvetica-Bold',
parent=styles['Heading1'],
leading=18,
alignment=TA_CENTER)
# create a simple document with page size in buffer
doc = SimpleDocTemplate(buffer, pagesize=letter, author='Me')
# create a list of paragraphs
AllParagraphs = []
# convert png image to jpeg
jpeg_image = get_jpeg_image(participant)
# add rows and columns so that the data can align
table_data = [
[Paragraph("My Heading - It should span the full page width", style_heading)],
[
Paragraph('Name:', style_general),
Paragraph(
f'{participant.first_name} {participant.middle_initial} {participant.last_name}',
style_general)
],
[
Paragraph(f'Signature:', style_general),
# image height of 30 to prevent overlapping since fontSize is 30,
# image width of double to maintain aspect ratio
Paragraph(
"<img src='{0}' valign='middle' width=60 height=30 />".format(
jpeg_image),
style_image)
]
]
# set rows and columns into Table object
table_element = Table(table_data)
# add table to list of paragraphs
AllParagraphs.append(table_element)
# build document with list of paragraphs
doc.build(AllParagraphs)
# get content of buffer
buffer.seek(0)
pdf_data = buffer.getvalue()
# save buffer content to django File object
file_data = ContentFile(pdf_data)
# name pdf file
file_data.name = f'{participant.last_name}.pdf'
# delete jpeg file
os.remove(jpeg_image)
# save pdf file to parent model
participant.pdf = file_data
participant.save()
For those interested in the answer: adjusting the table style to span multiple columns was the right approach.
In this case a table is being used to best align the signature elements, so spanning the columns similar to how you would in html or css is the solution.
...
# existing code for placement reference
# set rows and columns into Table object
table_element = Table(table_data)
# add table to list of paragraphs
# new code for spanning
# style table object to make single cells span both columns
table_element.setStyle(TableStyle([
('SPAN', (0, 0), (1, 0)),
]))
Using prompt_toolkit, I'd like to create an evenly-spaced vertical layout, regardless of width of content in each window (full-screen app). Undesired behavior - when changing content in one [or more] controls, layout is recalculated to accommodate for wider or narrower dynamic content.
Is there a way to make layout static for a given screen size; namely, only render windows on initialization or resize, keeping layout columns evenly spaced?
Example code below (press c to inject random-length content on either columns, layout width changes). Even adding a user message may cause un-even width initialization on a narrow enough terminal..
from random import randint
from prompt_toolkit.application import Application
from prompt_toolkit.key_binding import KeyBindings
from prompt_toolkit.layout.containers import VSplit, Window
from prompt_toolkit.layout.controls import FormattedTextControl
from prompt_toolkit.layout.layout import Layout
user_msg = "press 'c' to change, 'q' to quit"
body = VSplit(
[
Window(FormattedTextControl(text=user_msg)),
Window(width=1, char="|"),
Window(FormattedTextControl()),
]
)
kb = KeyBindings()
#kb.add("c")
def change_content(event):
for w in event.app.layout.find_all_windows():
prev_width = f"prev_width: {w.render_info.window_width}"
rand_str = "*" * randint(1, 50)
w.content.text = "\n".join([prev_width, rand_str])
#kb.add("q")
def quit(event):
event.app.exit()
layout = Layout(body)
app = Application(layout=layout, key_bindings=kb, full_screen=True)
app.run()
Passing the argument ignore_content_width works.
body = VSplit(
[
Window(FormattedTextControl(text=user_msg), ignore_content_width=True),
Window(width=1, char="|"),
Window(FormattedTextControl(), ignore_content_width=True),
]
)
from pptx import Presentation
from pptx.util import Inches
prs = Presentation("my_pptfile_begin.pptx")
left = Inches(0.6)
top = Inches(1.7)
blank_slide_1 = prs.slide_layouts[6]
add_the_slide = prs.slides.add_slide(blank_slide_1)
img_path1 = 'Three_Part_Set_Difference_excel_printout.png'
slide1=prs.slides[1]
pic = slide1.shapes.add_picture(img_path1, left, top)
blank_slide_2 = prs.slide_layouts[6]
add_the_slide = prs.slides.add_slide(blank_slide_2)
img_path2 = 'my_image.png'
slide2=prs.slides[2]
pic = slide2.shapes.add_picture(img_path2, left, top)
logoleft = Inches(4.7)
logotop = Inches(1.8)
blank_slide_3 = prs.slide_layouts[6]
add_the_slide = prs.slides.add_slide(blank_slide_3)
img_path3 = 'logo_slide.png'
slide3=prs.slides[3]
pic = slide3.shapes.add_picture(img_path3, logoleft, logotop)
prs.save('my_pptfile_Final.pptx')
How to remove the blank text boxes (or as a matter of fact all textboxes)?
I do not need any text boxes in my Powerpoint output file
If you're talking about empty placeholder shapes, just use a slide layout that has no placeholders.
In the default template used when you call Presentation() by itself (without an argument), that is the seventh layout I believe (prs.slide_layouts[6]).
But you need to adjust that for whatever starting .pptx file you're using ("my_pptfile_begin.pptx" in your case) by looking at it in slide-master view and counting down to the blank layout (or adding one if it doesn't have one).
blank_slide_3 = prs.slide_layouts[x] where x the slide number of the default template of your pptx application or your company's default template.
Therefore you need to know the default template and know the blank slide with no textboxes of that template based on the defaults of your ppt application
I am using python 2.7 with docx and I would like to change the background and text color of cells in my table based on condition.
I could not find any usefull resources about single cell formatting
Any suggestions?
Edit 1
my code
style_footer = "DarkList"
style_red = "ColorfulList"
style_yellow = "LightShading"
style_green = "MediumShading2-Accent6"
style_transperent = "TableNormal"
for a,rec in enumerate(data):
#V headinh se piše prvo polje iz table heada
document.add_heading(rec['tableHead'][0][0], level=1)
image_path = imageFolder + "\\" + slike[a]
document.add_picture(image_path, height=Inches(3.5))
#y += 28
#worksheet.insert_image( y, 1,imageFolder + "/" + slike[a])
for i, head in enumerate(rec['tableHead']):
table = document.add_table(rows=1, cols = len(head))
hdr_cells = table.rows[0].cells
for a in range(0,len(head)):
hdr_cells[a].text = head[a]
for a,body in enumerate(rec['tableData']):
row_cells = table.add_row().cells
for a in range(0,len(body)):
if body[a]['style'] == 'footer':
stil = style_footer
elif body[a]['style'] == 'red':
stil = style_red
elif body[a]['style'] == 'yellow':
stil = style_yellow
elif body[a]['style'] == 'green':
stil = style_green
else:
stil = style_transperent
row_cells[a].add_paragraph(body[a]['value'], stil)
document.save(wordDoc)
All cells are still the same.
If you want to color fill a specific cell in a table you can use the code below.
For example let's say you need to fill the first cell in the first row of your table with the RGB color 1F5C8B:
from docx.oxml.ns import nsdecls
from docx.oxml import parse_xml
shading_elm_1 = parse_xml(r'<w:shd {} w:fill="1F5C8B"/>'.format(nsdecls('w')))
table.rows[0].cells[0]._tc.get_or_add_tcPr().append(shading_elm_1)
Now if you want to also fill the second cell in the first row with the same color, you should create a new element
otherwise if you use the same element as above the fill will move on and will disappear from the first cell...
shading_elm_2 = parse_xml(r'<w:shd {} w:fill="1F5C8B"/>'.format(nsdecls('w')))
table.rows[0].cells[1]._tc.get_or_add_tcPr().append(shading_elm_2)
...and so on for other cells.
Source: https://groups.google.com/forum/#!topic/python-docx/-c3OrRHA3qo
With Nikos Tavoularis' solution, we have to create a new element for every cell.
I have created a function that achieves this. Works in Python revision 3.5.6 and python-docx revision 0.8.10
from docx.oxml import OxmlElement
from docx.oxml.ns import qn
def set_table_header_bg_color(table.rows[row_ix].cell):
"""
set background shading for Header Rows
"""
tblCell = cell._tc
tblCellProperties = tblCell.get_or_add_tcPr()
clShading = OxmlElement('w:shd')
clShading.set(qn('w:fill'), "00519E") #Hex of Dark Blue Shade {R:0x00, G:0x51, B:0x9E}
tblCellProperties.append(clShading)
return cell
"""
End of set_table_header_bg_color Function
"""
# main function
"""
..
..
..
1. Load Document
..
2. Access the required section
..
3. Load the required Table
..
4. Traverse to the cell by accessing the rows object
..
"""
for each_row in table.rows :
for each_cell in each_row.cells:
if each_cell.value satisfies a condition:
set_table_header_bg_color(each_cell)
"""
5. Continue execution
"""
What we found is that, if you do cell.add_paragraph('sometext', style_object), it will keep the existing empty paragraph and add an additional paragraph with the style, which is not ideal.
What you will want to do is something like:
# replace the entire content of cell with new text paragraph
cell.text = 'some text'
# assign new style to the first paragraph
cell.paragraphs[0].style = style_object
Note that the style is applied to the paragraph not the cell, which isn't ideal for background colors (since it won't fill the enter cell if you have some padding. I haven't found a way around that (except in the case where you want EVERY cell to have a background color, you can apply a style to table.style).
Also, make sure that your styles are defined. You can check
styles = documents.styles
for s in styles:
print s.name
to see all the styles you have. You can define new styles and also load a template document with pre-defined styles already.
It looks like instead of using the cell.text = "Something" method you need to use the cell.add_paragraph("SomeText", a_style) with a defined style - probably one of:
ColorfulGrid
ColorfulGrid-Accent1
ColorfulGrid-Accent2
ColorfulGrid-Accent3
ColorfulGrid-Accent4
ColorfulGrid-Accent5
ColorfulGrid-Accent6
Full list here.
If you use the “default” template document - otherwise you will have to create your own.
Taking from Nikos Tavoularis answer I would just change the shading_elm_1 declaration, as if you include the cell color in a loop for instance things might get messy.
As such, my suggestion would be:
from docx.oxml.ns import nsdecls
from docx.oxml import parse_xml
table.rows[0].cells[0]._tc.get_or_add_tcPr().append(parse_xml(r'<w:shd {} w:fill="1F5C8B"/>'.format(nsdecls('w'))))
I made a video demonstrating a way to do it here I took inspiration from the people above but I still had issues so I made this too help others.
https://www.youtube.com/watch?v=1Mgb95yigkk&list=PL_W7lgC2xeJfWBUllp7ALKOM5GUBMCVoP
from docx import Document
from docx.oxml import OxmlElement
from docx.oxml.ns import qn
document = Document("youfile.docx")
Table = document.tables[0]
#GET CELLS XML ELEMENT
cell_xml_element = Table.rows[1].cells[0]._tc
#RETRIEVE THE TABLE CELL PROPERTIES
table_cell_properties = cell_xml_element.get_or_add_tcPr()
#CREATE SHADING OBJECT
shade_obj = OxmlElement('w:shd')
#SET THE SHADING OBJECT
shade_obj.set(qn('w:fill'), "ff00ff")
#APPEND THE PROPERTIES TO THE TABLE CELL PROPERTIES
table_cell_properties.append(shade_obj)
document.save("yoursavefile.docx")
The code above will change the first cellof the second row of the first table in the document.Example of the output.
If you want to loop through the cells in a row use:
def color_row(row=0):
'make row of cells background colored, defaults to column header row'
row = t.rows[row]
for cell in row.cells:
shading_elm_2 = parse_xml(r'<w:shd {} w:fill="1F5C8B"/>'.format(nsdecls('w')))
cell._tc.get_or_add_tcPr().append(shading_elm_2)
run the function to color cells in the second row
color_row(2)
If you want to change the text color too, you can set it on the runs within the cell. I wrote this function to handle the cell background and text colors together (using Nikos' method for the fill):
def shade_cell(cell, fill=None, color=None):
if fill:
shading_elm = parse_xml(r'<w:shd {} w:fill="{}"/>'.format(nsdecls('w'), fill))
cell._tc.get_or_add_tcPr().append(shading_elm)
if color:
for p in cell.paragraphs:
for r in p.runs:
r.font.color.rgb = RGBColor.from_string(color)
I originally tried to expand Nikos' solution by adding w:color="XXXXXX" to the w:shd tag but that didn't work for me. However setting the font color on each run got the result I wanted.
I have compiled the previous answers and added some features.
Feel free to test: Create new file run the "main" part at the bottom.
""" adder for python-docx in order to change text style in tables:
font color, italic, bold
cell background color
based on answers on
https://stackoverflow.com/questions/26752856/python-docx-set-table-cell-background-and-text-color
"""
import docx # import python-docx (in order to create .docx report file)
from docx.oxml.ns import nsdecls
from docx.oxml import parse_xml
def change_table_cell(cell, background_color=None, font_color=None, font_size=None, bold=None, italic=None):
""" changes the background_color or font_color or font style (bold, italic) of this cell.
Leave the params as 'None' if you do not want to change them.
params:
cell: the cell to manipulate
background_color: name for the color, e.g. "red" or "ff0000"
font_color:
font_size: size in pt (e.g. 10)
bold: requested font style. True or False, or None if it shall remain unchanged
italic: requested font style. True or False, or None if it shall remain unchanged
background_color: the color of cells background"""
if background_color:
shading_elm = parse_xml(r'<w:shd {} w:fill="{}"/>'.format(nsdecls('w'), background_color))
cell._tc.get_or_add_tcPr().append(shading_elm)
if font_color:
for p in cell.paragraphs:
for r in p.runs:
r.font.color.rgb = docx.shared.RGBColor.from_string(font_color)
if font_size:
for p in cell.paragraphs:
for r in p.runs:
r.font.size = docx.shared.Pt(font_size)
if bold is not None:
for p in cell.paragraphs:
for r in p.runs:
r.bold = bold
if italic is not None:
for p in cell.paragraphs:
for r in p.runs:
r.italic = italic
def change_table_row(table_row, background_color=None, font_color=None, font_size=None, bold=None, italic=None):
for cell in table_row.cells:
change_table_cell(cell, background_color=background_color, font_color=font_color, font_size=font_size,
bold=bold,
italic=italic)
if __name__ == "__main__": # do the following code only if we run the file itself
#document = docx.Document('template.docx') # create an instance of a word document, use the style that we have defined in 'template.docx'
document = docx.Document()
num_rows = 4
num_cols = 3
table = document.add_table(rows=num_rows, cols=num_cols) # create empty table
#table.style = document.styles['MyTableStyleBlue'] # test overwriting the predefined style
# fill table
for row in range(num_rows):
for col in range(num_cols):
table.rows[row].cells[col].text = f'row/col=({row},{col})'
""" change color (see https://stackoverflow.com/questions/26752856/python-docx-set-table-cell-background-and-text-color) """
# Nikos Tavoularis answered Apr 18, 2017 at 8:38
shading_elm_1 = parse_xml(r'<w:shd {} w:fill="1F5C8B"/>'.format(nsdecls('w')))
table.rows[0].cells[0]._tc.get_or_add_tcPr().append(shading_elm_1)
# test new function derived from dyoung's answere of May 25 at 7:34, 2022
change_table_cell(table.rows[0].cells[0], background_color=None, font_color="ff0000", bold=False)
change_table_cell(table.rows[1].cells[2], background_color="00ff00", font_color="ff0000", font_size=20, bold=True)
change_table_row(table.rows[3], background_color="lightgreen", font_color="0000ff", italic=True) # https://www.delftstack.com/howto/python/colors-in-python/
document.save('table_shading_test.docx')
I'm trying to add a simple "page x of y" to a report made with ReportLab.. I found this old post about it, but maybe six years later something more straightforward has emerged? ^^;
I found this recipe too, but when I use it, the resulting PDF is missing the images..
I was able to implement the NumberedCanvas approach from ActiveState. It was very easy to do and did not change much of my existing code. All I had to do was add that NumberedCanvas class and add the canvasmaker attribute when building my doc. I also changed the measurements of where the "x of y" was displayed:
self.doc.build(pdf)
became
self.doc.build(pdf, canvasmaker=NumberedCanvas)
doc is a BaseDocTemplate and pdf is my list of flowable elements.
use doc.multiBuild
and in the page header method (defined by "onLaterPages="):
global TOTALPAGES
if doc.page > TOTALPAGES:
TOTALPAGES = doc.page
else:
canvas.drawString(270 * mm, 5 * mm, "Seite %d/%d" % (doc.page,TOTALPAGES))
Just digging up some code for you, we use this:
SimpleDocTemplate(...).build(self.story,
onFirstPage=self._on_page,
onLaterPages=self._on_page)
Now self._on_page is a method that gets called for each page like:
def _on_page(self, canvas, doc):
# ... do any additional page formatting here for each page
print doc.page
I came up with a solution for platypus, that is easier to understand (at least I think it is). You can manually do two builds. In the first build, you can store the total number of pages. In the second build, you already know it in advance. I think it is easier to use and understand, because it works with platypus level event handlers, instead of canvas level events.
import copy
import io
from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer
from reportlab.lib.styles import getSampleStyleSheet
from reportlab.lib.units import inch
styles = getSampleStyleSheet()
Title = "Hello world"
pageinfo = "platypus example"
total_pages = 0
def on_page(canvas, doc: SimpleDocTemplate):
global total_pages
total_pages = max(total_pages, doc.page)
canvas.saveState()
canvas.setFont('Times-Roman', 9)
canvas.drawString(inch, 0.75 * inch, "Page %d %s" % (doc.page, total_pages))
canvas.restoreState()
Story = [Spacer(1, 2 * inch)]
style = styles["Normal"]
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))
# You MUST use a deep copy of the story!
# https://mail.python.org/pipermail/python-list/2022-March/905728.html
# First pass
with io.BytesIO() as out:
doc = SimpleDocTemplate(out)
doc.build(copy.deepcopy(Story), onFirstPage=on_page, onLaterPages=on_page)
# Second pass
with open("test.pdf", "wb+") as out:
doc = SimpleDocTemplate(out)
doc.build(copy.deepcopy(Story), onFirstPage=on_page, onLaterPages=on_page)
You just need to make sure that you always render a deep copy of your original story. Otherwise it won't work. (You will either get an empty page as the output, or a render error telling that a Flowable doesn't fit in the frame.)