An Interest In:
Web News this Week
- April 24, 2024
- April 23, 2024
- April 22, 2024
- April 21, 2024
- April 20, 2024
- April 19, 2024
- April 18, 2024
Create selectable PDF files with Lambda Python and ReportLab
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 ioimport osimport uuidfrom datetime import datetimeimport boto3from reportlab.lib import colorsfrom reportlab.lib.pagesizes import A4, landscapefrom reportlab.lib.units import inch, mmfrom reportlab.pdfgen import canvasdef 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_passeddef 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_leftdef 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 bufferdef generate_n_char_id(n: int): unique_id = uuid.uuid4() short_id = str(unique_id)[:n] return short_iddef 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 responsedef send_sns_message(topic_arn, message): sns_client = boto3.client("sns") response = sns_client.publish( TopicArn=topic_arn, Message=message, ) return responsedef 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.
Original Link: https://dev.to/aws-builders/create-selectable-pdf-files-with-lambda-python-and-reportlab-5gp0
Dev To
An online community for sharing and discovering great ideas, having debates, and making friendsMore About this Source Visit Dev To