Optimize your PNGs with oxipng and pre-commit

Artist’s impression of a PNG optimizer.

PNGs can be losslessly optimized to reduce their file size, by picking a better compression algorithm for the given image. The savings can be significant, like 50% (especially for macOS screenshots). This is an easy way to make web sites load faster.

For a long time I used the pngcrush. It works okay, but its CLI is a bit clunky, and it doesn’t support nice features like parallel processing.

I recently came across oxipng as a modern, Rust-based alternative. It’s fantastic! I found it can shrink PNGs further, and faster thanks to its use of threading.

I wanted to run oxipng under pre-commit. This way, every time I added a PNG to my repositories, it would be optimized before being committed. To create the pre-commit hook, I took a path of configuring it in three different ways.

(If you just want to set up oxipng, skip to level 3.)

Level 1: Local Repository Hook

Initially I installed oxipng with cargo, and used a pre-commit system hook:

repos:
-   repo: local
    hooks:
    -   id: oxipng
        name: oxipng
        entry: oxipng
        language: system
        types: [png]

This worked well for individual use, but it isn’t ideal for teams. Every developer has to install and update oxipng themselves, and different developers could use different versions. Also the whole hook definition needs copying to each repository.

So it’s better to use configuration in a separate repository, which allows pre-commit to handle isolated installation and updates. This is what I did next.

Level 2: Mirror Repository

I set up a “mirror” repository on my account, adamchainz/pre-commit-oxipng. This contains a .pre-commit-hooks.yaml file that tells pre-commit to install oxipng in a separate environment, and how to run the hook. Project repositories could then use it like so:

-   repo: https://github.com/adamchainz/pre-commit-oxipng
    rev: v5.0.1
    hooks:
    -   id: oxipng

I made this repository with pre-commit-mirror-maker. This is a small tool that can create the hook configuration, and update it when new versions of the underlying tool are released. It’s used for “official” mirror repositories like mirrors-mypy.

My oxipng mirror worked well, but it’s a shame that it requires a stanadalone repository with the mirror maker running daily to pull in new versions. It’s easier if the upstream tool repository can maintain the configuration...

Level 3: Upstream Inclusion

Update (2023-03-12): Updated this section to cover pre-commit 2.21+ and oxipng 8.0.0.

I opened an issue on the official oxipng repository, asking if it could include .pre-commit-hooks.yaml. The creator, Josh Holmer, replied yes, so I made my first pull request to a Rust project!

So now you can use oxipng under pre-commit like so:

- repo: https://github.com/shssoichiro/oxipng
  rev: v8.0.0
  hooks:
  - id: oxipng

Since pre-commit 2.21.0, pre-commit will install Rust for you.

The above version in rev is the version at time of writing. When there’s a new oxipng version, pre-commit autoupdate will update this to the new tag.

One thing to note: optimizing all the PNGs in your repository is expensive and fairly pointless after they’ve been optimized. You probably want to skip oxipng on your CI system, and run it only on commit. On pre-commit.ci declare it to be skipped like so:

ci:
  skip:
  - oxipng

On other CI systems, or when running locally, use pre-commit’s SKIP environment variable:

$ SKIP=oxipng pre-commit run --all-files

Fin

I enjoyed this open source exercise. I saw how pre-commit’s configuration model (YAML in Git repositories) is flexible. It gives us a different ways to try out tools as hooks, and make them “official”.

May your PNGs be well optimized,

—Adam


Learn more about pre-commit in my Git DX book.


Subscribe via RSS, Twitter, Mastodon, or email:

One summary email a week, no spam, I pinky promise.

Related posts:

Tags: