summaryrefslogtreecommitdiff
path: root/source/blog/2023/website-adventures-images/index.md
blob: d67836212ef5b448afbddb71dd2c0a0f28442986 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
---
title: "Adventures in making this website:<br>image optimization"
date: 2023-02-27
layout: "blog"
toc: true
---

Published on 2023-02-27.

Making and managing this personal website has been an adventure.
In this series, I go over the technical challenges I've encountered
and philosophical decisions I've made,
and I review some of the tools I've used along the way.
After [part 1](/blog/2022/website-adventures-generators/),
[part 2](/blog/2022/website-adventures-basics/)
and [part 3](/blog/2022/website-adventures-maths/),
this is part 4.



## Principles

After some consideration,
I settled on the following five commandments of image optimization
for this website:

1.  **Provide the originals.**
    If any scaling or lossy compression has been done
    on an image displayed on the page,
    wrap it in `<a>` linking to the full-size lossless original file.

2.  **Prioritize quality**.
    My server provider doesn't charge me for traffic,
    and there aren't many images on this website,
    so I can afford to keep the quality high.
    The degradation of an 80% JPEG compared to 90%
    isn't worth it just to save a few kilobytes.
    You should do your own tests to decide what quality
    is acceptable for the kind of images you have.

3.  **Limit the resolutions.**
    It's wasteful to use a 2000px wide image
    if it's only 200px wide on a user's screen.
    I use the following rules to choose resolutions:

    1.  If an image is `W`px wide my screen,
        its resolution should be roughly `1.5*W`px:
        a 50% surplus is friendly to high-DPI screens,
        and prevents scaling artefacts.
        This rule also works in reverse:
        if I have an `X`px file for whatever reason,
        its scale should be chosen such that it takes up around `0.67*X`px on my screen.

    2.  Because my website's `<body>` is ~720px wide for me,
        the largest sensible image width is ~1080px.
        But sometimes more is needed to show all details:
        in that case, display a downscaled image on the page,
        linking to the original.
        When downscaling, always divide the width and height
        by an integer divisor of both.

4.  **Use modern formats.**
    The JPEG standard was released in 1992,
    and in the three decades since then,
    a lot of research has been done in image and video compression.
    If a modern format can shrink files by 75%
    with little to no change in quality, why wouldn't I use it?

5.  **Maintain compatibility.**
    Modern image formats are great, but some are so modern
    that almost nobody can use them, so fallbacks should be provided.
    Fortunately, HTML has a built-in way to do this, see below.

