Many of the makers I follow (including my friend Michael) write up weekly notes about their projects. It's a nice oportunity to take stock, and--especially if you're a one-person shop--remember you're making progress. Since I've been taking a break from my day job and spending far more time on my side projects, I thought it might be nice to try it out for myself.

In order to keep things flowing and not get drawn into long explanations, I plan to focus on what I've done in the past week, reserving more in-depth discussion for dedicated posts. If you see something you'd like me to elaborate on, let me know.


Mini Worlds and InContext

I've wanted to generate 'mini world' fisheye thumbnails for my 360° photos ever since I got my Ricoh Theta. Actually doing so has been an exercise in working out how to make InContext extensible enough that I can easily add the fisheye handling to my website without 'tainting' a generic tool I hope others will use one day.

Generating Fisheye Images

pano2fisheye comes with some amazing example images

Before getting too deep into things, I wanted to check that it would be easy enough to actually generate fisheye images from an equirectangular projection (as output by my camera). A quick search led me to Fred's ImageMagick Scripts, and pano2fisheye. As you can see from the opening image, pano2fisheye can do far more exciting things than I need, but it will also happily generate a simple fisheye image. It's not the fastest, but good enough to get me started, and I can always find something else in the future.1

Specifying Image Transforms

Having determined I could generate the desired images, I set about adding support for user-specified image transforms to InContext. Up to now, the set of image transforms has been hard-coded into InContext, making it impossible for a site author to even specify the output size of images.

To tackle this limitation, I've updated the image file handler to look for a new configuration file, media.yaml, which looks something like this:

profiles:
  default:
  - where: glob("*.heic") or glob("*.tiff")
    transforms:
    - resize("large", width=1600, format="image/jpeg", sets=["image", "previews"])
    - resize("small", width=480, format="image/jpeg", sets=["thumbnail", "previews"])
  - where: glob("*")
    transforms:
    - resize("large", width=1600, sets=["image", "previews"])
    - resize("small", width=480, sets=["thumbnail", "previews"])

This simple example generates two output images for every input image (one at 1600px, and the other at 480px), preserving the aspect ratio of the original. The where field uses a Python-like DSL to express glob pattern-matching on the filename, ensuring that HEIC and TIFF images are always converted to JPEGs, while all other image types are preserved.

The document associated with the transformed image (available to templates at render time) looks something like this:

{
  "url": "/posts/2021-01-24-copy-and-paste/terminal/",
  "parent": "/posts/2021-01-24-copy-and-paste/",
  "path": "/posts/2021-01-24-copy-and-paste/terminal.png",
  "template": "photo.html",

  "image": {
    "filename": "terminal/large.png",
    "height": 1066.0,
    "url": "/posts/2021-01-24-copy-and-paste/terminal/large.png",
    "width": 1572.0
  },

  "thumbnail": {
    "filename": "terminal/small.png",
    "height": 325.0,
    "url": "/posts/2021-01-24-copy-and-paste/terminal/small.png",
    "width": 479.0
  },

  "previews": [{
    "filename": "terminal/large.png",
    "height": 1066.0,
    "url": "/posts/2021-01-24-copy-and-paste/terminal/large.png",
    "width": 1572.0
  }, {
    "filename": "terminal/small.png",
    "height": 325.0,
    "url": "/posts/2021-01-24-copy-and-paste/terminal/small.png",
    "width": 479.0
  }],

  "scale": 1
}

The image handler transform operation adds three properties to the document: image, thumbnail, and previews. I debated how to best allow users to specify this, and settled on the sets parameter in the transform operations; named sets with more than one image are automatically promoted to array with the idea being that templates can easily use this array to easily generate a srcset of multiple image resolutions. This is very much a first-cut, and I suspect I'll revisit some of the syntax before I'm done, but it gives a site author a huge amount of flexibility when paired with custom templates.

Custom Plugins

