Files
music_library/backlog/docs/doc-29 - Lossless-Vix-libvips-compression-options-for-asset-endpoints.md
2026-05-27 11:31:01 +03:00

155 lines
5.5 KiB
Markdown

---
id: doc-29
title: Lossless Vix/libvips compression options for asset endpoints
type: other
created_date: "2026-05-27 08:28"
tags:
- research
- assets
- vix
- libvips
- lossless
---
# Lossless Vix/libvips compression options for asset endpoints
## Scope
This note covers image assets served through `MusicLibraryWeb.AssetController`:
- `/assets/:transform_payload`
- `/public/assets/:transform_payload`
- `/api/v1/assets/:transform_payload`
Static compiled files under `/assets/app.css`, `/assets/app.js`, etc. do not use Vix/libvips. They are served by `Plug.Static`, with gzip enabled in production when code reloading is disabled.
## Current behaviour
`MusicLibrary.Assets.Image` currently writes transformed images with Vix defaults:
```elixir
Image.write_to_buffer(image, ".jpg")
Image.write_to_buffer(image, ".webp")
```
Relevant libvips defaults:
- JPEG output uses lossy JPEG encoding (`Q: 75` by default).
- WebP output is lossy unless `lossless: true` is set.
- Saver metadata defaults keep EXIF, XMP, IPTC, ICC, and other metadata.
- `AssetController.pick_format/1` chooses WebP when the request `Accept` header contains `image/webp`; otherwise it chooses JPEG.
Strictly lossless compression is therefore not possible for all current dynamic asset responses without changing the fallback format strategy, because JPEG saves are inherently lossy.
## Important constraint: resize vs compression
Width transforms use `Operation.thumbnail_buffer/2`, which resamples pixels. Resizing is not bit-for-bit preservation of the original asset. The flags below avoid additional lossy codec compression of the transformed pixels; they do not make resizing itself reversible.
For untransformed assets, serving the original stored bytes is the only bit-for-bit lossless option.
## Useful non-lossy flags
### WebP lossless
Best candidate for browser-targeted lossless compression:
```elixir
Image.write_to_buffer(image, ".webp",
lossless: true,
effort: 6,
keep: [:VIPS_FOREIGN_KEEP_ICC]
)
```
Notes:
- `lossless: true` is the required flag.
- `effort: 6` is the maximum WebP compression effort documented by libvips; it trades CPU time for smaller output.
- `keep: [:VIPS_FOREIGN_KEEP_ICC]` strips EXIF/XMP/IPTC/other metadata while retaining the ICC colour profile. If metadata must also be preserved, omit `keep` or use `[:VIPS_FOREIGN_KEEP_ALL]`.
- Avoid `near-lossless: true`; despite the name, it preprocesses pixels and is not strictly lossless.
- Avoid `target-size`, `mixed`, low `alpha-q`, or lossy WebP presets when strict losslessness is required.
### PNG lossless
Suitable as a strict lossless fallback when WebP is unavailable:
```elixir
Image.write_to_buffer(image, ".png",
compression: 9,
filter: [:VIPS_FOREIGN_PNG_FILTER_ALL],
keep: [:VIPS_FOREIGN_KEEP_ICC]
)
```
Notes:
- `compression: 9` increases DEFLATE effort without changing pixels.
- `filter: [:VIPS_FOREIGN_PNG_FILTER_ALL]` enables adaptive PNG row filters, which are lossless and often reduce size.
- Avoid `palette: true`, lowering `bitdepth`, `Q`, or `dither`; those can quantize or otherwise change pixel data.
### JPEG: only metadata/Huffman optimisation is safe, but the result is still JPEG-lossy
If JPEG output remains necessary, the only useful non-quality-oriented saver option is Huffman table optimisation:
```elixir
Image.write_to_buffer(image, ".jpg",
"optimize-coding": true,
keep: [:VIPS_FOREIGN_KEEP_ICC]
)
```
Optional:
```elixir
interlace: true
```
Notes:
- `"optimize-coding": true` computes optimal Huffman tables. It can reduce file size without intentionally changing quality settings.
- `interlace: true` creates progressive JPEGs. This is not inherently lossy, but may increase size for small thumbnails.
- This does not make JPEG output lossless. JPEG encoding remains lossy.
- Avoid changing `Q`, `"subsample-mode"`, `trellis-quant`, `quant-table`, or similar JPEG options for this requirement; they affect quality/compression tradeoffs.
### Thumbnail generation
This avoids upscaling small source images:
```elixir
Operation.thumbnail_buffer(cover_data, size,
size: :VIPS_SIZE_DOWN
)
```
Notes:
- `:VIPS_SIZE_DOWN` prevents enlargement when the requested width is larger than the source.
- This can reduce bytes and avoid interpolation blur.
- It changes current semantics for requests that ask for a width larger than the stored image.
## AVIF/HEIF note
libvips also exposes `heifsave_buffer/2` with `lossless: true`, and AVIF can be selected by an `.avif` suffix or HEIF compression options. Example shape:
```elixir
Image.write_to_buffer(image, ".avif",
lossless: true,
effort: 9,
keep: [:VIPS_FOREIGN_KEEP_ICC]
)
```
This may be useful later, but it requires adding AVIF negotiation and checking production libvips/libheif codec support. WebP lossless is the safer near-term option.
## Recommendation
For strict non-lossy compression of dynamic image responses:
1. Use lossless WebP for clients that accept WebP.
2. Use PNG as the non-WebP fallback if strict losslessness is required.
3. Do not use JPEG for strict lossless transformed output. JPEG can only be mildly optimised with `"optimize-coding": true`; it remains lossy.
4. Keep ICC metadata unless colour-profile changes are acceptable.
5. Consider `size: :VIPS_SIZE_DOWN` for thumbnails if preventing upscaling is acceptable.
A conservative implementation would add format-specific saver options in `MusicLibrary.Assets.Image`, then change `AssetController.pick_format/1` so the strict-lossless path returns either `image/webp` with `lossless: true` or `image/png`, not JPEG.