2021-08-14
Ever since the pandemic started, I've noticed QR codes creeping into my daily life. Venue check-ins, digital menus at restaurants, and online payments.
I thought it'd be fun to explore the technology a little bit, so I've built a micro web-app that lets you save messages and view them later using a QR code. Sort of a "Hello World" QR code project.
In this post, I'll be sharing how I built this using Python and AWS free tier.
You can try out the app itself here. The source code is also available on GitHub.
We've all seen and used them before, but how does a QR code actually work? The first thing to understand is that it is an internationally standardized specification:
This International Standard [...] specifies the QR Code symbology characteristics, data character encoding methods, symbol formats, dimensional characteristics, error correction rules [...]
As long an image respects these standard, it is a "QR code" and can be understood by most smartphone cameras. The image itself also has a kind of anatomy:
It is quite interesting, but ultimately these were the three things I cared about:
I want to make an app that lets me author some arbitrary content (in this case a short text message), have it persisted somewhere, and generate a QR code that loads the content when I scan it with the phone.
So after breaking these requirements into technical tasks, here is the strategy:
tag
for the object.tag
I made above). Then I'll find a Python library that lets me turn this URL string into a QR image. I'll make the image accessible to the user.tag
as a query parameter, so I'll just use that to look up the message in my table and send it back to the page.Most of the app's 'meaty' logic lives in the qr-code-infrastructure/compute/api
folder, as a bunch
of Python functions.
tag
and a URLWhen a user sends a message, it generates a random tag using uuid4
(which I truncated to 12
characters to keep it a bit shorter). A URL to view this message will then be used to create a QR code.
# uuid is a built-in Python library to generate random IDs with, with low chance of collision.
qr_id = uuid.uuid4().hex[:12]
qr_tag = f"qr-{qr_id}"
# We'll later have to implement this page so that it can load our message with the given tag.
content = f"https://qr.pixegami.com/view?tag={qr_tag}"
One of the things I really love about Python is how there's a library for everything. I just typed in "qr code" into PyPI and found this library, which took a minute to install and use.
Using the qrcode
library, I create an image. It's a one-liner.
qr_image = qrcode.make(content)
But now we need to save this image somewhere. Since this function is running on AWS Lambda, we can't just save it anywhere.
Each Lambda function receives 500MB of non-persistent disk space in its own /tmp directory.
This must be saved into /tmp
folder on Lambda
since that is the only folder that is writable (hence why we need to pass down a path
).
image_path = os.path.join(path, f"{qr_tag}.png")
qr_image.save(image_path)
Now in our Lambda runtime we have a .png
file at image_path
. We need to find a way to get it to the user. We'll do this by storing the image somewhere permanent, and then generate a URL for that image.
Let's upload it to an S3 bucket.
bucket_name = os.environ["IMAGE_BUCKET_NAME"]
key = f"{qr_result.tag}.png"
s3client = boto3.client("s3")
s3client.upload_file(qr_result.image_path, bucket_name, key)
I then create a pre-signed URL from the bucket so that I can tell the frontend where to load the QR code image from.
All objects by default are private. Only the object owner has permission to access these objects. However, the object owner can optionally share objects with others by creating a presigned URL, using their own security credentials, to grant time-limited permission to download the objects.
It expires in 3600
seconds, but that's fine because I don't need the image
itself to be long lived.
presigned_url = s3client.generate_presigned_url(
"get_object",
Params={"Bucket": bucket_name, "Key": key},
ExpiresIn=3600,
)
This URL is sent back to the front-end for display.
tag
and the messageFinally I need to write an entry into the DynamoDB table so that when a user scans a QR code, we can fetch the message later.
The only important thing to know about DynamoDB here is that it acts as a simple key-value store, and it is also serverless.
item = QrItem()
item.pk = qr_result.tag # This is the UUID we generated above.
item.message = message # This is the message body.
self.database.put_item(item)
On the front-end, it's simple just a page for this URL we generated earlier https://qr.pixegami.com/view?tag={qr_tag}
to look up the table value for the item with that tag
.
I used a React useEffect
hook, which lets me make an API call once the page loads.
I have another API on the back-end, which receives this tag
and looks up the saved message. It then sends it back for the front-end to display.
serialized_item = self.database.get_item(QrItem(tag))
item = QrItem().deserialize(serialized_item)
message = item.message
That's pretty much it! Now if I ever need to use QR codes as part of an application in the future I'll just dig this up đŸ˜…
And if you're keen to try this out yourself, feel free to check out the source. It's a small project so you can probably build it out in a few hours (or a few days, if you're new to AWS as well).