Motivation
There are times when I need to create PDF files for reporting from data points using an AWS Lambda function.
This blog post was inspired by a blog post from Classmethod (in Japanese), which demonstrates how to create PDF files with a Lambda function using WeasyPrint + Jinja. In this post, I want to share an alternative approach to create PDF files with Python Lambda using the ReportLab library.
Here is the image of the PDF file.
(Note: We want the text in the PDF files to be selectable and copyable, so we won't be using Matplotlib this time.)
Architecture
Step by step deployment
Note: Python 3.9
Create S3 Bucket and SNS Topic
-
Create the S3 Bucket
- The default setting is OK. (No public access).
- your-bucket-name will be used later.
- aws-doc
-
Create the SNS Topic
- arn of the topic will be used later.
- aws-doc
Create Lambda Layer
Start from any directory on your terminal.
Install the reportlab
library to a directory named python (Note: the name must be python).
pip install reportlab -t python
Zip python
directory with the name my_lambda_layer.zip
(Note: arbitrary name).
zip -r my_lambda_layer.zip python
Upload this zip file in the step of creating a layer.
You will use the arn of the layer later.
Create Lambda function
On the Lambda console, Create Lambda (Python).
Configuration
-
General configuration
- The default 3 sec timeout might be short. Change it a little longer, like 10 sec.
-
Environmental variables
- SNS_ARN: arn of the topic you have
- BUCKET_NAME: name of the bucket you have
-
IAM Policy
- Select Permission, and Role name. Add an inline policy like the one below for sending SNS and uploading a file to S3. Change the arn of your SNS topic and S3 bucket name.
- Make sure you don't change the IAM policy while your pre-singed URL will be used.
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": "sns:Publish",
"Resource": "arn:aws:sns:REGION:ACCOUNT_ID:your-sns-topic-arn"
},
{
"Effect": "Allow",
"Action": [
"s3:PutObject",
"s3:GetObject",
"s3:ListBucket"
],
"Resource": [
"arn:aws:s3:::your-bucket-name/*",
"arn:aws:s3:::your-bucket-name"
]
}
]
}
Code
Add Lambda Layer
In the Code tab, add the Layer you've created. Specify the ARN of the layer you've created.
Write code
- In this code
- Percentage of the year passed and left days are calculated.
- The gauge for the percentage is generated with
draw_gauge()
. - Create an A4 PDF file. Upload it to the S3 bucket.
- Set the pre-assigned URL to the file in the bucket
- Send the URL with SNS.
- As the unit I used
mm
.inch
is available by changingunit = mm
toinch
, but some modifications are needed withx
,y
. -
on_grid
parameter increate_pdf_days_passed_left()
is for designing layouts. (See Appendix)
import io
import os
import uuid
from datetime import datetime
import boto3
from reportlab.lib import colors
from reportlab.lib.pagesizes import A4, landscape
from reportlab.lib.units import inch, mm
from reportlab.pdfgen import canvas
def percentage_of_year_passed(start_of_year, end_of_year, now):
total_days = (end_of_year - start_of_year).days + 1
days_passed = (now - start_of_year).days + 1
percentage_passed = int((days_passed / total_days) * 100)
return percentage_passed
def calculate_days():
now = datetime.now()
start_of_year = datetime(now.year, 1, 1)
end_of_year = datetime(now.year, 12, 31, 23, 59, 59)
percentage_passed = percentage_of_year_passed(start_of_year, end_of_year, now)
days_left = (end_of_year - now).days + 1
return percentage_passed, days_left
def draw_grid(c, width, height, unit):
step = 25 if unit == mm else 1
c.setLineWidth(1 / unit)
c.setStrokeColor(colors.grey)
c.setFont("Helvetica", 25 / unit)
for i in range(0, int(height) + 1, step):
c.line(0, i, width, i)
c.drawString(0, i, f"{i}")
for i in range(0, int(width) + 1, step):
c.line(i, 0, i, height)
c.drawString(i, 0, f"{i}")
def draw_gauge(c, x, y, radius, percentage, base_color, fill_color):
"""
Draw gauge with two arches: base 180 deg arch and filled with percentage.
"""
# Draw base half-circle
c.setLineWidth(15)
c.setStrokeColor(base_color)
c.arc(x - radius, y - radius, x + radius, y + radius, 180, -179.9)
# Draw filled half-circle
c.setStrokeColor(fill_color)
gauge = -180 * percentage / 100 + 0.01
c.arc(x - radius, y - radius, x + radius, y + radius, 180, gauge)
def draw_str(c, x, y, item, font_size, font_color="black"):
c.setFont("Helvetica", font_size)
c.setFillColor(font_color)
c.drawCentredString(x, y, f"{item}")
def create_pdf_days_passed_left(page_size, x, y, radius, on_grid = False):
unit = mm # mm or inch
buffer = io.BytesIO()
c = canvas.Canvas(buffer, pagesize=page_size)
c.scale(unit, unit)
if on_grid:
width = page_size[0] / unit
height = page_size[1] / unit
draw_grid(c, width, height, unit)
base_color = colors.HexColor("#c8c8c8") # light grey
fill_color = colors.HexColor("#1f77b4") # light blue
percentage_passed, days_left = calculate_days()
message = f'Created at {datetime.now().strftime("%Y-%m-%d %H:%M:%S")}'
draw_str(c, x + 50, y + 55, message, 8)
draw_gauge(c, x, y, radius, percentage_passed, base_color, fill_color)
draw_str(c, x, y, f"{percentage_passed}%", font_size=20)
draw_str(c, x, y - 25, "This year passed", font_size=10)
draw_str(c, x + 100, y, days_left, font_size=20, font_color="green")
draw_str(c, x + 100, y - 25, "days left", font_size=10)
c.showPage()
c.save()
buffer.seek(0)
return buffer
def generate_n_char_id(n: int):
unique_id = uuid.uuid4()
short_id = str(unique_id)[:n]
return short_id
def generate_presigned_url(bucket_name, object_name, expiration_sec=3600):
s3_client = boto3.client("s3")
response = s3_client.generate_presigned_url(
"get_object",
Params={"Bucket": bucket_name, "Key": object_name},
ExpiresIn=expiration_sec,
)
return response
def send_sns_message(topic_arn, message):
sns_client = boto3.client("sns")
response = sns_client.publish(
TopicArn=topic_arn,
Message=message,
)
return response
def lambda_handler(event, context):
s3 = boto3.client("s3")
bucket_name = os.environ["BUCKET_NAME"]
sns_topic_arn = os.environ["SNS_ARN"]
dt = datetime.now().strftime("%Y%m%d-%H%M%S")
uuid = generate_n_char_id(8)
filename = f"demo-{dt}-{uuid}.pdf"
page_size = landscape(A4)
x, y = 100, 125 # Center of gauge. Use this point as anchoring
radius = 40 # radius of gauge
pdf_buffer = create_pdf_days_passed_left(page_size, x, y, radius)
s3.upload_fileobj(pdf_buffer, bucket_name, filename)
url = generate_presigned_url(bucket_name, filename, expiration_sec=3600)
if url:
print(f"Generated presigned URL: {url}")
message = f"Download the PDF file here: {url}"
send_sns_message(sns_topic_arn, message)
else:
print("Failed to generate presigned URL")
return {
"statusCode": 200,
"body": "PDF created and uploaded to S3. Presigned url has sent with SNS.",
}
Result
You'll receive an email to download URL.
Summary
I've demonstrated creating a PDF file using a Lambda Python function, using Reportlab library.
Appendix
This is the example PDF file with unit = mm
and grid_on = True
, useful for designing the page. The dimension of the A4 landscape is 210 x 297 mm.
Top comments (0)