index.md (9271B)
1 --- 2 title: "Encrypt and Dither Photos in Hugo" 3 date: 2024-02-06T23:00:00-05:00 4 draft: false 5 tags: 6 - meta 7 references: 8 - title: "Ditherpunk — The article I wish I had about monochrome image dithering — surma.dev" 9 url: https://surma.dev/things/ditherpunk/ 10 date: 2024-02-05T14:50:25Z 11 file: surma-dev-e4sfuv.txt 12 - title: "About the Solar Powered Website | LOW←TECH MAGAZINE" 13 url: https://solar.lowtechmagazine.com/about/the-solar-website/ 14 date: 2024-02-05T14:50:28Z 15 file: solar-lowtechmagazine-com-vj7kk5.txt 16 - title: "Elliot Jay Stocks | 2023 in review" 17 url: https://elliotjaystocks.com/blog/2023-in-review 18 date: 2024-02-02T15:51:48Z 19 file: elliotjaystocks-com-fcit8u.txt 20 - title: "Encrypt and decrypt a file using SSH keys" 21 url: https://www.bjornjohansen.com/encrypt-file-using-ssh-key 22 date: 2024-02-05T14:50:24Z 23 file: www-bjornjohansen-com-hqud3x.txt 24 --- 25 26 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. 27 28 <!--more--> 29 30 *** 31 32 **Update 2024-02-12:** [Hugo will support native dithering in the next release.][1] If you're after the lo-fi look and don't need encryption, that'll be a lot cleaner than the approach outlined below. 33 34 [1]: https://github.com/gohugoio/hugo/pull/12016 35 36 When I was first setting up this site, I considered giving all the photos a monochrome [dithered][2] treatment à la [Low-tech Magazine][3]. Hugo has impressive [image manipulation functionality][4] but doesn't include dithering and [seems unlikely to add it][5]. I opted for full-color photos and went on with my life. 37 38 [2]: https://surma.dev/things/ditherpunk/ 39 [3]: https://solar.lowtechmagazine.com/about/the-solar-website/#dithered-images 40 [4]: https://gohugo.io/content-management/image-processing/ 41 [5]: https://github.com/gohugoio/hugo/issues/8598 42 43 Most of what I post on this site are these monthly [dispatches][6] 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][7]," and he's adamant 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 strike 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][8], so I also needed to ensure that the source images wouldn't be available on SourceHut. 44 45 [6]: /tags/dispatch/ 46 [7]: https://elliotjaystocks.com/blog/2023-in-review 47 [8]: https://git.sr.ht/~dce/davideisinger.com 48 49 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 full-sized 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. 50 51 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][9], 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. 52 53 Here's a picture of me, before and after dithering: 54 55 {{<thumbnail race_121539.jpg "782x900" />}} 56 {{<dither race_121539.jpg "782x900">}}Runner No. 534 pushes through the final stretch, focused and strong as he nears the finish line in an overcast parking lot race.{{</dither>}} 57 58 Read on for more details, and if you want to follow along, you'll need to have Ruby installed (I recommend [asdf][10] if you're on a Unix-y OS) as well as ImageMagick and OpenSSL. 59 60 [9]: https://gohugo.io/content-management/image-processing/#remote-resource 61 [10]: https://asdf-vm.com/ 62 63 ### 1. Encrypt all images 64 65 66 We'll use OpenSSL to encrypt our images ([here's a guide][11]). First, we'll generate a secret key (the `-hex` option gives us something we can easily copy/paste): 67 68 [11]: https://www.bjornjohansen.com/encrypt-file-using-ssh-key 69 70 ```sh 71 openssl rand -hex -out secret.key 32 72 ``` 73 74 [Make a backup][12] of the key and then `gitignore` it: 75 76 [12]: https://bitwarden.com/ 77 78 ```sh 79 echo secret.key >> .gitignore 80 ``` 81 82 Then we'll use the key 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: 83 84 ```ruby 85 Dir.glob("content/**/*.{jpg,jpeg,png}").each do |path| 86 %x( 87 openssl \ 88 aes-256-cbc \ 89 -in #{path} \ 90 -out #{path}.enc \ 91 -pass file:secret.key \ 92 -iter 1000000 93 ) 94 end 95 ``` 96 97 ### 2. Build a tiny image server 98 99 I made a [standalone image server][13] using [Sinatra][14] and [MiniMagick][15] that takes a path to an encrypted 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. 100 101 [13]: https://git.sr.ht/~dce/davideisinger.com/tree/bf5238dd56b6dfe9ee2f1d629d017b2075750663/bin/dither/dither.rb 102 [14]: https://sinatrarb.com/ 103 [15]: https://github.com/minimagick/minimagick 104 105 If you want to run it yourself, copy down everything in the [`bin/dither`][16] folder and then run the following: 106 107 [16]: https://git.sr.ht/~dce/davideisinger.com/tree/bf5238dd56b6dfe9ee2f1d629d017b2075750663/bin/dither 108 109 110 ```sh 111 > cd bin/dither 112 > bundle install 113 > ROOT=../../content \ 114 KEY=../../secret.key \ 115 bundle exec ruby dither.rb 116 ``` 117 118 Then, assuming you have an encrypted image at `content/path/to/file.jpg.enc`, you should be able to visit [localhost:4567/path/to/file.jpg?geo=400x300](http://localhost:4567/path/to/file.jpg?geo=400x300) in your browser to see it working. 119 120 ### 3. Create a Hugo shortcode to fetch dithered images 121 122 We need to tell Hugo where to find our image server, which we'll supply with an environment variable. First, we'll give Hugo access to `DITHER_SERVER` in `config.toml`: 123 124 ```toml 125 [security.funcs] 126 getenv = ['DITHER_SERVER'] 127 ``` 128 129 Then start Hugo like this: 130 131 ```sh 132 DITHER_SERVER=http://localhost:4567 hugo server 133 ``` 134 135 Now we'll create the shortcode ([`layouts/shortcodes/dither.html`][17]): 136 137 ```html 138 {{ $file := printf "%s%s" .Page.File.Dir (.Get 0) }} 139 {{ $geo := .Get 1 }} 140 {{ $img := resources.GetRemote (printf "%s/%s?geo=%s" (getenv "DITHER_SERVER") $file $geo) }} 141 {{ $imgClass := .Get 2 }} 142 143 <a href="{{ $img.RelPermalink }}"> 144 <img src="{{ $img.RelPermalink }}" 145 width="{{ $img.Width }}" 146 height="{{ $img.Height }}" 147 class="{{ $imgClass }}" 148 > 149 {{ with .Inner }} 150 <figcaption> 151 {{ . }} 152 </figcaption> 153 {{ end }} 154 </a> 155 ``` 156 157 Adjust for your needs, but the gist is: 158 159 1. Construct a URL from `DITHER_SERVER`, the directory that the page lives in, the supplied file name, and the (optional) geometry string 160 2. Use `resources.GetRemote` to fetch the image 161 3. Display as appropriate 162 163 [17]: https://git.sr.ht/~dce/davideisinger.com/tree/2cda4b8f4e98bb9df84747da283d13075aac4d41/themes/v2/layouts/shortcodes/dither.html 164 165 Use it like this: 166 167 ``` 168 {{</*dither IMG_2374.jpeg "782x1200" /*/>}} 169 ``` 170 171 ### 4. Delete the unencrypted images from the repository 172 173 Now that everything's working, let's remove all the unencrypted images from the repository. It's not enough to just `git rm` them, since they'd still be present in the history, so we'll use [`git filter-repo`][18] to rewrite the history as if they never existed. 174 175 ```ruby 176 Dir.glob("content/**/*.{jpg,jpeg,png}") do |path| 177 `git filter-repo --invert-paths --force --path #{path}` 178 end 179 ``` 180 181 [18]: https://github.com/newren/git-filter-repo 182 183 ### 5. Tweak site styles 184 185 The resulting images will be entirely black and white, and this site doesn't use a pure white background color. We can improve the display of the dithered images with some CSS that sets `mix-blend-mode` to `multiply`: 186 187 ```css 188 img { 189 mix-blend-mode: multiply; 190 } 191 ``` 192 193 The blacks will still show as black, but the whites will now be the background color of the site. 194 195 ### 6. Update the deploy workflow 196 197 This site uses [SourceHut Builds][19] 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: 198 199 * Add the decryption key as a secret 200 * Add workflow steps to 201 * Install Ruby and the required Gem dependencies 202 * Start the dither server as a background task (using `rackup` with the `-D` option) 203 * Add the `DITHER_SERVER` environment variable to the build step so that Hugo knows where to find it 204 205 [Here's the deploy workflow for this site][20] for reference. 206 207 [19]: https://builds.sr.ht/ 208 [20]: https://git.sr.ht/~dce/davideisinger.com/tree/main/.build.yml 209 210 *** 211 212 This was super fun to build, and I'm really happy with [the result][21]. It makes the local authoring and deploy processes a bit more complicated since we have to run the separate image server, but I think the result is worth it. Hope you found this interesting, and please [reach out](mailto:[email protected]) if you have any thoughts or questions. 213 214 [21]: /journal/dispatch-12-february-2024/