Oxipng

Elixir bindings for oxipng, a PNG optimizer written in Rust.

Optimize PNG binaries and files, or encode PNGs from raw pixels. Calls are synchronous and use Rustler NIFs on dirty CPU schedulers. The calling process waits while normal BEAM schedulers remain available.

Compression is lossless by default. scale_16: true reduces precision, optimize_alpha: true can change hidden RGB values in transparent pixels, and stripping color metadata can affect image appearance.

Installation

Add the dependency to mix.exs:

{:oxipng, "~> 0.1.0"}

rustler_precompiled downloads NIF binaries from GitHub Releases for these platforms:

PlatformArchitectures
Linux (glibc)x86_64, ARM64, ARM32 hard-float
Linux (musl)x86_64, ARM64
macOSx86_64, ARM64
Windowsx86_64

These binaries do not require Rust. To compile from source, also add {:rustler, "~> 0.38.0"} to your dependencies, install Rust and a C toolchain, and set this before compiling:

export OXIPNG_BUILD=true

Usage

PNG binaries

{:ok, optimized} = Oxipng.optimize(png_binary)
{:ok, optimized} = Oxipng.optimize(png_binary, level: 4, strip: :safe)

Files

# Write to a separate file
{:ok, stats} = Oxipng.optimize_file("input.png", "output.png", level: 3)
# Optimize in place
{:ok, stats} = Oxipng.optimize_file("photo.png", level: 2)

File calls return {:ok, %{in_bytes: input_size, out_bytes: output_size}}. Empty filenames are rejected. Output is written to a temporary sibling and replaces the destination only after a successful write. The destination directory must be writable. Symbolic links are followed; other hard links retain the old contents.

Raw pixels

raw_rgba = <<255, 0, 0, 255, 0, 255, 0, 255>>
{:ok, png} = Oxipng.create_optimized_from_raw(raw_rgba, 2, 1, :rgba, 8)

Color types are :rgba, :rgb, :grayscale, :grayscale_alpha, and {:indexed, palette_binary}. Palettes contain 1–256 RGBA entries of four bytes each, must fit the bit depth, and must define every pixel index.

Grayscale supports bit depths 1, 2, 4, 8, and 16; indexed images support 1, 2, 4, and 8; other types support 8 and 16. Pack each row into whole bytes for depths below 8. Use big-endian samples for 16-bit data.

Each function also has a ! variant that returns the result directly or raises Oxipng.Error.

Options

All three functions accept a keyword list, an atom-keyed map, or an %Oxipng.Options{} struct. Unknown keys and malformed options return errors.

OptionDefaultDescription
:level2Optimization preset from 0 to 6.
:interlaceniltrue requests Adam7 interlacing; false requests removal. nil or :keep preserves it. Set force: true to apply changes even without a size improvement.
:strip:noneMetadata stripping policy; see below.
:optimize_alphafalseAllow RGB values of fully transparent pixels to change.
:bit_depth_reductiontrueAttempt bit depth reduction.
:color_type_reductiontrueAttempt color type reduction.
:palette_reductiontrueAttempt palette reduction.
:grayscale_reductiontrueAttempt grayscale reduction.
:idat_recodingtrueRecode IDAT chunks. Reductions can require recoding even when this is false.
:scale_16falseAllow lossy 16-bit to 8-bit scaling when bit depth reduction is enabled.
:deflaternilUse the preset default, {:libdeflater, 0..12}, :zopfli, {:zopfli, iterations}, or {:zopfli, iterations, without_improvement}.
:filtersnilUse the preset default or a list of filters described below.
:fast_evaluationnilUse the preset default, or set true/false to control fast filter evaluation.
:timeoutnilSoft budget in milliseconds. Skips further work after the deadline, but does not interrupt running compression.
:max_decompressed_sizenilMaximum decompressed input IDAT size in bytes.
:forcefalseReturn or write output even if it is not smaller than the input.
:fix_errorsfalseAttempt to recover from decoding errors.
:preserve_attrsfalseFor files, preserve permissions and modification time, but not access time.

Stripping policies:

Chunk names are four-byte strings or atoms, such as "tEXt" or :tEXt.

A custom filter list must be nonempty. Filters are :none, :sub, :up, :average, :paeth, :min_sum, :entropy, :bigrams, :big_ent, or {:brute, lines, level}. Brute filtering takes a positive line count and a compression level from 1 to 12.

Releasing

The review fixes retain version 0.1.0 for replacement of the initial release. Until its rebuilt archives and checksums are published, compile this checkout with OXIPNG_BUILD=true.

  1. Keep the versions in mix.exs and native/oxipng_nif/Cargo.toml aligned and commit the updated Cargo lockfile.
  2. Push the changes. The release workflow builds all archives, generates their checksums, and tests the precompiled Linux artifact. Tag pushes publish the verified archives, checksum manifest, and Hex package to GitHub Releases. To replace the existing release from the updated branch, run the workflow manually with publish enabled; it replaces assets for the project version.
  3. Copy the generated checksum-Elixir.Oxipng.Native.exs into the checkout before building a package manually. Always use the checksums of the rebuilt archives. The Hex archive produced by the workflow already contains the correct manifest.
  4. Replace the initial Hex package with OXIPNG_BUILD=true mix hex.publish --replace while replacement is permitted. Hex publishing is a separate step from the GitHub release workflow.

The checked-in manifest is empty while the replacement is being prepared. Source builds work without it; precompiled installations require the generated manifest.

License

MIT.