Optimize PNGs with Oxipng from the CLI and Rust
To compress a PNG with Oxipng while keeping the original file, use --out with a different filename.
The walkthrough below previews the result, writes an optimized copy, and then performs the same
operation from Rust. It uses ordinary, nonanimated PNGs and leaves the optional lossy transformations
disabled.
Lossless PNG optimization can change compression, filtering, palette layout, and color representation without changing the decoded pixel values. A different file hash therefore does not mean the image lost quality. It also does not prove that every piece of metadata survived. Keep the source file when you need an archival original.
Install Oxipng
These Bash examples were tested on Linux with Rust and Cargo 1.98.1 and Oxipng 10.2.1. You need Cargo
on your PATH, a C compiler and linker for the native compression dependency, and Cargo’s binary
directory on your PATH. Oxipng 10.2.1 declares
Rust 1.88.0 as its minimum.
cargo install oxipng --version 10.2.1 --locked &&
oxipng --version
The version command should print oxipng 10.2.1. If it prints another version, check which binary
your shell finds before continuing. cargo install
builds in release mode by default; --locked uses the crate’s packaged dependency lockfile.
Optimize a single PNG
Place your PNG in the current directory as input.png. First, preview optimization without writing
anything:
oxipng -o 4 --dry-run -v -- input.png
--dry-run still performs the optimization work; it only skips writing the result. Version 10
replaced the old --pretend flag.
To save the result separately and compare file sizes:
oxipng -o 4 -v --out optimized.png -- input.png &&
wc -c input.png optimized.png
This leaves input.png untouched. It replaces an existing optimized.png without asking, including
on a noninteractive rerun. Choose a fresh destination name if you need to keep an earlier output.
The destination’s parent directory must already exist.
Preset four is a starting point, not a promised saving. The available levels are zero through six; the default is two. Higher presets spend more work searching, but do not guarantee a smaller result. An already optimized PNG may yield no reduction. With these settings, a separate output still gets written, using the original bytes when Oxipng cannot improve them. Without a separate destination, that case leaves the input file alone.
Oxipng uses the available logical CPUs by default. Add --threads 4 to limit its worker threads on a
shared machine. See the versioned CLI manual
for the full option list.
Choose what to preserve
Three different kinds of preservation matter here:
| Setting | What it means |
|---|---|
No --alpha | Retain RGB values even in fully transparent pixels. Adding --alpha permits changing those hidden colors for compression; it is visually lossless but changes pixel data. |
No --strip | Keep PNG metadata that remains valid after optimization, subject to the exceptions below. |
--preserve | Try to retain filesystem permissions and modification time. This does not control PNG metadata or retain access time. |
The library options also leave alpha
optimization and lossy 16-to-eight-bit scaling disabled by default. Do not add --scale16 if you need
lossless pixel values.
Default metadata retention is not exact archival preservation. Oxipng drops bKGD, sBIT, and
hIST if a color type or bit depth change invalidates them. It also removes C2PA caBX and Apple
iDOT chunks by default. An embedded ICC profile may be recompressed. These behaviors are described
in the chunk handling implementation
and the CLI manual.
Use --strip safe only when you intend to discard ancillary information such as text and EXIF.
It keeps selected display-related chunks, including sRGB and pHYs, but it is not a promise to
retain all metadata. --strip all goes further and can remove color-management information, changing
how an image is displayed. Neither option is needed for the examples here.
If you deliberately want to replace your input while retaining its file permissions and modification time, use:
oxipng -o 4 --preserve -- input.png
This is an in-place operation: keep a backup before running it. Attribute preservation is best effort; Oxipng warns if it cannot restore an attribute.
Batch-process a folder
For a directory of PNG assets you are prepared to replace, run:
find ./images -type f -name '*.png' -exec oxipng -o 4 --preserve -- {} +
This visits nested directories and passes filenames with spaces safely. It matches lowercase
.png extensions and modifies files in place; a rerun operates on those same files. Add --dry-run
to the Oxipng arguments for a preview. Use a copied asset directory when you need to keep originals.
Embed Oxipng in your Rust code
Create a new project and pin the same crate version:
cargo new --bin --vcs none png-optimize &&
cd png-optimize &&
cargo add oxipng@=10.2.1
Continue only after this succeeds. An existing png-optimize directory makes cargo new fail;
choose a new project name instead of deleting an existing project. Keep the generated Cargo.lock
to retain the resolved dependency versions.
Put a copy of your PNG in this project directory as input.png. Replace the generated src/main.rs
with this complete program:
use oxipng::{optimize, InFile, Options, OutFile};
use std::path::PathBuf;
fn main() -> Result<(), oxipng::PngError> {
let input = InFile::Path(PathBuf::from("input.png"));
let output = OutFile::from_path(PathBuf::from("output.png"));
let options = Options::from_preset(4);
let (before, after) = optimize(&input, &output, &options)?;
println!("{before} -> {after} bytes: output.png");
Ok(())
}
From png-optimize, run:
cargo run --release
The program reads input.png relative to the current directory, writes output.png, and prints the
input and output byte counts returned by
optimize. An existing output.png is replaced
without a prompt. Equal counts are a valid result, not a failure.
OutFile::from_path chooses a separate
file and does not request attribute preservation. Use OutFile::None for a library dry run, or
OutFile::Path { path: Some(PathBuf::from("output.png")), preserve_attrs: true } when you need the
input’s permissions and modification time on the output.
Handle failed inputs in a build
Missing or malformed input makes the CLI exit unsuccessfully. The Rust example propagates the
PngError through ?, so it also exits unsuccessfully and does not print the success byte counts.
Run it once without input.png to see this error path. A missing output directory or a write
permission error can also fail the operation.
Check the exit status before consuming an output file: an older output can still exist after a failed run. Successful optimization is also different from achieving a size reduction. In an asset build, treat failures as errors and record actual byte counts instead of requiring a fixed saving.