Once it was possible to richly express the transforms (using the same Python-like DSL used for the where field), the next step was to introduce a new site-specific transform, fisheye, allowing me to generate fisheye thumbnails under certain conditions. The goal was to get to the point that adding an additional set of transforms at the beginning of the configuration file would allow me to generate fisheye thumbnails for all images with an equirectangular projection:

profiles:
  default:
  - where: metadata(projection="equirectangular")
    transforms:
    - resize("large", width=10000, sets=["image"])
    - fisheye("preview-small", width=480, format="image/png", sets=["thumbnail", "previews"])
    - fisheye("preview-large", width=960, format="image/png", sets=["previews"])
  - where: ...

Since the built-in resize operation is already a plugin itself (a design principal of InContext), I tested this out by quickly adding a built-in fisheye plugin and, having proven it was working, set about adding support for site-specific plugins.

Pulling everything together, was then a matter of moving the fisheye-sepcific code into a reassuringly lightweight site-specific plugin:

import os.path
import subprocess

import handlers.gallery as gallery

class Fisheye(gallery.Resize):

    def perform(self, source, destination):
        subprocess.check_call(["pano2fisheye",
                               "-d", str(self.width),
                               "-b", "transparent",
                               source,
                               destination])
        destination_root, destination_basename = os.path.split(destination)
        document = gallery.get_details(destination_root, "", destination_basename)
        return gallery.TransformResult(files=[destination], documents=[document])


def initialize_plugin(incontext):
    incontext.add_plugin(gallery.IMAGE_HANDLER_TRANSFORM_PLUGIN, "fisheye", Fisheye)

Final Results

I'm pretty pleased with the outcome. Without introducing any fisheye-specific code to InContext, I'm now able to generate 'mini world' thumbnails for my gallery overviews, while still performing a straight resize for the 'full size' image to be used in my WebGL viewer:

360° photos from our recent escape to Willow Creek

Custom Bookmarks

I dislike physical bookmarks; the kind of things with tassels, bought at country houses or museums, and variously adorned with motivational phrases: the tassels mean they invariably get caught and pulled out of books, causing you to loose your place; the designs, distracting; they're often big and bulky; and, as someone who has multiple books on the go, there are never enough to go around.

Finding myself with too much time on my hands (and reading more than usual), I put a little thought into something more suited to my needs. I decided my ideal bookmarks would be small, unobtrusive, beautiful, and multitudinous (in a way that means you're not sad if you misplace one). Fortunately, I realised there's already something well established and easily customisable that fits the bill: business cards.

To quickly try out the idea, I ordered 50 blank cards from Moo. They have a design that's almost perfect for books (Marble Marvel), which mirrors the classic marbling inside old hardback books:

So far, the user experience has been great: I have enough bookmarks to leave in every book I'm reading, I enjoy the more minimalist aesthetic, and I get to bask in the warm glow of a problem solved.

In the future, I'd love to explore letter-pressing or plotting my own bookmarks, perhaps using some of the designs from pattern.js.

Calendar Summaries

Having lived abroad for many years (and encountered many tax authorities), I've got into the habit of recording where I am and what I'm doing every day of the year. I use a fairly lightweight process whereby I add all-day events to a separate calendar: it does feel a bit clunky, but it saves me a lot of time when filing my taxes and countries are arguing over who gets my hard-earned cash.

Every year, while manually tallying up the days for my tax filing, I promise myself I'll knock up an app to make it easier. This year, I did exactly that:

The app performs a very simple task: it sums the amount of time, per-month, attributed to identically named calendar entries. There are still a few bugs to work on (eagle-eyed readers will notice the monthly totals are incorrect), but I'm pretty pleased with the results and have published it on GitHub. It's already saved me a bunch of time.

Long-term, I'd love to update the app to offer different ways to cut the data as I think it could be fascinating. Taking the time to write a calendaring app has also reminded me that I'd like to have a go at improving long-term scheduling by improving year-timescale calendar visualisations.


  1. Unfortunately, pano2fisheye does have a pretty unhelpful, non-explicit, license, so I'll have to make sure I keep it out of InContext.