Breaking Changes
- add SVG input support rendered through
resvg(static SVG and gzipped SVGZ), usable with every output format - replace the
libavifAVIF decoder with adav1d-based one (dav1d+avif-parse+yuvutils-rs); decoding now links a system-installeddav1d(>= 1.3.0) through pkg-config instead of building libaom from source with cmake - report the side a failure happened on and give every outcome its own exit code:
0success,2usage,3input,4output,5partial, with1left unused because the runtime reports it for a signal or abort, so wrappers keying off the previous0/1must be updated - raise the minimum supported Rust version to 1.95.0 (
rust-version), required by thesysinfodependency behind the newlimitsfeature
Features
- add
-p/--preserve-timestampsto stamp an output with its input's last modification time instead of the time it was written; the temporary file is stamped before the publishing rename, so the output is never observable with the wrong time, and access times and creation times are left alone because birth time cannot be set at all on Linux - resize SVG inputs through the existing
--resizeoption by rendering the SVG vectorly withresvgdirectly at the final target size, so upscaling keeps the vector quality of the source - load system fonts for SVG text and substitute missing fonts with a warning: the serif default and the CJK fallback are resolved once at load time from platform-appropriate seeds (Times New Roman/Liberation Serif/DejaVu Serif, etc., YaHei/PingFang/Noto CJK, etc.), the
serifgeneric alias is pointed at a family that actually exists, and a text span is never dropped for the lack of a font — as a last resort it renders with whatever face the system has - derive the image size ceiling at runtime behind a new
limitsfeature instead of hard-coding it: the ceiling is the intersection of the dimensions a format itself declares as a hard limit (WebP's 16383, AVIF's 65536, and for JPEG and PNG the limit their decoder actually enforces), the memory available to the process divided by--threadsand the peak number of live pixel buffers, and the free space on the destination volume, and it now bounds every decode and encode path - reject oversized inputs from their file header before decoding, reading a bounded 64 KiB prefix: JPEG, PNG, AVIF, TIFF and WebP headers are probed for the dimensions they declare, so an image is refused before the allocation it would have caused
- classify every pipeline failure by the side it happened on, the format it concerns and a stable
kind()slug, with an actionablehintline, in place of the flat upstream enum that named neither direction nor format - add a hidden
--print-limitsdiagnostic that reports the probed budget, the per-format caps, the per-stage cost multipliers and both the pixel and byte ceilings together with the source that produced each, then exits without touching a file
Bug Fixes
- show warning-level logs by default without
RUST_LOG; drop the spurious premultiply warning that fired on every run without--premultiplyand demote the routine missing-ICC-profile notice to debug level - reject SVG render targets whose three live pixel buffers would exceed the decode budget instead of attempting multi-gigabyte allocations that could terminate the process through out-of-memory; the budget is now derived at runtime like every other ceiling, and the previous 512 MiB constant (currently 44,739,242 pixels) is kept as the fallback for standalone callers
- replace the panics and unchecked arithmetic that would abort the whole process under the release profile's
panic = "abort": guard the AVIF decoder against allocation overflow and out-of-bounds plane access, validate the pixel buffer before handing it toravif, clamp the number of used bits, and reject a buffer whose length disagrees with its dimensions instead of panicking in a worker thread - surface a failing
WebPConfig::new()as an encoder-construction error instead of unwrapping it, and validate the MozJPEG quality at the library boundary, since the public options type accepts anyf32while the encoder asserts on the rest - reject EXIF APP1 payloads above 65533 bytes, where the segment length used to overflow the
u16and wrap into a malformed JPEG - guard the SVG render buffer allocation against
usizeoverflow on 32-bit targets and with extremely large render targets - replace the premultiply
assert!and fiveindices_of().unwrap()call sites with a warning and a defensive fallback, so a user-facing argument-ordering conflict can no longer terminate the run - keep JSON metadata finite when the aspect ratio of a zero-height image is computed, accept a logger that is already installed instead of panicking, and log the reason when the current directory cannot be read
- preserve a source JPEG's EXIF by copying its APP1 segment verbatim, instead of decoding and re-encoding it through
little_exif, which rejects valid files that storeExifVersionas a STRING rather than UNDEF - carry the source JPEG's JFIF pixel density into the MozJPEG output, instead of leaving the default 1x1 aspect-ratio header that tools report as a resolution of 1 with no unit
- encode
--colorspace rgbwithJCS_RGBrather thanJCS_EXT_RGB - cap the WebP and SVG decoder reads at 256 MiB and reject TIFF images above 100 MP before allocating, since a decoder used as a library was otherwise unbounded
- reject dimensions that exceed the
u32limit of the OxiPNG and WebP FFI entry points instead of silently truncating them on 64-bit hosts - derive the JPEG and PNG ceilings from the limit their decoder enforces rather than the larger encoder-side constant, so the pre-check no longer admits a file the decoder then refuses with an unrelated message
- resolve the destination volume's free space against the nearest existing ancestor of the output path: canonicalizing a file that does not exist yet failed outright, and on Windows the verbatim prefix never matched the mount points
sysinforeports, leaving the disk check dead on that platform - divide the memory budget by
--threadsrather than anRIMAGE_THREADSvariable that nothing sets, which left the divisor at 1 while the whole worker pool was in flight - report a memory-derived byte ceiling as a memory limit rather than a disk limit when free space happens to look tighter, and name the source that produced each ceiling
- decode only the first frame of animated WebP and APNG instead of allocating a full-size buffer per frame; the still entry point is now tried first, which also stops forcing an alpha channel on files that do not have one, so such a file decodes as
RGB - give encode failures an output-side hint instead of input-side advice about a truncated file, and stop printing the path twice in failure log lines
- reject degenerate
--resizevalues at parse time (the@and%forms accepted@0,@-2,@nan,@inf,0%and-5%, and theWxHform accepted zero dimensions) and name the offending dimension in the malformedWxHmessage - stop stripping surrounding quotes and trailing backslashes from every
file.listline, which rewrote legitimate Unix file names, remove a warning that could never fire on valid UTF-8, and bound the list at 64 MiB - reject output names Windows would silently rewrite — trailing dots and spaces, and reserved device names such as
CON .png— and escalate the silent root-escape clamp to a warning, since it rewrites where output lands - compile and lint cleanly with decoders and encoders trimmed: gate the
AvailableEncoders::encodevariants, theFiledeclaration and the seek imports on the features that use them, and keep the no-codec andlimits-disabled builds warning-free - generate the CLI help epilogue from the enabled features so a trimmed build stops advertising subcommands that do not exist
- parse
--threadsasu16so hosts with more than 255 cores can actually use them - collapse the repeated metadata segment checks into match guards, clearing the remaining
cargo clippy --all-targets -- -D warningswarnings
Improvements
- cover the JPEG metadata scanner with tests built from synthetic segments, including the guard that keeps the first APP0/APP1 copy and the XMP-in-APP1 case that stops a bare APP1 marker from claiming the EXIF slot
- replace the hand-written Newton integer square root with
u64::isqrt - document why the ICC profile lock spans the transform, why the SVG intrinsic size is stored unrounded, and what
--subsamplevalues 3 and 4 do, and expand the subsampling introduction in the README and the MozJPEG CLI help - use
CARGO_MANIFEST_DIRin the test fixtures instead of paths that only exist on one development machine - drop the unneeded
unsafefrom the AVIFftyptest fixture, and make the input-side classification infallible so its callers no longer carry a dead fallback
Dependencies
- add
sysinfobehind the newlimitsfeature, the only implementation of the cgroup-aware memory probe and the mount-point lookup the size ceilings depend on - drop
thiserror, whichsrc/error.rsnever referenced - replace the unmaintained
pretty_env_loggerwithenv_logger, with the timestamp disabled to keep the compact single-line style
Build / CI
- auto-detect the MSVC and Windows SDK versions in
ci/msvc-env.shinstead of hard-coding paths that only exist on one machine, withRIMAGE_MSVC_ROOTandRIMAGE_SDK_VERoverrides for non-standard layouts - support ARM64 hosts in the same script by parameterising the toolchain architecture, falling back to the x64-hosted cross tools that Windows on ARM runs emulated
- pin the dav1d clone in the cross image and
build-dav1d.shto a verified commit, since upstream can re-point a tag, and add a usage check while dropping the GNU-onlyrealpathdependency - declare the build script's actual inputs so the resource compilation no longer re-runs whenever any file in the package changes, and tolerate an unset
CARGO_CFG_TARGET_OS