Skip to content
sqzer-devPublic

About

Multi-format image optimizer with best-in-class defaults. Pure Rust by default, C codecs when you want the last few percent.

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

sqzer

Multi-format image optimizer with best-in-class defaults. A library, a CLI and a WebAssembly package for the browser, pure Rust by default, C codecs when you want the last few percent.

Note: Early days. The command line and its defaults are what this README describes; the library API will still change before 1.0. The browser build is the sqzer package on npm, described below, and the page to drop an image on is sqzer.dev. The design is in docs/adr/0001-system-design.md, the command line in docs/adr/0003-cli-interface.md, the native backends in docs/adr/0004-native-tier.md, colour in docs/adr/0007-colour-management.md, the browser build in docs/adr/0011-browser-build.md.

What it is for

Squoosh had one thing nobody else had: defaults tuned by people who understood the codecs. Drop an image in, pick AVIF, get something close to the best a non-expert could tune by hand. It has no releases, no batch mode and lives in a browser tab.

sqzer is that, as a tool you can script:

# one photo, default target format (AVIF for photos), written next to the input
sqzer photo.jpg

# one input, three outputs
sqzer photo.jpg -f webp,avif,jxl

# recurse a folder, mirror the tree into ./dist
sqzer ./assets -r -f avif -o ./dist

# lossless conversion
sqzer *.png --preset lossless -f webp

# perceptual target instead of a quality number, plus a resize
sqzer in.png --target 60 --max-width 1600

# machine output for integrations: sizes, scores, chosen params
sqzer in.png --json

The default mode is a perceptual target, not a quality slider. sqzer searches encoder quality until the output hits a SSIMULACRA2 score (70 by default), so a flat screenshot and a noisy photo get different settings for the same visible result. The search starts from a seed table calibrated per encoder on a corpus of photos, graphics and screenshots, so the first encode is usually close: on a held-out split the search takes two to four encodes instead of four to six, at the same quality and size.

Install

Release binaries carry the native tier: the portable backends plus every C backend the target can build and run. Nothing to install next to them; HEIC input needs a decoder on the machine, see the note under Formats.

# Linux and macOS
curl --proto '=https' --tlsv1.2 -LsSf https://github.com/sqzer-dev/sqzer/releases/latest/download/sqzer-cli-installer.sh | sh

# Windows
powershell -ExecutionPolicy Bypass -c "irm https://github.com/sqzer-dev/sqzer/releases/latest/download/sqzer-cli-installer.ps1 | iex"

# Homebrew, macOS and Linux. The formula pulls libheif for the runtime loader.
brew install sqzer-dev/tap/sqzer

# from source, portable tier. --features native adds the C backends; see Development
cargo install sqzer-cli

The archives on the releases page, and what each one carries:

sqzer-cli-x86_64-unknown-linux-gnu.tar.xz     all five native backends
sqzer-cli-aarch64-unknown-linux-gnu.tar.xz    no jpegli; JPEG is mozjpeg-rs
sqzer-cli-x86_64-unknown-linux-musl.tar.xz    static; no jpegli, no JPEG XL encoder, no HEIC
sqzer-cli-x86_64-apple-darwin.tar.xz          all five
sqzer-cli-aarch64-apple-darwin.tar.xz         all five
sqzer-cli-x86_64-pc-windows-msvc.zip          no jpegli; JPEG is mozjpeg-rs

sqzer --list-codecs prints what the binary in front of you has. The rows are decided per target in crates/sqzer-native-tier and the reasons are in docs/adr/0006-release-matrix.md.

Command line

One flat command. Flags can go anywhere, -q means quality everywhere, and codec-specific knobs go through one flag instead of one flag per codec. sqzer -h shows the flags most runs need, sqzer --help shows all of them.

# the target: a SSIMULACRA2 score (default 70), an explicit quality, or lossless
sqzer photo.jpg -t 60
sqzer photo.jpg -q 82 -e 8
sqzer photo.jpg --lossless -f png
sqzer photo.jpg --preset thumbnail          # web (70), thumbnail (60, inside 512 x 512), archive (85), lossless
sqzer photo.jpg --fast                      # one encode at the calibrated seed quality, no search

# colour: an ICC profile is converted to sRGB and dropped; --keep-icc embeds it untouched
sqzer photo-p3.jpg
sqzer photo-p3.jpg --keep-icc -f jxl

# metadata: EXIF and XMP are stripped; --keep-metadata carries them over, orientation applied
sqzer photo.jpg --keep-metadata -f webp

# resize: fit inside, keep the aspect ratio, never enlarge. the target is scored against the resized image
sqzer photo.jpg --max-width 1600
sqzer photo.jpg --max-width 1600 --max-height 1600

