📅 Published 2021-10-09
It seems that dithering is undergoing a renaissance on Gemini. Not only does it introduce a pleasant retro aesthetic, but for images with smaller dimensions, dithering with a reduced palette can both save bandwidth and make content more accessible to old devices. (Believe it or not, an Amiga 500 may not handle a massive 24-bit color JPEG very well.) I've even seen it used outside of Gemini; for instance, Low Tech Magazine hosts a solar powered version of their website with dithered images.
For all of you Gemini users, here's an example of a dithered image using an 8 color palette. (Web users will see a normal, boring, undithered image. Sorry about that. You should really try Gemini.)
Thanks to Openclipart for the image. More woodblock prints are available (free for any use) on the same site.
In this context, I recently stumbled across makeworld's excellent tool "didder" for dithering images. It comes with a huge array of dithering algorithms, and it lets you tweak the results to create artistically minimalist graphics. It's also written in go and compiles to a nice small binary. A truly fantastic tool.
After finding didder, I decided to put together a script to quickly dither all of the images on the Gemini version of my site. Not only will it make my Gemini site more accessible for devices with less memory, but it should really help users who have restricted bandwidth. Now I'm a big fan of retrocomputing, but this last part matters to me much more than supporting ancient devices and hacked Linux toasters. Those of us who care about decentralization need to be focused on building simple, low power, low cost alternative networks, and many of the most promising options here have strict bandwidth constraints. But that's a post for another day!
I've packaged up my little script for anyone to use and named it "gemdither." Below I'll explain a little about what it can do and how it was built.
Although it is a great tool, didder is designed to operate on either one image at a time or a single directory of images. For a site with many images, scattered across multiple directories, running this command manually could be tedious. If you're like me, you prefer to automate this process, and that's where this script comes in.
I had a small set of must-have requirements:
I also had a couple of bonus requirements:
After a bit of work, I ended up with a Bash script (source) that fulfills all the requirements. It's not a huge script, but I put a decent amount of work into it, and it should hold up under most conditions.
Why Bash? Well, in retrospect, I'm not sure if that was a great choice. It's nice that it's just a script, meaning that people can download and run it without compiling, as long as the dependencies are installed. But Bash is *loose* for writing code that you plan to distribute, and `shellcheck` can only do so much for you.
I've never published a Bash script that's designed to be a robust CLI command, with proper handling of most corner cases. I think I'm like a lot of other folks in that I usually just write shell scripts as fill-in-the-gap sysadmin tools for myself or a small team. That is a different experience entirely! In such a situation, you can be fairly certain of the runtime conditions, and you often don't have to be as picky.
But you know what? In the end it wasn't so bad. After adding a bunch of error checking and escaping, I think I managed to eliminate all the casual footguns. I'm not saying you can't break it if you try to--please don't feed it untrusted input--but it should be solid for personal use, even if you're pure evil and you name a file `#! hello {;} .jpg .gif`.
Since my own build process uses Nix to manage dependencies, I also wanted gemdither to be installable using Nix. I created a simple default.nix file that builds didder (which is not yet packaged) and pulls in dependencies.
One thing that was not clear from the Nix documentation was how to package a shell script. After some digging, I found a couple of examples and adapted those. Sadly, it isn't quite as simple as using a hypothetical "buildShellScript" function. The most flexible approach, usable for people without Nix, is to create a Makefile to handle installation. Then you can have Nix's `stdenv.mkDerivation` patch the script with correct dependency paths during postFixup. (If you are new to Nix, you may want to browse the documentation on stdenv build phases to learn more about phases.)
The `mkDerivation` portion of `default.nix` looks like this:
stdenv.mkDerivation rec { inherit pname version; nativeBuildInputs = []; buildInputs = [ bash getopt didder file ]; makeFlags = [ "DESTDIR=$(out)" ]; src = builtins.path { path = ./.; name = "gemdither"; }; postFixup = let runtimePath = lib.makeBinPath buildInputs; in '' sed -i "2 i export PATH=${runtimePath}:\$PATH" $out/bin/gemdither ''; meta = with lib; { description = "Dither and resize images for a Gemini site and update internal image links"; homepage = "https://sr.ht/~mntn/gemdither"; license = licenses.gpl3; platforms = platforms.linux; }; }
I'll walk you through the most important parts below.
When working on a project like this, you can check that everything builds properly by running `nix-build` and inspecting the output. You can also use `nix-shell` to enter a shell and observe the results of the individual build phases. Pretty useful!
Now that we have a script, I have to include it into the build process. Since gemdither has its own `default.nix`, it is easy to include this as a package in my website's own `default.nix`:
gemdither = import ( builtins.fetchTarball { name = "gemdither-0.0.5"; url = "https://git.sr.ht/~mntn/gemdither/archive/0.0.5.tar.gz"; sha256 = sha256:0a6q2zrx660akci2zz87a9w2q55hrr1lv1dnyrjbc071y14xzfs1; }
This fetches the tagged tarball from the Sourcehut repository and checks the SHA256 hash. The package will be built automatically. Not bad! To see the complete context, you can view the default.nix source for my site.
Once gemdither is in place as a dependency, I simply updated my build aliases for local testing and my `.build.yml` file for automatic builds on Sourcehut. Now whenever I commit a change, the process will run automatically, and I have nicely dithered images on my Gemini site. Living like it's 1995 again!
---
Comments? Email the author: mntn at mntn.xyz