diff --git a/content/journal/encrypt-and-dither-photos-in-hugo/index.md b/content/journal/encrypt-and-dither-photos-in-hugo/index.md index 102361a..dbbe82c 100644 --- a/content/journal/encrypt-and-dither-photos-in-hugo/index.md +++ b/content/journal/encrypt-and-dither-photos-in-hugo/index.md @@ -1,7 +1,7 @@ --- title: "Encrypt and Dither Photos in Hugo" date: 2024-02-05T09:47:45-05:00 -draft: true +draft: false references: - title: "Elliot Jay Stocks | 2023 in review" url: https://elliotjaystocks.com/blog/2023-in-review @@ -21,44 +21,137 @@ references: file: solar-lowtechmagazine-com-vj7kk5.txt --- -Intro text here ... +I encrypted all the photos on this site and wrote a tiny image server that decrypts and dithers the photos, then created a Hugo shortcode to display dithered images in posts. It keeps high-res photos of my kid off the web, and it looks cool. -* https://github.com/gohugoio/hugo/issues/8598 +*** -A more ambitions version of me would take a crack at adding this functionality to Hugo and opening a PR. +When I was first setting up this site, I considered giving all the photos a monochrome [dithered][1] treatment à la [Low-tech Magazine][2]. Hugo has impressive [image manipulation functionality][3] but doesn't include dithering and [seems unlikely to add it][4]. I opted for full-color photos and went on with my life. + +[1]: https://surma.dev/things/ditherpunk/ +[2]: https://solar.lowtechmagazine.com/ +[3]: https://gohugo.io/content-management/image-processing/ +[4]: https://github.com/gohugoio/hugo/issues/8598 + +Most of what I post on this site are these monthly [dispatches][5] that start with what my family's been up to in the last month and include several high-resolution photos. Last week, I was reading Elliot Jay Stocks' "[2023 in review][6]," and he's adament about not posting photos of his kids. That inspired me to take another crack at getting dithered images working -- I take a lot of joy out of documenting our family life, and low-res, dithered images seemed like a good balance between giant full-color photos and not showing people in photos at all. And to add another wrinkle: this site is [open source][7], so I also needed to ensure that the source images wouldn't be available on GitHub. + +[5]: /tags/dispatch/ +[6]: https://elliotjaystocks.com/blog/2023-in-review +[7]: https://github.com/dce/davideisinger.com + +I tried treating the full-size images with ImageMagick on the command line and then letting Hugo resize the result, but I wasn't happy with the output -- there's still way too much data in a dithered 3000x2000px image, so when you scale it down, it just looks like a crappy black-and-white photo. Furthermore, the encoding wasn't properly optimizing for two-color images and so the files were larger than I wanted. + +I needed to find some way to scale the images to the appropriate size and _then_ apply the dither. Fortunately, Hugo has the ability to [fetch remote images][8], which got me thinking about a separate image processing service. After a late night of coding, I've got a solution I'm quite pleased with. Read on for more details. + +[8]: https://gohugo.io/content-management/image-processing/#remote-resource + +### 1. Encrypt all images + +We'll use OpenSSL to encrypt our images. I used [this guide][9] to get started. First, generate a secret key (the `-hex` option gives us something we can paste into a GitHub secret later): + +[9]: https://github.com/dce/davideisinger.com/blob/7285c58add56e2ac6b5f7bf62914f0615ac23c9f/.github/workflows/deploy.yml ```sh openssl rand -hex -out secret.key 32 ``` ---- +Don't forget to `gitignore` the key: ```sh -openssl \ - aes-256-cbc \ - -in secretfile.txt \ - -out secretfile.txt.enc \ - -pass file:secret.key \ - -iter 1000000 +echo secret.key >> .gitignore ``` ---- +Then use it to encrypt all the images in the `content` folder (I use an interactive Ruby shell for this sort of thing because I'm not very good at shell scripting): ```ruby Dir.glob("content/**/*.{jpg,jpeg,png}").each do |path| - `openssl aes-256-cbc -in #{path} -out #{path}.enc -pass file:secret.key -iter 1000000` + %x( + openssl \ + aes-256-cbc \ + -in #{path} \ + -out #{path}.enc \ + -pass file:secret.key \ + -iter 1000000 + ) end ``` -* https://gohugo.io/content-management/image-processing/#remote-resource +### 2. Build a tiny image server -## Deleting images out of Git history +I wrote a [very simple image server][10] using [Sinatra][11] and [MiniMagick][12] that takes a path to an image and an optional geometry string and returns a dithered image. I won't paste the entire file here but it's really pretty short and simple. -* https://stackoverflow.com/a/64563565 -* https://github.com/newren/git-filter-repo -* https://formulae.brew.sh/formula/git-filter-repo +[10]: https://github.com/dce/davideisinger.com/blob/bf5238dd56b6dfe9ee2f1d629d017b2075750663/bin/dither/dither.rb +[11]: https://sinatrarb.com/ +[12]: https://github.com/minimagick/minimagick + +Run it like this: + +```sh +cd bin/dither && \ + bundle install && \ + ROOT=[SITE_ROOT]/content \ + KEY=[SITE_ROOT]/secret.key \ + bundle exec ruby dither.rb +```` + +Then you should be able to visit in your browser (assuming you have an encrypted image at `content/path/to/file.jpg.enc`) to see it working. + +### 3. Create a Hugo shortcode to fetch dithered images + +We need to tell Hugo where to find our dither server. Give Hugo access to the `DITHER_SERVER` environment variable in `config.toml`: + +```toml +[security] + [security.funcs] + getenv = ['DITHER_SERVER'] +``` + +Then start Hugo like this: + +```sh +DITHER_SERVER=http://localhost:4567 hugo server +``` + +Now we'll create the shortcode ([`layouts/shortcodes/dither.html`][13]): + +```html +{{ $file := printf "%s%s" .Page.File.Dir (.Get 0) }} +{{ $geo := .Get 1 }} +{{ $img := resources.GetRemote (printf "%s/%s?geo=%s" (getenv "DITHER_SERVER") $file $geo) }} +{{ $imgClass := .Get 2 }} + + + + {{ with .Inner }} +
+ {{ . }} +
+ {{ end }} +
+``` + +Adjust for your needs, but the gist is: + +1. Construct a URL from `DITHER_SERVER`, the directory that the page lives in, the supplied file name, and the (optional) geometry string +2. Use `resources.GetRemote` to fetch the image +3. Display as appropriate + +[13]: https://github.com/dce/davideisinger.com/blob/2cda4b8f4e98bb9df84747da283d13075aac4d41/themes/v2/layouts/shortcodes/dither.html + +Use it like this: + +``` +{{}} +``` + +### 4. Delete the unencrypted images from the repository + +Now that everything's working, let's remove all the uncrypted images from the repository. It's not enough to just `git rm` them, since they'd still be present in the git history, so we'll use [`git filter-repo`][14] to rewrite the history as if they never existed. ```ruby Dir.glob("content/**/*.{jpg,jpeg,png}") do |path| @@ -66,6 +159,35 @@ Dir.glob("content/**/*.{jpg,jpeg,png}") do |path| end ``` +[14]: https://github.com/newren/git-filter-repo + +### 5. Tweak site styles + +The resulting images will be entirely black and white. If your site, like mine, doesn't use a perfectly white background, you can improve the display of the dithered images by setting `mix-blend-mode` to `multiply`: + +```css +img { + mix-blend-mode: multiply; +} +``` + +The blacks will still show as black, but the whites will now be the background color of your site. + +### 6. Update the deploy workflow + +This site uses GitHub Actions to deploy on pushes to the `main` branch, and we need to make a few updates to our workflow to generate the static site with dithered images: + +* Add the secret key as an Actions Secret +* Add workflow steps to + * Install Ruby and the required Gem dependencies + * Write the secret key to a file + * Start the dither server as a background task (i.e. with `&`) +* Add the `DITHER_SERVER` environment variable so that Hugo knows where to find it + +[Here's the deploy workflow for this site][15] for reference. + +[15]: https://github.com/dce/davideisinger.com/blob/7285c58add56e2ac6b5f7bf62914f0615ac23c9f/.github/workflows/deploy.yml + *** I'm 41 years old, and this stuff still gives me a buzz like it did when I was 14.