Your Web News in One Place

Help Webnuz

Referal links:

Sign up for GreenGeeks web hosting
April 23, 2023 11:02 pm GMT

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.
PDF sample

(Note: We want the text in the PDF files to be selectable and copyable, so we won't be using Matplotlib this time.)

Architecture

Architecture of creating PDF files

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 changing unit = mm to inch, but some modifications are needed with x, y.
  • on_grid parameter in create_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.

email example

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.

grid example


Original Link: https://dev.to/aws-builders/create-selectable-pdf-files-with-lambda-python-and-reportlab-5gp0

Share this article:    Share on Facebook
View Full Article

Dev To

An online community for sharing and discovering great ideas, having debates, and making friends

More About this Source Visit Dev To