# the box in CSS object-fit terms: inside (default), cover crops, contain pads, fill stretches,
# outside covers without cropping
sqzer photo.jpg --width 400 --height 400 --fit cover --position top
sqzer photos/*.jpg --width 1000 --height 1000 --fit outside   # shortest side 1000, portrait or landscape
sqzer photo.jpg --width 800 --height 800 --fit contain --background white
sqzer photo.jpg --scale 50%
sqzer sprite.png --scale 400% --enlarge --filter nearest

# srcset: several widths from one decode, each searched on its own. photo-480w.avif, photo-480w.webp, ...
sqzer photo.jpg --width 480,960,1600 -f avif,webp

# codec-specific options, checked against the backend before anything runs
sqzer photo.jpg -f jpeg -x jpeg:progressive=false
sqzer --list-codecs -v                      # every backend's keys and defaults

# a PNG reduced to a palette, as pngquant writes them. lossy: --json says "lossless": false,
# and --lossless refuses it. an image that already has that few colours is written exactly
sqzer logo.png -f png -x png:colors=256
sqzer logo.png -f png -x png:colors=32 -x png:dither=50   # fewer colours, half the dithering

# where outputs go
sqzer photo.jpg -o out.avif                 # a file, when there is one input and one format
sqzer *.jpg -o dist                         # a directory otherwise
sqzer *.jpg --suffix -min                   # photo-min.avif next to photo.jpg
sqzer *.jpg --template "{stem}-{width}w.{ext}"
sqzer *.jpg --in-place --backup             # write over the input, keep photo@backup.jpg
sqzer ./assets -r -o dist --exclude "**/raw/*"

# inputs from a list or a pipe
find . -name '*.png' -print0 | sqzer --files-from - -0 -f webp --lossless
curl -s https://example.com/a.png | sqzer - -f png --lossless > a.png

# machine output and planning
sqzer *.jpg --json                          # one JSON Lines object per output, nothing else on stdout
sqzer *.jpg -n --json                       # dimensions, alpha, format and planned outputs, no encode

Defaults that differ from most optimisers: the output never overwrites the input unless --in-place is given, an output larger than its input is not written unless --force is, and metadata is stripped with the ICC profile converted to sRGB (--keep-metadata keeps EXIF and XMP, --keep-icc keeps the profile; EXIF orientation is applied and its tag reset either way). Every such "nothing happened" prints one line saying why and which flag changes it.

Exit codes, from ADR-0001:

0   every input produced every requested output (skipped-as-larger counts as success)
1   at least one input failed; the failures are on stderr and in --json
2   argument error, including --target with --quality and an unknown --codec-opt key
3   nothing could be done: no input matched, or no encoder for the format in this build

Paths: an argument that exists on disk is taken literally and never parsed as a glob. One that does not exist and contains *, ? or [ is expanded by sqzer itself, case-insensitively, so *.png works in cmd.exe and finds .PNG. The output stem is everything before the last dot, so a.b.c.jpg becomes a.b.c.avif.

Memory: -j sets how many files are in flight and defaults to the CPU count, but each file also reserves an estimate of its peak memory before it decodes, and files wait while the total would pass three quarters of the memory available when the run started. The target search is the expensive part, about 160 bytes per pixel, so a folder of 24-megapixel photos runs one or two at a time on an 8 GB machine. A file whose estimate is over the budget on its own runs alone with a warning; -q skips the search. Encoders run single-threaded; the parallelism is across files.

Coming from rimage

rimage had one subcommand per codec and local flags under each. sqzer has one flat command; the codec is -f. A rimage codec name as the first argument prints the equivalent sqzer line and exits 2.

rimage mozjpeg -q 75 in.jpg          sqzer -f jpeg -q 75 in.jpg
rimage oxipng in.png                 sqzer -f png in.png
rimage webp -q 80 in.png             sqzer -f webp -q 80 in.png    (lossy WebP needs the native tier)
rimage avif in.jpg                   sqzer -f avif in.jpg
-d <dir>                             -o <dir>
-s <suffix>                          --suffix <suffix>
-t <threads>                         -j <jobs>        (-t is now --target)
--quantization / --dithering         -x png:colors=<2-256> -x png:dither=<0-100>    (a colour count, not a quality)
--resize 1600w                       --width 1600 --enlarge
--resize 900h                        --height 900 --enlarge
--resize 512l                        --width 512 --height 512 --enlarge
--resize 300s                        --width 300 --height 300 --fit outside --enlarge
--resize 800x600                     --width 800 --height 600 --fit fill --enlarge
--resize 50%                         --scale 50%
--resize @1.5                        --scale 150% --enlarge
--no-upscale / --reduce-only         drop --enlarge: never scaling up is the default
--no-downscale / --enlarge-only      no equivalent
--filter <filter>                    --filter <filter>, the same names except hamming
--backup                             --backup, unchanged, with --in-place

Note: rimage scales an image up to the size asked for unless --no-upscale is given, which is why the lines above carry --enlarge. Most runs only mean to shrink, and for those --enlarge can go.

In the browser

sqzer.dev is a page you can drop an image on. It encodes in the tab and uploads nothing; its source is sqzer-dev/sqzer.dev.

It runs the same pipeline and defaults as a WebAssembly package, sqzer on npm: the portable tier minus SVG, which the browser's canvas rasterises instead.

// npm install sqzer
import init, { optimize } from "sqzer";
await init();

// AVIF for a photograph, lossless WebP for a graphic, searched to a score of 70
const out = optimize(bytes);
// the command line's flags in camel case
const avif = optimize(bytes, { format: "avif", target: 60, width: 1600 });

It is synchronous and single-threaded, so a page runs it in a Web Worker. The options, decode once and encode many times, decodeAny for SVG and HEIC, the result record and the errors are in crates/sqzer-wasm/README.md, which is the package's README on npm.

Formats

Decode: JPEG, PNG, WebP, AVIF, JPEG XL, GIF, TIFF, BMP, TGA, ICO, QOI, PNM, OpenEXR, SVG. HEIC behind a feature. OpenEXR is read as linear light in its own primaries, rotated to sRGB and encoded to 16-bit sRGB with anything above display white clipped; there is no tone mapping.

Encode: JPEG, PNG, WebP, AVIF, JPEG XL.

Backends come in tiers, mirrored by Cargo features:

portable   permissive licences, no system library, every target including
           wasm32. Always on.
           JPEG (mozjpeg-rs / zune-jpeg), PNG (oxipng), AVIF (ravif / rav1d),
           WebP (image-webp, lossless write), JXL decode (jxl-oxide).
           Input only: GIF (gif), TIFF (tiff), BMP, TGA, ICO, QOI and PNM (image,
           decoders only), SVG rasterised by resvg.
native     C bindings, opt-in, one feature per library, `native` for all five.
           native-webp    libwebp, lossy and lossless WebP (webpx)
           native-jxl     libjxl, JPEG XL encoding (gamut-jxl)
           native-avif    libavif + libaom, AVIF encoding (libavif)
           native-heif    HEIC decoding: ImageIO on macOS, WIC on Windows, libheif
                          loaded at run time elsewhere (heif-imageio, heif-wic, heif-dl)
           native-jpegli  jpegli, JPEG encoding (jpegli)
           A native encoder takes its format over from the portable one.
agpl       reserved. Never a default dependency, never in the library.

Note: The portable tier cannot write lossy WebP or JPEG XL. No permissive pure-Rust encoder exists for either as of September 2026 (jixel is a candidate for JPEG XL, unmeasured). Requesting one in a portable build returns EncoderUnavailable with the feature that would provide it, it never silently falls back. With no -f, a graphic goes to lossless WebP whatever -q says, and the record says lossless.

Note: Every native feature vendors and builds its C library from source (cc or cmake; nasm on x86; a C++ compiler for native-jxl and native-jpegli), except native-heif, which links nothing. HEIC comes from the OS decoder on macOS (ImageIO, every Mac since 10.13) and Windows (WIC, needs the HEIF Image Extension and HEVC Video Extensions from the Microsoft Store), and from libheif (LGPL-3.0, >= 1.17, with an HEVC decoder) loaded at run time everywhere but musl: libheif1 plus libheif-plugin-libde265 on Debian and Ubuntu, libheif-freeworld on Fedora, brew install libheif on a Mac without ImageIO's decoder, or SQZER_LIBHEIF pointing at the library. A binary always starts; sqzer --list-codecs says which HEIC decoder it has and whether this machine can use it, and a HEIC input on a machine with none gets an error naming the fix. Static musl builds have no HEIC. docs/adr/0004-native-tier.md has the crate choices and the licence facts, docs/adr/0005-heic-through-os-decoders.md the HEIC design, and which targets CI covers: all five on x86_64 Linux and both macOS targets, four on aarch64 Linux (jpegli crashes there), four on Windows (jpegli cannot share a cmake generator with libjxl there), three on musl (no C++ toolchain there yet). That per-target list is what --features native builds on sqzer and sqzer-cli and what the release binaries carry; a single native-* feature is strict and fails to build where the backend cannot (docs/adr/0006-release-matrix.md).

Note: Input-only formats decode to their first image: the first frame of an animated GIF, the first page of a TIFF, the largest entry of an ICO. An SVG is rasterised at its own size, one CSS pixel per pixel, with the system's fonts for text (none on wasm32); references to files on disk are ignored, data: URLs are rendered, .svgz is not read. TIFF files in CMYK or YCbCr are refused rather than converted.

Note: AVIF decodes on every target, wasm32 included, through sqzer-rav1d: upstream rav1d main with its safe Rust API, which no upstream release carries yet, and a stand-in for the libc names wasm32 lacks. The fork is dropped for rav1d itself once a release has both. docs/adr/0011-browser-build.md has the reasoning.

Note: oxipng is the one portable backend that is not pure Rust: its DEFLATE step is libdeflate, a vendored C library compiled by cc with no system package to install. wasm32 has no libc, so there it is built in its freestanding mode and the build needs clang on PATH; from the same pixels it writes the same PNG, byte for byte. docs/adr/0002-libdeflate-in-the-portable-tier.md and docs/adr/0011-browser-build.md have the reasoning.

Layout

crates/sqzer-core      image model, codec traits, params, errors. No codecs.
crates/sqzer-codecs    every backend behind a feature flag
crates/sqzer-metrics   SSIMULACRA2 and the target-quality search
crates/sqzer           library facade, the thing you depend on
crates/sqzer-cli       the binary, `sqzer`
crates/sqzer-wasm      browser build: the JavaScript API, portable tier minus SVG. `sqzer` on npm
crates/sqzer-native-tier  what `native` means per target, no code
docs/adr               design decisions
dist-workspace.toml    the release matrix for `cargo-dist`

Development

# everything, portable tier
cargo build --workspace

# native tier: cmake, a C++ compiler and nasm on PATH; cmake 4 needs
# CMAKE_POLICY_VERSION_MINIMUM=3.5 for the vendored libjpeg-turbo in jpegli's
# tree. Nothing to install for HEIC at build time; the tests need a libheif
# with an HEVC decoder at run time (libheif1 + libheif-plugin-libde265 on
# Ubuntu, `brew install libheif` on macOS, or SQZER_LIBHEIF=/path/to/it).
# `native` is what this target can build and run, the same set a release
# carries. Or one backend at a time, for example --features
# native-webp,native-avif on a box without a C++ compiler; a single feature
# is strict and fails to build where the backend cannot
cargo build -p sqzer-cli --features native
cargo test -p sqzer-codecs -p sqzer -p sqzer-cli --features sqzer-codecs/native,sqzer/native,sqzer-cli/native

# what a tag does, without the tag: check the release config, then build this
# machine's archive into target/distrib. dist is cargo-dist, installed from
# github.com/axodotdev/cargo-dist; edit dist-workspace.toml, never release.yml,
# and run `dist generate` after
dist plan
dist build --artifacts=local

# prove the portable tier builds for a target with no libc. needs clang on
# PATH, for oxipng's libdeflate
cargo build -p sqzer-wasm --target wasm32-unknown-unknown

# the browser build's JavaScript API over the fixtures, in Node. needs
# wasm-pack; a RUSTFLAGS variable would replace the wasm32 flags of
# .cargo/config.toml, so run it without one
(cd crates/sqzer-wasm && wasm-pack test --node)

# the npm package as a tag publishes it: release build, packed, installed
# from the tarball and run. leaves crates/sqzer-wasm/pkg/sqzer-<version>.tgz.
# needs wasm-pack, jq and Node
crates/sqzer-wasm/pack.sh

# licence allow-list, runs in CI
cargo deny check

cargo test --workspace
cargo clippy --workspace --all-targets

The seed tables that start the target search live in crates/sqzer-metrics/src/seeds/tables.rs and are regenerated by hand, not at build time:

# downloads the corpus on first use, sweeps every lossy encoder, rewrites the tables
cd tools/calibrate && cargo run --release -- sweep

# then measures the new tables on a held-out split
cargo run --release -- verify

tools/calibrate is its own package, outside the workspace, because codec-eval pulls in an AGPL dependency that must not reach anything that ships. tools/calibrate/README.md has the details.

Contributing

Issues and pull requests are welcome, in English preferably. ROADMAP.md has what comes next, CONTRIBUTING.md the checks, the licence rules for new dependencies and how PRs are merged. Questions go to Discussions, security reports through SECURITY.md.

Licence

MIT or Apache-2.0, at your option. The release archives and the npm package carry THIRD-PARTY-LICENSES.txt, the notice of every crate compiled into the binary or the module, the C libraries of the native tier included, and the patent grants that travel with some of those licences.

About

Multi-format image optimizer with best-in-class defaults. Pure Rust by default, C codecs when you want the last few percent.

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages