./axel.leroy.sh

Developer, tech nerd and photographer

The making of PhotoGallery

03 Apr 2020

A little over 6 months ago, a fellow photographer asked this seemingly simple question: “Do you have a portfolio? I mean, something other than an Instagram account”. Oh boy, did he even knew what he was getting me into with this question!

Link to section Why make a custom portfolio in the first place?

Most people would have replied either “Oh, I have an account on Flickr/500px/some other photo sharing platform” or “No, but now that you’re talking about it, I might create one using Squarespace/Wix/Wordpress with a ton of plugins”, which are all valid answers (well, except the Wordpress one: tons of plugins is never a good idea).

But you know us, developers: we are eager to build everything ourselves, so the obvious answer was “Not yet, but I will build one tailored to my exact needs”.

Link to section The inspiration

Before this conversation, I already had an itch to completely redo my personal website for something more streamlined (as I explained in A Clean Slate) and had a vision for a photo gallery that would look a lot like Google Photo’s handsome looking gallery.

Screenshot of a Google Photos album

At some point I even toyed with using its API to fetch and display pictures from Google Photos on the web, as it already has all my pictures (I might talk about this in a future post). I finally scrapped this idea as Google Photo’s API doesn’t fit my use-case: it’s really intended for Android OEMs to integrate Google Photo’s features into their galleries.

Link to section The architecture

Back to this conversation back in September: as we talk, I start thinking of a simple solution. I already knew I wanted the next iteration of my website to be static for obvious security, cost and performance reasons.

I ended using a static site generator for this exact website you’re browsing, but not for my gallery project, for the following reasons:

My second option was then to develop a webapp: I could embed it in one of my site’s page and it would live its life happily, fetching the data it needs from… Oh right, how do I manage the data representation of my albums?

Since I wanted this gallery to be cost-efficient, there was no way I would build, host and maintain a back-end. Instead I chose to store the data as JSON files alongside the pictures, in AWS S3.

Link to section The development

With the basics figured out, I went on to develop my webapp. I picked Angular because of my familiarity with the framework and as a way to learn features I have not used yet.

Once the core feature set done (list the albums, the pictures within an album and display a single picture) the next feature to tackle was displaying a picture’s information.

Screenshot of Photo Gallery displaying a picture's information

Hopefully someone already tackled the problem and developed a JS library to extract EXIF data from an <img> element. The hardest thing was eventually getting it to work with Angular.

In a nutshell:

Screenshot of Photo Gallery displaying an album

And then there was the Google Photos look. It first seemed difficult after reading Google’s article describing the design process behind their now iconic grid, but after a few searches, I found a simple CSS and JS solution that gave me the exact look I wanted. The final touch was adding the height and width attributes so that the thumbnails have the correct ratio and size before loading on supported browsers.

Obviously, to make my life easier, I built a script to upload and create albums. I chose Python as it is really well suited for handling JSON and has libraries for interacting with AWS’s services and ImageMagick, the go-to cross-platform image conversion software.

Link to section Efficiency

As I stated earlier, I wanted this gallery to be fast and cost-efficient. In order to achieve this, I worked on caching and filesize.

So in order to make the app feel fast and to not have to fetch the JSON from S3 every time the user would navigate to the albums list or an album page, I used Dexie.js −an IndexedDB wrapper I already used on my previous project Comme Chez Soito store the content of the JSONs and then act as an offline cache.

I then set to reduce the size of the pictures and their thumbnails with ImageMagick, which is incredibly easy to do using Wand, a Python binding for ImageMagick.

While I’m talking of Wand, here is a little tip: always use Image.auto_orient() so that ImageMagick will orient the picture according to the data from the EXIF, as web browsers do not always to it properly.

Finally, I used the incredible versatility of CloudFront to speed up the delivery of the pictures using its CDN feature, and I also added a Cache-Control header using a very simple Lambda@Edge function to tell the browser to keep the pictures in cache for 14 days.

Link to section What’s next?

Functionnaly, I’m pretty pleased with the result, but I think I can still squeeze some megabytes off my monthly CloudFront bill by using responsive images (srcset) to serve smaller thumbnails to small screens and using the WebP image format to serve pictures with even smaller file sizes.

These two will definitely be challenges as I will have to rewrite a good portion of the app to handle them!

Link to section April 28th’s update about the responsive images and WebP update

Well, handling responsive images and WebP was indeed a challenge and it was much more difficult than I originaly anticipated.

The first hurdle was generating WebP pictures: some builds of ImageMagick do not handle WebP conversion, which is the case of the version of ImageMagick in Ubuntu 18.04 LTS. To get arround this problem, I had to refactor the upload script to use Pillow instead of Wand, which was quite painful since Pillow’s API is quite different from Wand and has some dubious default behaviors:

After modifying the picture processing, I then had to change my data model to handle different sizes and format, which for each picture looks like this:

{
  "id": 1,
  "thumbnail": {
    // Default size, used when the browser does not handle <picture>
    "default": {
        "url": "https://www.zzz/album/thumbnail/picture.jpg",
        "with": 200,
        "height": 133
    },
    "sizes": {
      // For each media type, an array with the different thumbnail sizes
      "jpeg": [
        {
            "url": "https://www.zzz/album/thumbnail/picture-200.jpg",
            "with": 200,
            "height": 133
        },
        {
            "url": "https://www.zzz/album/thumbnail/picture-400.jpg",
            "with": 400,
            "height": 267
        },
        // ...
      ],
      "webp": [
        {
            "url": "https://www.zzz/album/thumbnail/picture-200.jpg",
            "with": 200,
            "height": 133
        },
        {
            "url": "https://www.zzz/album/thumbnail/picture-400.jpg",
            "with": 400,
            "height": 267
        },
        // ...
      ],
    }
  },
  // For fullsize pictures, same deal as thumbnails
  "fullsize": {
    // Default size, used when the browser does not handle <picture>
    "default": {
        "url": "https://www.zzz/album/picture.jpg",
        "with": 6000,
        "height": 4000
    },
    "sizes": {
      // For each media type, an array with the different thumbnail sizes
      "jpeg": [
        {
            "url": "https://www.zzz/album/thumbnail/picture-200.jpg",
            "with": 200,
            "height": 133
        },
        // ...
        {
            "url": "https://www.zzz/album/thumbnail/picture-1920.jpg",
            "with": 1920,
            "height": 1280
        }
      ],
      "webp": [
        // ...
      ],
    }
  }
}

Changing the datamodel also meant migrating the existing data:

For the data initially stored in the front-end’s IndexedDB, I ended up ditching Dexie as the browser already caches the JSON following the Cache-Control header. Using the browser cache is also more stable as some vendors are imposing constraints on IndexedDB: Apple for example announced that Safari will clear local storage if the website has not been visited in the last 7 days.

I also had to migrate my exisiting albums’ JSONs, so I wrote a migration script that, given an album ID:

Finally, I had to let go of parsing EXIF on the front-end as neither exif-js nor the more recent ExifReader and exifr are currently able to parse EXIF from WebP pictures, forcing me to parse the EXIF in the upload script using Pillow. At least it allowed me to shave a few dozen kilobytes off the generated Javascript!

It was frustrating at times, especially having to rewrite major parts of the app twice, but in the end it was worth it as thumbnails now load a lot faster on limited bandwith!