If you or someone you know participated in this year's AP Collegeboard Exams, you probably recognize the stress of submitting handwritten work within a small time constraint.
Bunnimage aims to help alleviate that stress for students and others working at home. It takes an image as an input on an upload page and converts it into a PDF that is available at a download page.
Overview
In this tutorial, we'll be walking through:
- Creating the "Upload" page and an HTTP Trigger Function that will upload the user's image to a storage container.
- Setting up an Event Grid Subscription and a Function that converts the image into a PDF and stores it again.
- This is where the API will live!
- Creating the "Download" page and an HTTP Trigger Function that retrieves the correct PDF.
-
Optional For those who are interested, we can add another Function to delete the files and keep our containers squeaky clean.
- Note: The diagram above excludes the optional deletion feature.
You can find a sample of the final product at my Github repository.
Before we start:
- Make sure you have an Azure Subscription so we can utilize the amazing features of Microsoft Azure Functions (It's free!) 🤩
- Register for an account on Online Convert (with the free version), as we will be using this API convert our images
- If you want to host your website somewhere, check out Repl.it, or you can just have your project run locally
Step 1: Upload the image ⬆️
Creating a Function App
We're going to have a lot of triggers in this project, so let's get started by creating a Function App! Follow those steps to create the Function App, and then create the first HTTP trigger (this will upload our image).
Note: it will be helpful if you keep track of these strings for reference later in the project: Storage account name (found in "Hosting"), Function app name, Resource Group
Before we start coding the trigger, though, we need to install some npm
packages/libraries.
Click on the "Console" tab in the left panel under "Development Tools".
Inside the console (shown on the right panel), type in the following commands:
npm init -y
npm install parse-multipart
npm install node-fetch
npm install @azure/storage-blob
Tip: The Azure Storage Blob client library is going to be a key piece of the project. After all, it's about blobs!
Setting up your storage account
This is the storage account you created when creating the Function App. If you don't know what it is, search "Storage Containers" in the query box in Azure portal.
We're going to need to create 2 containers: "images" and "pdfs." Think of these as folders in the account.
You will need to upgrade your storage account because Event Grid Subscriptions will only work with a v2 version. Follow this tutorial to upgrade it.
Writing our First Azure Function to Upload an Image
⬇ Some housekeeping...
- For the function to work, we have to initialize the packages/libraries we installed in the beginning of part 1.
-
Take note of the
process.env
value being assigned toconnectionstring
in the code below (Line 3). Use this tutorial to add in your own secret strings from your storage container.- The storage container is the one you created when you started your Function App. Navigate to it and find your secret strings here:
- Keep these safe, and use the connection string in the corresponding variable in the code.
- Note: You'll need to store other strings in environment variables later on in the tutorial
Let's start just by initializing a few variables we'll need.
⬇ The main block of code
- Notice that we are able to name the file with the user's username in line 10 by receiving it from the header.
- Later on in the JS, we will send the username in the header of the request.
- The
parse-multipart
library is being used in lines 4-11 to parse the image from the POST request we will later make with the frontend; refer to the documentation linked above. - Some if-else logic is used from lines 13-22 to determine the file extension.
- We then call the
uploadBlob()
function in line 24.
⬇ Uploading the image blob to the "images" container
- Notice the
uploadBlob()
function! This is what uploads the parsed image to the specified "images" blob container.- Here's a YouTube Video to help explain the handy dandy library
Frontend: The "upload" webpage
Next, I created a static HTML page that will accept the image from the user and send to the Azure Function we just coded using Javascript.
Note: I removed unnecessary sections of my code because I wanted to make the webpage ✨fancy✨, but you can see the whole thing here.
Above we have:
- Input box for the username (simple but insecure auth system)
- Button to submit
However, a static HTML webpage can't make a request to the Azure Function itself, which is where we're going to cook up some JS. 😯
Frontend: Javascript for interacting with the Azure Function
This block of Javascript updates the preview thumbnail while getting the picture, gets the username, and sends them both over to the function we just coded.
First, loadFile()
is called when the file input changes to display the thumbnail.
async function loadFile(event){
console.log("Got picture!");
var image = document.getElementById("output");
// Get image from output
image.src = URL.createObjectURL(event.target.files[0])
// load inputted image into the image src and display
}
Then, handle()
is called when the file is submitted to POST the image and username. The image is sent in the body, and username is sent as a header. Lines 15-30
Be sure to change the function url on Line 19 to the one of your upload image Function!
Deploy your code
- Try doing it locally with the live server extension for VS Code
- Try Azure Web Apps
- I personally used repl.it
Update CORS Settings
This is a crucial step!! 😱 If you don't change your CORS (Cross-origin resource sharing) settings, the POST request won't work. This tells the Function App what domains can access our Azure Function.
Options:
-
Recommended: Change it to a wildcard operator (
*
), which allows all origin domains to make requests Change it to the domain you are using to host your code
Home stretch! 🏃🏻♀️
It's finally time to test our first step that our app will make!
- Navigate to your HTML page and submit an image
Go to the "images" storage container and check to see if your image is there!
Error? Check the log in your Function
Step 2: Convert The Image 🔄
Create another Azure Function
Yep... We need yet another Azure Function. (What can I say? They're pretty helpful.) This one will trigger when the image blob is stored, then convert it into a PDF, and store it in the "pdfs" container.
However, this time, it will be an Event Grid Trigger, so make sure you select the right one!
What's the difference?
- Event Grid Triggers trigger based on an Event Grid Subscription, which we will create later in this step. Our trigger will fire when a blob is stored in the "images" container
- HTTP Triggers fire when a GET or POST request is made to the endpoint (function URL)
Commercial Break 📺
Let's recap:
- Step 1 ✅: We created the "Upload" page and an HTTP Trigger Function that uploaded the user's image to a storage container.
- Step 2: We will create an Event Grid function that converts the image into a PDF by calling the Online Convert API and will upload the PDF to blob storage.
⚠😵WARNING😵⚠ Lots of code ahead, but it's all good! I split it into sections.
First off, the Online-Convert API!
- We're going to need to get another secret key, except this time from the API. Here's how to get that.
- Once again, save it in your environment variables so it's accessible.
- Note: This API does have restrictions on the amount of conversions during 24 hours, so just be aware that you may get an error after reaching the limit.
⬇ This convertImage()
function does exactly what it's called: convert the image by calling the Online-Convert API. Here's some documentation around how to use the API with Azure Blob Storage.
async function convertImage(blobName){
const api_key = process.env['convertAPI_KEY'];
const accountKey = process.env['accountKey'];
const uriBase = "<https://api2.online-convert.com/jobs>";
// env variables (similar to .gitignore/.env file) to not expose personal info
// check out documentation
img = {
"conversion": [{
"target": "pdf"
}],
"input": [{
"type": "cloud",
"source": "azure",
"parameters": {
"container": "images",
"file": blobName
},
"credentials": {
"accountname": "bunnimagestorage",
"accountkey": accountKey
}
}]
}
payload = JSON.stringify(img);
// making the post request
let resp = await fetch(uriBase, {
method: 'POST',
body: payload,
// we want to send the image
headers: {
'x-oc-api-key' : api_key,
'Content-type' : 'application/json',
'Cache-Control' : 'no-cache'
}
})
// receive the response
let data = await resp.json();
return data;
}
⬇To check the status of the conversion and determine whether we can store the PDF to blob storage yet, let's use this checkStatus()
function that makes a request to the same https://api2.online-convert.com/jobs
endpoint, except with a GET request instead of POST.
async function checkStatus(jobId){
const api_key = process.env['convertAPI_KEY'];
const uriBase = "<https://api2.online-convert.com/jobs>";
// env variables to keep your info private!
// making the post request
let resp = await fetch(uriBase + "/" + jobId, {
/*The await expression causes async function execution to pause until a Promise is settled
(that is, fulfilled or rejected), and to resume execution of the async function after fulfillment.
When resumed, the value of the await expression is that of the fulfilled Promise*/
method: 'GET',
headers: {
'x-oc-api-key' : api_key,
}
})
// receive the response
let data = await resp.json();
return data;
}
Then we can use the same uploadBlob()
function from before to upload our object!
After this we get to the main section of our code.
⬇It gets the blobName, calls the functions, and downloads the PDF to be stored.
- The
blobName
is retrieved from theEventGrid
subscription subject* in lines 10-11 - Because the API does not convert the image immediately, we need a while loop to repeatedly check for the status of the conversion in lines 21-36
- The last portion is used to download the converted PDF by sending a GET request to the URI from the completed file conversion response. Lines 43-47
What's a subject? This part of the Event response contains information about what specific file caused the Azure Function to fire. For example:
/blobServices/default/containers/test-container/blobs/new-file.txt
where the file isnew-file.txt
Now that the long block of code is done with, let's take a look at some responses you should expect from the API.
- This is what you would get if the file is still converting 🤔
- Here's what you would get when the conversion is complete! (yay) 🥳
In particular, there are 3 important pieces of the output we should examine:
-
update.status.code
: This tells us whether its done processing or not -
update.output[0].uri
: This gives us the URL where we can download the PDF (used in the last GET request) -
result.id
: Gives the ID of the file conversion "job" so we can continually check for its status
Before we can test our code, we need one last step: the trigger!
Creating an Event Subscription
When the image blob is stored in the "images" container, we want the conversion from jpg/jpeg/png to pdf to begin immediately!
Just like how "subscribing" to a YouTube channel gives you notifications, we're going to subscribe to our own Blob Storage and trigger the Azure Function.
Tip: You'll want to keep the names for your storage account and resource group handy.
- Search "Event Grid Subscriptions" in the search bar
- Click "+ Event Subscription" in the top left
- Fill in the form to create the Event Subscription:
- If it asks you for a name, feel free to put anything you want - I named it "fileUploaded"
- Under Topic Types, select "Storage Accounts"
- The "Resource Group" is the Resource Group that holds your storage account
- The "Resource" is your storage account name
Note: If your storage account doesn't appear, you forgot to follow the "upgrade to v2 storage" step
- Under Event Types: filter to Blob Created
- The "Endpoint Type" is "Azure Function"
- The "Function" is the function we want triggered when an image is uploaded, so the
convertImage
function - Tweaking some settings...
-
Navigate to the "Filters" tab and "Enable Subject Filtering"
-
Change the "Subject Begins With" to
/blobServices/default/containers/images/blobs/
- This way, the subscription will not trigger when a PDF is stored in the "pdfs" container. It will only trigger when something is stored in "images."
Congratulations! You have now subscribed to the "blob created" event in your "images" container that triggers the convert image function!
Upload a converted PDF to the "pdfs" container!
Now that we've connected our Functions and frontend together with an Event Grid Subscription, try submitting another image to check if it successfully uploads as a PDF into the "pdfs" container.
If you used my code and have the same context.log()s, you should get something like this when the PDF uploads:
Step 3: Downloading the PDF on the HTML page ⬇
Now that we have a PDF stored in the "pdfs" container, how will we get the PDF back to the user? You got it right, yet another Azure Function!
Create another HTTP Trigger - this one will return the PDF download URL to the frontend when triggered.
Commercial Break 📺
Let's recap:
- Step 1 ✅: We created the "Upload" page and an HTTP Trigger Function that uploaded the user's image to a storage container.
- Step 2 ✅: We will create an Event Grid function that converts the image into a PDF by calling the Online Convert API and will upload the PDF to blob storage.
- Step 3: We will create a HTTP Trigger function that returns the PDF to the user when triggered by the "Download" page.
- Step 4: Optional If you choose, create another HTTP Trigger function and modify other code to delete the image and PDF blobs from storage containers once they are unneeded.
Azure Functions: Check if the PDF is ready to be served 🍝
⬇First, it receives the username to get the correct PDF from the header of the request, which is made by the webpage. You will see this request later on in the JS of this step.
var fetch = require("node-fetch");
module.exports = async function (context, req, inputBlob) {
context.log('JavaScript HTTP trigger function processed a request.');
var username = req.headers['username'];
var download = "<https://bunnimagestorage.blob.core.windows.net/pdfs/>" + username + ".pdf";
⬇Then, using the personalized URL, it performs a GET request to check if the PDF has been stored in the "pdfs" container.
let resp = await fetch(download, {
method: 'GET',
})
let data = await resp;
if (data.statusText == "The specified blob does not exist.") {
success = false;
context.log("Does not exist: " + data)
} else {
success = true;
context.log("Does exist: " + data)
}
⬇The function then returns the URL for downloading the PDF and whether or not the PDF is ready for download to the webpage.
context.res = {
body: {
"downloadUri" : download,
"success": success,
}
};
// receive the response
context.log(download);
context.log(data)
context.done();
}
Frontend: Creating the Download HTML page
Once again, the "fancy" stuff is omitted.
⬆Like we created the "upload" page in Step 1, we now need a "download" page for users to receive the PDF.
This piece of code creates:
- An input for the username Line 6
- One button for refreshing to check if the PDF is ready Line 8
- One button for downloading the file Line 9
Frontend: Downloading the PDF on the Webpage
Time to get bombarded with some lovely JS!
Part 1 ⬇:
- Change the HTML on lines 2-4 to display the current status (whether it's looking for the PDF, whether it's ready for download, etc.)
- Make a request on lines 9-16 to the HTTP Trigger Function we just coded, sending the username inputted on the HTML page along with it
Part 2 ⬇:
- First we're going to find the link to download the PDF with
data.downloadUri
on line 1 - Change buttons from "Refresh" to "Download" when PDF is ready for download
- How to do this? Remove the "Refresh" button Lines 10-11 and make "Download" visible Line 9
- Set the
onclick
attribute of the "Download" button to call thegetPdf()
function with the unique username + link for download. Line 8- The
getPdf()
function allows for immediate download withwindow.open(link)
Lines 16-19
- The
Amazing! You're done!
Here's the finished product in which I download the cute bunny shopping picture I uploaded earlier.
Congratulations! I hope this knowledge of Azure Functions helps you create even more fun apps!
If you're interested in augmenting this app, try using your new knowledge of Blob Storage, HTTP Triggers, the Node SDK (@azure/storage-blob), and some Stack Overflow to assist you to add a feature to delete the image and PDF blobs.
Top comments (1)
BRILLIANT!! Let's get everyone to use this asap