The HTML `<picture>` tag can be used to provide multiple formats
depending on what the browser supports,
or multiple resolutions depending on the user's screen size
(I don't use this feature as of writing).
`<picture>` is supported by all modern browsers,
but obviously [not IE11](https://caniuse.com/picture),
although that doesn't matter, because backwards compatibility is baked into it!
Here it is:

```html
<!-- I wrap the whole image in a link to the original file -->
<a href="file.png">
  <picture>
    <!-- Various image source files are listed in order of preference:
    the browser will use whichever one fulfills its conditions first. -->
    <source srcset="file.avif" type="image/avif">
    <!-- Each `<source>` should include the MIME type, so the browser
    can know which format it is without needing to download the file. -->
    <source srcset="file.webp" type="image/webp">
    <!-- If no `<source>` is suitable, or the browser doesn't support
    `<picture>`, a traditional `<img>` at the bottom is used instead. -->
    <img src="file.jpg" alt="..." title="...">
    <!-- Other attributes like `alt` and inline CSS are always taken
    from `<img>`, even if a `<source>` tag is used to fetch the file. -->
   </picture>
</a>
```

To automate rules 1, 3, 4 and 5, I wrote a Jekyll template:
[`image.html`](/code/prefetch-jekyll/tree/source/_includes/image.html).
To handle rule 3 there's a bit of logic, but the idea is as follows:

*   If the file is small enough not to need downscaling,
    provide an AVIF and the original JPEG or PNG (in that order).
    In my testing, WebP didn't help much for such tiny images.
*   For larger images, the full-size file ends in `-full`,
    and the half-size file in `-half`. The template detects this,
    and then displays the downscaled AVIF, WebP and JPEG versions,
    wrapped in a link to the original PNG/JPEG.

Let me tell you how I squeezed every drop out of those PNG, JPEG, WebP and AVIF files.



## Optimizing each format

I will use the test image given below, which shows a simulation of
[modulational instability](/know/concept/modulational-instability/)
in an optical fiber.
The raw file, as exported by [Veusz](https://veusz.github.io/),
is a 1,207,860-byte (1.21MB, 1.15MiB) PNG
with a resolution of 2362x1062, or 2,508,444px&sup2; (2.51MP).
Click for lossless full-size view:

{% include image.html file="simulation-full.png"
    width="100%" alt="Modulational instability simulation results" %}

This is representative of the kind of pictures I have on this website,
and it has a combination of high-contrast text and lines, gradual colour transitions,
and fine details (especially at the top of the left panel).
Depending on what images you want to optimize,
your mileage may vary if you take my advice.
Figure out what works for you, and I hope this post helps with that.



### PNG

Despite dating from the late 1990s,
[PNG](https://en.wikipedia.org/wiki/PNG) is still pretty solid.
Its losslessness makes it by far the largest format discussed here,
but it would be unfair to hold that against it.
For full quality, PNG is the perfect choice
before more exotic formats like TIFF.

But not all lossless compression is the same.
Given a PNG image, you may be able to reduce its size
using [Oxipng](https://github.com/shssoichiro/oxipng),
which plays with the file's internal knobs
to squeeze every last drop out of the compression algorithms.
Let's dial all settings to eleven and see what it can do:

```sh
$ oxipng --strip=all --alpha --filters=9 --zc=12 image.png
# 1,207,860 to 982,403 bytes: reduced by 225,457 bytes (18.7%)
# Took 2s on my system (oxipng 8.0.0)
```

A 16.4% reduction for losslessly compressed data is pretty impressive.
`--strip=all` removes all unneeded metadata,
`--alpha` enables transparency optimizations,
e.g. removing the A channel from an opaque RGBA image,
and `--zc=12` sets the highest [Deflate](https://en.wikipedia.org/wiki/Deflate) level.

`--filters=9` tells Oxipng to brute-force the PNG's per-row
[filtering](https://en.wikipedia.org/wiki/PNG#Filtering) setting,
which always gave the best results in my tests.
I you want, you can instead use `--opt=max` to try all filtering approaches
(including `9`): this is slower, but may be better for some images?
I'm not sure.

But can we go smaller? Yes, in fact we can, although it comes at a cost.
Yes, that's over 2 minutes to optimize a single 2.5MP image:

```sh
$ oxipng --strip=all --alpha --filters=9 --zopfli image.png
# 1,207,860 to 973,478 bytes: reduced by 234,382 bytes (19.4%)
# Took 2m9s on my system (oxipng 8.0.0)
```

So what does `--zopfli` do? Well, [Zopfli](https://github.com/google/zopfli)
is a Google-made compressor aiming to minimize file sizes
while being Deflate-compatible.
Basically, it's [zlib](https://zlib.net/) but better.
Interestingly, Zopfli ships with its own PNG optimizer `zopflipng`...
Let's see what it can do:

```sh
$ zopflipng --iterations=15 --filters=b -y input.png output.png
# 1,207,860 to 975,943 bytes: reduced by 231,917 bytes (19.2%)
# Took 1m3s on my system (zopfli 1.0.3)
```

You can increase the number of iterations if you want to,
but the returns diminish strongly,
and it wasn't enough to beat Oxipng in a reasonable time.
Time is the key word:
no, `zopflipng` doesn't beat Oxipng's Zopfli mode,
but it does save a lot of time!

File links:
[original PNG](png-original.png),
[Oxipng (no Zopfli)](png-oxipng.png),
[Oxipng (Zopfli)](png-oxipng-zopfli.png),
[Zopflipng](png-zopflipng.png),



### JPEG

While PNG has held up well, [JPEG](https://en.wikipedia.org/wiki/JPEG) hasn't really.
It's the most popular lossy compression format,
but its distinctive damage to the image's quality
is worse than modern alternatives,
and high-quality JPEGs tend to be quite large.

Figuring out how to optimize JPEGs is easy,
because someone else has already done the hard work for us:
[MozJPEG](https://github.com/mozilla/mozjpeg)
is a [friendly fork](https://libjpeg-turbo.org/About/Mozjpeg)
of the popular `libjpeg-turbo` package,
where Mozilla uses all the latest tech to make small, good-looking JPEGs:

```sh
$ cjpeg -quality 90 -outfile output.jpg input.png
# 1,207,860 to lossy 339,094 bytes: reduced by 868,766 bytes (71.9%)
# Took 0.2s on my system (MozJPEG 4.1.1)
```

But what if you don't have a PNG, only a non-optimized JPEG?
Then you can choose between:

```sh
$ jpegtran -copy none -outfile output.jpg input.jpg
$ jpegoptim --strip-all image.jpg # separate package
```

And as long as you have MozJPEG installed,
these will squeeze every drop out of your file.
If you can't use MozJPEG for some reason,
`jpegoptim` can still help a bit.

If you're feeling adventurous, you can pass the `-arithmetic` flag
to `cjpeg` or `jpegtran` to employ [arithmetic coding](https://en.wikipedia.org/wiki/Arithmetic_coding)
instead of [Huffman coding](https://en.wikipedia.org/wiki/Huffman_coding),
which knocks a few percent off the size:

```sh
# Don't actually do this!
$ cjpeg -quality 90 -arithmetic -outfile output.jpg input.png
# 1,207,860 to lossy 308,893 bytes: reduced by 898,967 bytes (74.4%)
# Took 0.4s on my system (MozJPEG 4.1.1)
```

The problem is that many image viewers (including browsers)
can't display these files because arithmetic coding was patented,
so implementations couldn't just be freely distributed or used.
The patents have expired now, but the world hasn't caught up yet.

File links:
[normal JPEG](jpeg-huffman.jpg),
[arithmetic JPEG](jpeg-arithmetic.jpg).
Bonus points if you can open the latter.



### WebP

[WebP](https://en.wikipedia.org/wiki/WebP) is the Swiss army knife of formats,
supporting both lossless and lossy compression of static or animated images.
It's only been around for a few years, and hasn't gained so much popularity,
probably because it was immediately overshadowed by AVIF (to be discussed below).
Internally, it relies on VP8 video compression:
this provides a nice size reduction,
but the images tend to get blurred slightly,
and low-contrast areas can become blocky.

Indeed high-quality JPEGs look better than high-quality WebPs,
but the latter tend to be less than half the size, so it isn't bad at all.
According to my tests, the best way to create a WebP is:

```sh
$ cwebp -q 90 -m 6 -sharp_yuv -sns 100 input.png -o output.webp
# 1,207,860 to lossy 139,868 bytes: reduced by 1,067,992 bytes (88.4%)
# Took 0.3s on my system (libwebp 1.3.0, libsharpyuv 0.2.0)
```

The flags `-q 90` and `-m 6` set the quality to 90%
with the slowest compression method.
The most important setting is `-sharp_yuv`,
which counteracts a lot of the blurriness
and more accurately preserves the contrast,
at the cost of slightly larger files.
`-sns 100` has a big effect on file size, without much influence on quality.

I don't use WebP's lossless mode, but according to
[this post](https://siipo.la/blog/whats-the-best-lossless-image-format-comparing-png-webp-avif-and-jpeg-xl)
it's one of the best out there, so I had a quick look,
and it's indeed pretty impressive compared to PNG:

```sh
$ cwebp -lossless -z 9 input.png -o output.webp
# 1,207,860 to 766,604 bytes: reduced by 441,256 bytes (36.5%)
# Took 7.2s on my system (libwebp 1.3.0)
```

You can also pass `-near_lossless 0` to let it slightly change
pixel values for better compressibility.
This causes visible degradation, but still looks better than a lossy-mode WebP:

```sh
$ cwebp -lossless -near_lossless 0 -z 9 input.png -o output.webp
# 1,207,860 to 492,606 bytes: reduced by 715,254 bytes (59.2%)
# Took 6.6s on my system (libwebp 1.3.0)
```

File links:
[lossy WebP](webp-lossy.webp),
[lossless WebP](webp-lossless.webp),
[near-lossless WebP](webp-near-lossless.webp).



### AVIF

[AVIF](https://en.wikipedia.org/wiki/AVIF),
the new kid on the block based on [AV1](https://en.wikipedia.org/wiki/AV1) video compression,
is honestly incredible:
it reduces file sizes much further than WebP,
and the final quality is so good that I sometimes struggled
to tell the difference with the original PNG.
I suspect that AVIF's success is the reason why Google is winding down
their work on [WebP 2](https://en.wikipedia.org/wiki/WebP#WebP_2)
and [JPEG-XL](https://en.wikipedia.org/wiki/JPEG_XL#Preliminary_web_browser_support).

AV1 compression is complicated business,
so I haven't exhaustively tested all options as I did for JPEG and WebP.
Instead, I used the settings I found in
[this Reddit post](https://www.reddit.com/r/AV1/comments/o7s8hk/high_quality_encoding_of_avif_images_using/),
because I trust random strangers on the Internet:

```sh
$ avifenc --min 0 --max 63 --speed 0 -a end-usage=q -a cq-level=31 -a color:sharpness=2   \
   -a tune=ssim -a color:enable-chroma-deltaq=1 -a color:deltaq-mode=3 -a color:aq-mode=1 \
   input.png output.avif
# 1,207,860 to lossy 59,792 bytes: reduced by 1,148,068 bytes (95.1%)
# Took 11s on my system (libavif 0.11.1)
```

Yes, it's slower to encode, and you can see some degradation if you look closely,
but that's a 95% compression ratio!
It looks slightly worse than the JPEG from earlier (which was 5 times as big),
but better than the lossy WebP (which was over twice the size).
Incredible.

AVIF is quite new, so support is still lacking.
As of writing, Firefox and Chromium work perfectly, but Edge can't display AVIFs yet,
and if I try to open a file with [`imv`](https://sr.ht/~exec64/imv/), it looks messed up.

File links:
[lossy AVIF](avif-reddit.avif).



### JPEG-XL

As of writing, Firefox can't display JPEG-XL yet, and Google has
[aborted support](https://bugs.chromium.org/p/chromium/issues/detail?id=1178058#c84) for it.
If it doesn't work on the web and I can't even test it locally,
then I definitely can't optimize it!

It's sad, because there's evidence
(e.g. in [this post](https://chipsandcheese.com/2021/02/28/modern-data-compression-in-2021-part-2-the-battle-to-dethrone-jpeg-with-jpeg-xl-avif-and-webp/))
that JPEG-XL is a significant improvement over AVIF
in every way: fidelity, file size, and encoding/decoding speed,
although the latter might be due to AV1's relative immaturity.
It can also losslessly recompress existing JPEGs!



## Conclusion

Of course, optimizing images is optional,
but doing so speeds up page loading for users on bad connections,
and, if your server provider bills you for traffic,
can save you a significant amount of money too.
My website doesn't contain many images, but on the pages that do,
using a modern format like AVIF reduces transfers by >80% compared to JPEG,
which has been the standard for far too long.
WebP isn't as good as AVIF, but is also a respectable choice.
JPEG-XL would be the best of all words, but sadly doesn't seem meant to be.