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 thesqzerpackage on npm, described below, and the page to drop an image on is sqzer.dev. The design is indocs/adr/0001-system-design.md, the command line indocs/adr/0003-cli-interface.md, the native backends indocs/adr/0004-native-tier.md, colour indocs/adr/0007-colour-management.md, the browser build indocs/adr/0011-browser-build.md.
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 --jsonThe 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.
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-cliThe 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.
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 encodeDefaults 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.
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:
rimagescales an image up to the size asked for unless--no-upscaleis given, which is why the lines above carry--enlarge. Most runs only mean to shrink, and for those--enlargecan go.
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.
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 (
jixelis a candidate for JPEG XL, unmeasured). Requesting one in a portable build returnsEncoderUnavailablewith the feature that would provide it, it never silently falls back. With no-f, a graphic goes to lossless WebP whatever-qsays, and the record sayslossless.
Note: Every native feature vendors and builds its C library from source (
ccor cmake; nasm on x86; a C++ compiler fornative-jxlandnative-jpegli), exceptnative-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 fromlibheif(LGPL-3.0, >= 1.17, with an HEVC decoder) loaded at run time everywhere but musl:libheif1pluslibheif-plugin-libde265on Debian and Ubuntu,libheif-freeworldon Fedora,brew install libheifon a Mac without ImageIO's decoder, orSQZER_LIBHEIFpointing at the library. A binary always starts;sqzer --list-codecssays 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.mdhas the crate choices and the licence facts,docs/adr/0005-heic-through-os-decoders.mdthe 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 nativebuilds onsqzerandsqzer-cliand what the release binaries carry; a singlenative-*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,.svgzis not read. TIFF files in CMYK or YCbCr are refused rather than converted.
Note: AVIF decodes on every target,
wasm32included, throughsqzer-rav1d: upstreamrav1dmainwith its safe Rust API, which no upstream release carries yet, and a stand-in for thelibcnameswasm32lacks. The fork is dropped forrav1ditself once a release has both.docs/adr/0011-browser-build.mdhas the reasoning.
Note:
oxipngis the one portable backend that is not pure Rust: its DEFLATE step islibdeflate, a vendored C library compiled byccwith no system package to install.wasm32has no libc, so there it is built in itsfreestandingmode and the build needsclangonPATH; from the same pixels it writes the same PNG, byte for byte.docs/adr/0002-libdeflate-in-the-portable-tier.mdanddocs/adr/0011-browser-build.mdhave the reasoning.
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`
# 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-targetsThe 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 -- verifytools/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.
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.
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.