Reducing image size with ImageMagick and cwebp

🗒️ If you came here from an internet search and use Hugo for your website, just skip everything and go straight to Preparing a Hugo shortcode.

In the post describing the rationale behind using a single Org file for this website, I mentioned that “it would be pretty cool to have a place to share small trips with pictures and some comments”. Well, it is indeed really cool from my perspective as I have a single document describing what and how I felt at the moment. Well, you can also see that there’s also a mention of pictures there. What does this entail to this website?

This might be a bit obvious for some, but not so much for others, but I do aim to make this website as “lightweight” as possible. Why the quotes? Lightweight is really subjective, while some may think that I was successful with this goal, others might disagree due the 13 kB image this website currently loads on every single page1.

Another point is that I use Git to manage this website’s source code. It’s well known that managing large binary blobs with it is a pain and I want to avoid that.

Right, enough talking, what about requirements to aim for here?

Starting out, most of the pictures I take have some weird format, it is either Nikon’s NEF format or Apple’s HEIC. With this in mind, the first step would be to convert them to an easier format to work on.

Toying with image formats

Considering that we have a shell where ImageMagick is already available for use:

# convert all HEIC files to PNG
$ mogrify -format png *.HEIC

Now, the next step would be to reduce the image’s size. As you can see below, the converted image is quite big compared to the original HEIC image:

$ du -ah
17M   ./01.png
4.1M  ./01.HEIC

There are quite a few ways to reduce its size. You can reduce its resolution, reduce the color depth, change its format to a lossy one. In this case what I’m going to do is to use the cwebp command-line tool to resize and compress the picture. Alright, why webp? I chose it for two main reasons: it is supported by quite a few web browsers and has a really nice CLI.

🗒️ You might be thinking this already, but I know, this isn’t too fair! I’m comparing a lossless format with a lossy one.

Let’s run a test on our 01.png image first by running the following commands:

$ cwebp -q 80 01.png -o 01.webp -metadata all -resize 0 1200 -progress

# checking the size
$ du -ah
160K  01.webp
17M   01.png

Woah, this is a huge difference! However, how good is it really? Let’s compare the original image with the .webp one.


Zoom in on the PNG image originated from the original image.
Zoom in on the PNG image originated from the original image. (full size)

Zoom in on the WEBP image originated from the PNG image.
Zoom in on the WEBP image originated from the PNG image. (full size)

This opinion might differ from person to person, but this is more than good enough for me. The image lost some of its definition, but the shapes aren’t unrecognizable nor it lost most of its colors. This is perfectly fine for what I’m aiming for here.

Shell scripting our solution

The missing piece is a shell script that can do the hard work for me as I don’t want to copy and paste from this blog post every time. 🤪

Well, guess what I’m going to use for this? That’s right, Nix! Leveraging its package management features to help us always have the needed tools to transform our images.

apps.transform-images =
  let
    cwebp = "${libwebp}/bin/cwebp";
    identify = "${imagemagick}/bin/identify";
  in
utils.lib.mkApp {
  drv = writeShellScriptBin "transform-images" ''
    CWEBP_PARAMS=('-q 80 -metadata all -progress')

    shopt -s nullglob nocaseglob extglob

    for FILE in *.@(jpg|jpeg|tif|tiff|png); do
        ORIENTATION=$(${identify} -format '%[fx:(h/w)]' "$FILE")
        if (( orientation > 1 ))
        then
          SIZE="0 1000"
        else
          SIZE="1000 0"
        fi
        ${cwebp} $PARAMS -resize $SIZE "$FILE" -o "''${FILE%.*}".webp;
    done
  '';
};

This script loop through all images on the directory, makes sure that we resize the image to the proper size based on its orientation and then convert it to WEBP. All I need to do is run nix run .#apps.transform-images on my terminal.

Preparing a Hugo shortcode

Hugo has a really cool feature called Shortcodes. They allow you to write custom snippets that can do all kinds of transformation on your website’s content. Imagine that you have a task X that you don’t want to copy and paste every time, you can write down a shortcode taking some parameters and have it render the content for you.

My idea here is to leverage HTML’s Responsive Images feature and send a different image size based on the user screen size. This can be useful to reduce bandwidth usage as the user will download a smaller image.

{{/* possible sizes */}}
{{ $sizes := (slice "480" "800" "1200") }}

{{ $side := .Get "side" }}
{{ $src := resources.Get (.Get "src") }}
{{ $caption := .Inner | default "" }}

<div class="image image-{{- $side -}}">
  <figure>
    <img
      sizes="(min-width: 35em) 1200px, 100vw"

      {{/* Only resize if the image width size is bigger than the resize size. */}}
      srcset='
        {{ range $sizes }}
          {{ if ge $src.Width . }}
            {{ ($src.Resize (printf "%sx" .)).Permalink }} {{ (printf "%sw" .) }},
          {{ end }}
        {{ end }}'

      {{/* when no support for srcset (old browsers, RSS), we load small (800px) */}}
      {{/* if image smaller than 800, then load the image itself */}}
      {{ if ge $src.Width "800" }}
        src="{{ ($src.Resize "800x").Permalink }}"
      {{ else }}
        src="{{ $src.Permalink }}"
      {{ end }}
      alt="{{- $caption -}}" loading="lazy"/>

    {{ if $caption }}
      <figcaption><em>{{ $caption | markdownify }}</em> (<a href="{{ $src.Permalink }}">original</a>)</figcaption>
    {{ end }}
  </figure>
</div>

This code is basically a modified version of my previous shortcode and gist:cpbotha/figure.html.The cool part is that not only this will generate the HTML we need for the responsive images, but it will also convert the images to those specified sizes automatically.

Conclusion

At the end of this journey, I realized that the only thing holding me from using better quality images is Git. Hugo does a pretty good job2 at reducing image size with its Resize function and it is already part of the build process this website goes through anyway.

Hardware and network connection tend to get faster and faster as time passes by. In this case, it makes sense to keep the highest image resolution available on your source code, this way increasing it on the build step is as easy as modifying the shortcode.


  1. You will probably download it only a single time due to the browser caching it. ↩︎

  2. The only missing requirement was to generate images smaller than 180 kB. Although, this only happens on bigger screens that I assume to be a desktop or laptop. ↩︎


Links to this article


Articles from blogs I follow around the net

The four tenets of SOA revisited

Twenty years after. In the January 2004 issue of MSDN Magazine you can find an article by Don Box titled A Guide to Developing and Running Connected Systems with Indigo. Buried within the (now dated) discussion of the technology…

via ploeh blog March 4, 2024

Building a demo of the Bleichenbacher RSA attack in Rust

Recently while reading Real-World Cryptography, I got nerd sniped1 by the mention of Bleichenbacher's attack on RSA. This is cool, how does it work? I had to understand, and to understand something, I usually have to build it. Well, friends, that is what…

via ntietz.com blog March 4, 2024

How to unbreak Dolphin on SteamOS after the QT6 update

A recent update to Dolphin made it switch to QT6. This makes it crash with this error or something like it: dolphin-emu: symbol lookup error: dolphin-emu: undefined symbol: _Zls6QDebugRK11QDockWidget, version Qt_6 This is fix…

via Xe Iaso's blog March 3, 2024

Generated by openring