Wednesday, 27 June, 2018 UTC


Summary

Think you've got a handle on how to serve images for your app or website? Think again!
Fly makes serving images a breeze, and gives you the power and flexibility to edit and modify images before they're even sent to your client. Oh, and by the way, they're sent using Fly's lightning fast CDN to ensure they get there in milliseconds, not seconds.
In this article, I'll walk you through building a simple yet powerful image API with Fly. We'll let clients dynamically change the size of the images they request, and add a colored border, all using simple URL parameters. If that sounds cool, let's dive in and get started!
First things first though, I'll assume you have a basic Fly project setup. For information on how to do that check out our Getting Started Guide.
Telling Fly about the images
Our first step is to prepare the images we want to serve. For simplicity sake, I just have 3 basic .jpg files, but obviously, in an actual production app, you could have as many as you want. I'll go ahead and store these in a folder called images inside the root directory of the project.
Now that our images are safe and sound, we need to tell Fly where they are. In order for Fly to eventually do its thing, it first needs to know what images are available and where they're located. Let's go ahead and open up our .fly.yml file (if its not there just create it), and add the following code.
# .fly.yml
app: image-api
files:
- images/1.jpg
- images/2.jpg
- images/3.jpg
This YAML file is kinda like the settings for our app.
Here we've got two fields. The app field lets you give a name to your application, and the files field is an array that holds entries for each one of the files we want Fly to know about. In our case, we've added the paths to our three image files. Notice how we include the path and not just the name of the file.
Now that we've added the paths to our images, Fly will know about them and be able to serve them up when the time comes.
Serving up the raw images
Speaking of serving up our images, let's go ahead and write some code that will do exactly that.
Inside your index.js file add in the following code
import { Image } from '@fly/image'

fly.http.respondWith(async function (req) {
  const url = new URL(req.url)
  try {
    const imageFile = await fetch('file:/' + url.pathname);
    const imageToServe = await new Image(await imageFile.arrayBuffer())
    return new Response(imageToServe.data);
  } catch (e) {
    return new Response('File Not Found', { status: 404 })
  }
})
This is the boilerplate code we need to serve up these images to the client. We start by creating a URL object from the URL that was requested. Then we create an imageFile constant which loads in the corresponding file that we have stored in our project directory (if you didn't put the path to the file in the .fly.yml file this won't work). Once we've grabbed our file we can then create an Image object called imageToServe whose data gets returned to the client.
Notice also that all of this code is surrounded by try/catch blocks. In the case that the client requests a file that doesn't exist or that's not specified in the .fly.yml file, instead of throwing an error, we'll just send them a 404.
Run the fly server command and try loading up an image on your browser
localhost:3000/images/2.jpg
So far so good, we've built a basic app that can serve images. But in the big scheme of things that's not really all that impressive. What is impressive is being able to edit and modify these images before we send them out. This is where Fly separates itself from the pack. Let's take a look at how we can use Fly's image API to resize and give some color to these images.
Resize & add a border
For the purposes of this article we're gonna do two things, resize the image, and then give it a colored border. Keep in mind, however, that we're only scratching the surface of what's possible with Fly's image API.
Let's add a few more lines of code to our index.js file and see what happens.
import { Image } from '@fly/image'

fly.http.respondWith(async function (req) {
  const url = new URL(req.url)
  try {
    const imageFile = await fetch('file:/' + url.pathname);
    const imageToServe = await new Image(await imageFile.arrayBuffer())
        .resize(300, 300)     // (width, height) - remove height to maintain aspect ratio
        .background('blue')   // background color
        .extend(15)           // border width
        .toBuffer()
    return new Response(imageToServe.data);
  } catch (e) {
    return new Response('File Not Found', { status: 404 })
  }
})
Take a look below the declaration of the imageToServe variable. After we create a new Image object, we can call a few of it's methods, and modify the image a bit.
First up is the resize(width, height) method. We can pass this method a width and a height, and our image will be resized/cropped to the desired dimensions.
Next, we add a background color. This will be the color of the background that the image is going to sit on. We then want to pass in a color value, which can either be the name of a color (most common color names are supported), a hex color value (ex. #FF5733), or an RGB map (ex. {r: 0, g: 0, b: 0, alpha: 1}).
We’ll also want to use the extend(size) method to extend the background out from under the image. Normally the background will be sitting behind the image and won't be visible. By extending the background, we create a visible border around the image, which will give us the border effect that we're after.
Finally, we call the toBuffer() method and we can return the image back to the client in a Response object just like before.
Incorporating URL parameters
Okay, so now that we know how to serve up image files, and modify them on the way out, there's just one missing piece. We want to allow the client to decide how the image should be modified. In other words they should be able to determine the dimensions and the border color dynamically.
We'll do this by allowing the client to pass in a series of URL parameters specifying these values.
If you're not familiar with URL parameters, the concept is simple. When you enter a URL into your web browser (like for example the URL to these images on our CDN), you can pass in a series of key-value pairs called parameters, which will give the backend system the information it needs to do its job.
In this case we want to pass in 4 different parameters
  1. width - The width of the image
  2. height - The height of the image
  3. bcolor - The background color
  4. bsize - The size of the image border
These parameters will be used as arguments into the resize(), background(), and extend() methods, allowing the client to control each corresponding property of the image.
An example of a URL we might use when accessing one of these images is
localhost:3000/images/2.jpg?width=200&height=200&bcolor=blue&bsize=5
Notice that after the localhost:3000/image/2.jpg we place a ?. This question mark delineates the actual hostname, port & file name from the URL parameters. Then we specify each parameter using name=value and separate them with a &.
Let's go ahead and add this functionality into our code:
import { Image } from '@fly/image'

fly.http.respondWith(async function (req) {
  const url = new URL(req.url)
  try {
    const imageFile = await fetch('file:/' + url.pathname);
    const imageToServe = await new Image(await imageFile.arrayBuffer())
        .resize(
          parseInt(url.searchParams.get('width')),
          parseInt(url.searchParams.get('height')))   
        .background(url.searchParams.get('bcolor'))
        .extend(parseInt(url.searchParams.get('bsize')))         
        .toBuffer()
    return new Response(imageToServe.data);
  } catch (e) {
    return new Response(e, { status: 404 })
  }
})
In the code above, instead of using constant values for the Image methods, we can simply pass in the values of the URL parameters. We’ll access the URL parameters by calling the searchParams.get(paramName) method on the URL object we created. Assuming the URL contains the specified parameter it should give us the value.
You'll also notice that for the resize() and extend() methods we’ve surrounded the search params with the parseInt() function. By default, all of theses URL parameters are going to be strings. The problem is that JavaScript is expecting numerical values, so we have to take an extra step and convert any parameters we expect to be numbers into integers.
Let's run this code and see what happens.
You can use the same URL from above localhost:3000/images/2.jpg?width=200&height=200&bcolor=blue&bsize=5 or try your own. Play around with a couple different combinations of values and see what looks cool.
Tip - if you want to pass in a hex color value as the bcolor, you can't use the # symbol. In URLs the # symbol means something special so we can't literally pass it in. Instead, use %23. So if you wanted to pass in hex value #ffffff instead pass in %23ffffff (ex. bcolor=%23ffffff)
What else can we do?
At this point we have a fully functional image API. With just a few lines of code we've created a smart and adaptable CDN which allows images to be modified before they're sent to the client.
Here’s the thing though, we’ve only scratched the surface of Fly’s image API. By incorporating more Image methods, and adding more complex URL parameters, this app could go from good to great in a matter of minutes! But for now, this should give you a good idea of what's possible and act as a starting point to a more complex application!