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

5.5 KiB

id, title, type, created_date, tags
id title type created_date tags
doc-29 Lossless Vix/libvips compression options for asset endpoints other 2026-05-27 08:28
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:

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:

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:

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:

Image.write_to_buffer(image, ".jpg",
  "optimize-coding": true,
  keep: [:VIPS_FOREIGN_KEEP_ICC]
)

Optional:

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:

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:

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.