Skip to content

Commit

Permalink
edits
Browse files Browse the repository at this point in the history
  • Loading branch information
knzai committed Sep 2, 2024
1 parent f8848e4 commit 7c957e2
Show file tree
Hide file tree
Showing 10 changed files with 94 additions and 60 deletions.
117 changes: 79 additions & 38 deletions _posts/2024-09-01-svg-ccc.md
Original file line number Diff line number Diff line change
@@ -1,51 +1,82 @@
---
layout: post
title: "The holy grail of image formats: SVG cross-codec-compilation & AVIF"
image: /assets/img/posts/svg-ccc/100.avif
image:
path: '/assets/img/posts/svg-ccc/original.png'
height: 560
width: 918
alt: 'SVG: SVG cross-codec-compilation'
image: /assets/img/posts/svg-ccc/original.png
description: >
Photos and text or diagrams in the same image file, at a small size, without losing cripsness
toot_id: 113064123926263617
---

When using images on the web you've always had to choose between size and quality. This gets particularly pointed when you have one image containing both a photo and crisp lines (for instance text or a diagram). Compressing both of those in the same image will generally either require a large image, or the compression artifacts will become apparent, especially around the crisp lines.
With essentially all browsers being some variation of [evergreen](https://nordvpn.com/cybersecurity/glossary/evergreen-browser/) now, we can start tossing out old assumptions, like how stark the tradeoff is between filesize, image quality, and features (like transparency) in image compression, especially in images that mix photos with other graphics (like lineart or text).

For instance, here's the header image for this post, originally a 627kb PNG, compressed to a 18kb jpg (a "25% quality" setting in the app I was using), to exaggerate the effect beyond what you'd ever typically settle on:

![](/assets/img/posts/svg-ccc/25.jpg)


But now SVG support is good enough that you can avoid this by using an SVG as a container for two images with different codecs/formats that are Base64 data encoded directly into the same file:
* toc
{:toc}

## SVG Compositing
SVG support is now good enough that you can avoid a lot of said tradeoffs by using an SVG as a container for two images with different codecs/formats. The images are then base64 data encoded directly into the same file. This allows you to optimize your graphics separately from your photos, which overall makes for a much smaller size, at essentially equivalent quality, in a single file. I doubt I'm anywhere near the first to notice this, but it should be talked about more widely, because it's **groundbreaking**.
```xml
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<image xlink:href="data:image/png;base64,[DATA]"/>
<image xlink:href="data:image/jpg;base64,[DATA]"/>
```

This lets you optimize the the photo down to a reasonable size, without having to negatively impact your text/diagram rendering. And not having to use two separate image files via html and css is better for optimizing mobile loading (with fewer separate requests to the server) and handles scaling to different resolutions smoothly without any additional css fiddling.

To optimize this further, I started playing around with different compression approaches for the photo, and realized that we can now pretty much [just use AVIF](https://caniuse.com/avif), a better replacement even than jpg2000. Pretty quickly I discovered that the image size vs quality tradeofss were such that you might as well just use an AVIF for the whole things, rather than the complexity of layering a png and avif into one file, for comparable quality and size.

Lossless avif of the whole file cut it down to to 413kb while taking it all the way down to a similar 25%, at 15kn being even smaller than the crappy jpg, without the horrendous artifacts it displayed:

![](/assets/img/posts/svg-ccc/25.avif)

You can of course adjust to wherever in between. If you want really crisp lines for a decent size, you could also try using a webp image in place of a png with the avif. Really, though, if you truly want lines that crisp, you should already be using a vector instead anyways. Since we were already using a SVG for the compositing, lets just do everything but the photo in the svg itself, and then layer in a well-compressed AVIF for the photo.

This is the result I ended up on for this header, originally (I ended up using an AVIF after all, for now, so I didn' have to deal with hacking the jekyll plugin to use a separate image for the OpenGraph share. I'll probably swap back to the smaller and crisper svg-ccc). The font, gradient, and overlay are all done in SVG making it even easier to swap in different photos (svg autogenerated by Figma then cleaned up a bit via [svgviewer.dev](https://www.svgviewer.dev)).

![](/assets/img/posts/svg-ccc/composite.svg)
## Background and alternatives
For example of why you'd want to do this, here are the artifacts that can form around the text of this post's header image when you compress the whole file down to a reasonably sized jpg:

![](/assets/img/posts/svg-ccc/artifacts.jpg)

You could compress the text and the background separately, of course, and layer them in CSS. This has some tradeoffs of it's own, such as requiring CSS that can be tricky to get just right in responsive layouts; requiring additional requests to the server, which is worse for optimizing mobile load time; and overall not being easy to maintain (or fitting in most peoples content management approaches). And then you will still need a single image version for things like the sharing/OpenGraph tile, if it's the main image for the page.
## Enter AVIF
In exploring this technique and optimizing further, I noticed that we now can pretty broadly [just use AVIF](https://caniuse.com/avif). AVIF is an **incredible** format that greatly reduces artifacts and other compression problems, even when smaller than a JPG (and thus a fraction of the size of corresponding PNGs). And you can then combine SVG compositing and AVIF for very high quality images, at a fraction of the size.

I discovered that for this particular image (with just a little text on it) a well compressed AVIF basically beats out even the SVG compositing with a PNG. This isn't too surprising, once you realize how great AVIF is. The base64 encoding has a cost, and the composited form still has to have a similar (if even more well compressed) AVIF as well as the data for the PNG. Swapping in WebP for the PNG works even better at getting the size down. For images with more complex graphics, or higher resolution needs, SVG compositing an AVIF and WebP can be a great solution.

But...
## A match made in heaven: AVIF + vectors
If you are trying to get the crispest lines and overall best graphical art, you shouldn't even be using a raster format anyway. And since we're already working with SVGs you might as well include the vectors in the file we were using to composite images. It's debatable at this point if that's "compositing" or just "inlining a photo into an SVG". Either way, for a small size penalty you can have an even better quality image than the large PNG we were using as our baseline.
## Comparison table

Note: OG is "OpenGraph" support. See section in [Tradeoffs](#tradeoffs). Late I may redo this with a larger image with more graphics in it to better demonstrate the tradeoffs.

| Format | Filesize | OG | Notes | Image (link to full-size) |
|-|-|-|
| PNG | 627 KB | X | Very high quality, larger file | [![](/assets/img/posts/svg-ccc/original.png)](/assets/img/posts/svg-ccc/original.png) |
| AVIF lossless | 413 KB | | Same quality; 30% size reduction | [![](/assets/img/posts/svg-ccc/lossless.avif)](/assets/img/posts/svg-ccc/lossless.avif) |
| 🔥 **AVIF lossy (25%)** | 15 KB | | Great size, okay quality. Good enough for many images or mobile generally | [![](/assets/img/posts/svg-ccc/25.avif)](/assets/img/posts/svg-ccc/25.avif) |
| JPG (0%) | 21 KB | X | Visible artifacts, still not as small as the lossy AVIF | [![](/assets/img/posts/svg-ccc/0.jpg)](/assets/img/posts/svg-ccc/0.jpg) |
| JPG (25%)| 51 KB | X | Same as above, just a 2.5x file with *slightly* fewer artifacts | [![](/assets/img/posts/svg-ccc/25.jpg)](/assets/img/posts/svg-ccc/25.jpg) |
| SVG + AVIF + PNG | 65 KB | | Tiny quality improvement over plain AVIF, at much larger size. | [![](/assets/img/posts/svg-ccc/composite-png.svg)](/assets/img/posts/svg-ccc/composite-png.svg) |
| SVG + AVIF + WebP | 55 KB | | Similar quality, slightly better file size. Images with more graphics than photo will likely show more improvement | [![](/assets/img/posts/svg-ccc/composite-webp.svg)](/assets/img/posts/svg-ccc/composite-webp.svg) |
| 🔥 SVG + AVIF + vector | 36 KB | | Best quality. Razor crisp lines with zero artifacts at a decent size | [![](/assets/img/posts/svg-ccc/composite.svg)](/assets/img/posts/svg-ccc/composite.svg) |


## Tradeoffs
### No progressive rendering
Unlike JPGs, AVIFs don't do [progressive rendering](https://docs.imgix.com/apis/rendering/format/jpg-progressive) so they won't fill in until they are completely downloaded. I feel like the much smaller overall size more than negates that, since the whole image might download before the JPG even got to its first pass anyway.
### Missing OpenGraph
Neither AVIFs or composite SVG work everywhere you can share an OpenGraph/preview tile images (and that's not super easy to track, unlike browser support, since it's based on every individual site or apps support). If you care enough about optimizing your site's images to be reading this post, you are already used to having different sizes and formats for different uses. And since the OpenGraph tile image doesn't actually load on your site, you can just use your default large png that you were likely already using for your largest responsive layout.
### Difficulty managing multiple responsive images (SVG composite)
Using an inlined image file in an SVG makes it trickier to autogenerate different sizes for responsive layouts. You could write some custom tooling to extract the image, resize it, and make a new SVG. But much like the progressive rendering, I think this is somewhat moot. If you can just make all your images better quality at a fraction of the size, there is less need for using different images per layout anyway.
### Missing tooling support (AVIF + composite with AVIF)
This will likely get better over time. To make an SVG composite I had to export the SVG from Figma with the original PNG in it, then manually swap in the compressed AVIF's data. You may need to compress your AVIFs separate anyway, due to the next point.
### Compression time
Running the highest compression settings on AVIF can be slow. This will get better as computers get faster. You may end up wanting some automated tooling around producing your AVIFs to manage this, or you may want to hand-tune them instead if you are only doing a few at a time anyway. Either way you probably want a decent standalone compressor. I found [ImageTool+](https://apps.apple.com/us/app/image-tool/id1524216218?mt=12) to be well worth the $8 when trying to do a lot of images with different settings.
## Final approach
Ironically, for the header of this post I'm still using the PNG, but that's just till I hack on the jekyll-seo plugin enough to allow me a separate OpenGraph image. Below is the gist of what I ended up on for my ideal SVG, though. The font, gradient, and overlay are all done in SVG making it even easier to swap in different photos. The SVG was autogenerated by Figma then cleaned up a bit via [svgviewer.dev](https://www.svgviewer.dev), which has been super helpful through the whole process of experimenting wiah all these possibilities.

```xml
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" fill="none" viewBox="0 0 918 560">
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="918" height="560" fill="none" viewBox="0 0 918 560">
<g clip-path="url(#a)">
<path fill="url(#b)" d="M-134 0H918v560H-134z"/>
<g filter="url(#c)" style="mix-blend-mode:overlay">
<path fill="url(#c)" d="M423 0h496v560H423z" shape-rendering="crispEdges"/>
<path fill="url(#d)" d="M423 0h496v560H423z" shape-rendering="crispEdges"/>
</g>
<path fill="#fff" d="M52.078 311.608q-3.343 0-6.457-.76t-5.127-1.975l2.886-6.533q1.899 1.103 4.216 1.785 2.355.646
..."/>
</g>
<path fill="#fff" d="[PATHDATA]"/>
</g>
<defs>
<linearGradient id="b" x1="-355.072" x2="190.854" y1="369.927" y2="-569.885" gradientUnits="userSpaceOnUse">
<stop stop-color="#B45BCF"/>
Expand All @@ -54,16 +85,26 @@ This is the result I ended up on for this header, originally (I ended up using a
<clipPath id="a">
<rect width="918" height="560" fill="#fff" rx="7"/>
</clipPath>
<pattern id="c" width="1" height="1" patternContentUnits="objectBoundingBox">
<use xlink:href="#d" transform="matrix(.00204 0 0 .00184 -.013 -.03)"/>
<pattern id="d" width="1" height="1" patternContentUnits="objectBoundingBox">
<use xlink:href="#e" transform="matrix(.00204 0 0 .00184 -.013 -.03)"/>
</pattern>
<image id="d" xlink:href="data:image/png;base64,...
<filter id="c" width="504" height="568" x="419" y="0" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feColorMatrix in="SourceAlpha" result="hardAlpha" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/>
<feOffset dy="4"/>
<feGaussianBlur stdDeviation="2"/>
<feComposite in2="hardAlpha" operator="out"/>
<feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.25 0"/>
<feBlend in2="BackgroundImageFix" result="effect1_dropShadow_3226_131"/>
<feColorMatrix in="SourceAlpha" result="hardAlpha" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/>
<feOffset dy="4"/>
<feGaussianBlur stdDeviation="2"/>
<feComposite in2="hardAlpha" operator="out"/>
<feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.25 0"/>
<feBlend in2="effect1_dropShadow_3226_131" result="effect2_dropShadow_3226_131"/>
<feBlend in="SourceGraphic" in2="effect2_dropShadow_3226_131" result="shape"/>
</filter>
<image xlink:href="data:image/avif;base64,[IMGDATA" id="e" width="496" height="560"/>
</defs>
</svg>
```
Tools like Figma usually don't support AVIF yet, so you may have to do something like export the SVG with just the raw png in the svg, delete out it's href and replace it with the avif data. This can get a little cumbersome, and it may just be worth it using a reasonably well encoding AVIF. Hopefully the tooling starts supporting AVIF more broadly and it will get a little simpler.
You'll need a non-SVG photo for your OpenGraph preview anyway, so in the case of header images for pages, it may extra not be worth it doing the SVG shuffle. And if you really want to optimize your mobile loading you may want to make separate SVGs for the different responsive sizes and that starts getting annoying without rolling some additional tooling, as you can't just use ImageMagic or some other tools frequently used to autogenerate the different sized. Thankfully you can get filesizes down small enough it may not even be worth it either way though
This small filesize also offsets one of the few disadvantages of AVIF, it doesn't do progressive rendering while it's not downloaded yet, like jpg does. But, given the smaller sizes you can acheive, it's still probably worth using AVIF, since the whole file will be loaded in the same time the progressive rendering would start.
So overall, you can just use AVIF 90% of the time and adjust the quality to a decent trade-offs. In cases where that isn't sufficient, you can stick the AVIF into a SVG like a displayed and layer in different images of whatever format, though in my case the png usage wasn't enough of an improvment. Webp probably works out well, but then why not just use vectors anyway.
4 changes: 4 additions & 0 deletions _sass/my-style.scss
Original file line number Diff line number Diff line change
Expand Up @@ -46,4 +46,8 @@ mastodon-comments {
--comment-indent: 40px;

h2 { display: none }
}

article h1 h2 h3 h4 h5 {
margin: 1rem 0 1rem;
}
Binary file added assets/img/posts/svg-ccc/0.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/img/posts/svg-ccc/17kb.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified assets/img/posts/svg-ccc/25.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/img/posts/svg-ccc/artifacts.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 5 additions & 0 deletions assets/img/posts/svg-ccc/composite-png.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 5 additions & 0 deletions assets/img/posts/svg-ccc/composite-webp.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading

0 comments on commit 7c957e2

Please sign in to comment.