webpx 0.3.4

Complete WebP encoding/decoding with ICC profiles, streaming, and animation support
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
# Changelog

## [Unreleased]

## [0.3.4] - 2026-05-05

### Security

- **`StreamingDecoder::update` no longer holds a dangling pointer to
  the caller's buffer.** Earlier versions (≤ 0.3.3) called
  `libwebp_sys::WebPIUpdate`, which is documented by libwebp as "data
  buffer is not copied to the internal memory" — libwebp stores the
  raw `data.as_ptr()` and re-reads it on subsequent calls. Because
  the `update(&mut self, data: &[u8])` signature did not tie `data`'s
  lifetime to the decoder, a caller could drop the buffer between
  calls; the next `update` / `finish` / `get_partial` would
  dereference the dangling pointer. **Use-after-free reachable from
  safe Rust without an `unsafe` block.**

  Fix: route `update` through `append` (which copies internally via
  `WebPIAppend`). Functional behavior for the documented single-call
  use is unchanged; the cost is one `memcpy` of the input. The
  function signature is unchanged so no downstream code needs to
  update.

### Testing

- 19 soundness regression tests (was 18). New
  `streaming_decoder_update_does_not_dangle_input_buffer` exercises
  the canonical UAF shape (drop the input buffer between `update`
  and `finish`). Under ASan the prior code would trip a
  heap-buffer-overflow report; under the fix it must succeed
  because the data was copied during `update`.

## [0.3.3] - 2026-05-03

### Security

- **`progress_hook` (encode.rs) and `write_callback` (streaming.rs)
  now wrap the user-supplied callback in `catch_unwind`.** Both are
  invoked by libwebp from C frames; a panic from the user's
  `Stop::should_stop()` impl or `StreamingEncoder::*_with_callback`
  body would have unwound through libwebp's C stack — undefined
  behavior. The capture now stashes the panic payload in the encode
  context and re-raises it from a Rust frame after `WebPEncode`
  returns control. `#[cfg(feature = "std")]`; `no_std` builds
  typically configure `panic = "abort"` and don't reach the unwinding
  path.
- **`AnimationDecoder::next_frame` now guards against a null `buf`
  pointer** before constructing a Rust slice. libwebp's contract is
  that `WebPAnimDecoderGetNext` returns a non-null pointer on
  success; the guard turns a potential UB into a clean
  `DecodingError::BitstreamError` if libwebp ever violates the
  contract.
- **`decode.rs::decode_into` now uses `output.len().saturating_mul(bpp)`**
  for the buffer-size computation. On 32-bit `usize` (i686 in CI),
  the unchecked multiplication could wrap to 0 for caller-owned
  typed-pixel slices large enough that `len × bpp` exceeds
  `u32::MAX`; the wrapped size was passed to libwebp as the buffer
  byte count, mismatching the actual extent.
- **`config.rs::EncoderConfig::to_libwebp` now passes
  `WEBP_ENCODER_ABI_VERSION`** to `WebPConfigInitInternal` instead of
  the decoder constant. Both equal `528` in libwebp-sys 0.14.2 so
  the bug is harmless today; the fix prevents silent breakage if
  libwebp ever bumps the constants asymmetrically.
- **The `WebPConfig::new_with_preset()` helper from libwebp-sys uses
  `MaybeUninit::uninit()` internally**, the same latent-UB pattern
  fixed in earlier sweeps and PR #8 for the other three bindgen
  helpers. `EncoderConfig::to_libwebp` now goes through explicit
  `MaybeUninit::zeroed()` + `WebPConfigInitInternal`. This was the
  fourth and final bindgen `*::new()` helper webpx still routed
  through; an `unsafe`-block sweep confirms zero remaining sites.

### Added

- **`zencodec` Cargo feature with full trait integration.** webpx now
  exposes `pub mod zencodec` with the same wrapper-type names and
  method signatures as `zenwebp::zencodec`, so source code that
  consumes the trait surface compiles unchanged when swapping crates.
  All four executor traits are wired:
  - `zencodec::encode::Encoder` on `WebpEncoder` — RGBA / BGRA / RGB
    input, ICC / EXIF / XMP metadata, generic-quality and
    generic-effort calibration, `Limits` propagation.
  - `zencodec::encode::AnimationFrameEncoder` on
    `WebpAnimationFrameEncoder` — push / finalize lifecycle, with
    canvas-dimension validation across frames.
  - `zencodec::decode::Decode` + `push_decoder` on `WebpDecoder`    one-shot and sink-based single-image decoding.
  - `zencodec::decode::AnimationFrameDecoder` on
    `WebpAnimationFrameDecoder` — frame-by-frame and sink-based
    rendering, with `frame_count` / `loop_count` accessors.
  - `zencodec::decode::StreamingDecode` on `WebpStreamingDecoder`    buffered shape (single-batch); a row-batch path against
    libwebp's incremental decoder is a future iteration.
- **`From<webpx::Limits> for zencodec::ResourceLimits`** and the
  reverse impl. Field-for-field mirror minus `max_metadata_bytes`
  (no zencodec counterpart, drops on round-trip).
- **`examples-crates/zencodec-swap-demo/`** — workspace-member crate
  demonstrating the cfg-gated swap pattern with `--features
  use-webpx``use-zenwebp`. The demo's `main.rs` body is
  identical between backends; only the `use` import is cfg-gated.
  Both feature combos compile, run, and produce valid WebP via the
  same `zencodec` trait calls.
- **`pub mod zencodec`** re-export with `WebpAnimationFrameDecoder,
  WebpAnimationFrameEncoder, WebpDecodeJob, WebpDecoder,
  WebpDecoderConfig, WebpEncodeJob, WebpEncoder, WebpEncoderConfig,
  WebpStreamingDecoder` — names match `zenwebp::zencodec` exactly.

### Changed (internal — no public API change)

- **`WebpAnimationFrameEncoder` validates canvas dimensions across
  frames.** The first `push_frame` captures the canvas size; later
  frames with mismatched dimensions are rejected with
  `Error::InvalidInput` instead of being silently passed to
  libwebp's fixed-canvas animation encoder.
- **Saturating multiplications added to `width × bytes_per_pixel`
  sites** in `codec.rs:947`, `codec.rs:1021`, `animation.rs:551`.
  Within libwebp's 16383-cap dimensions these can't overflow today,
  but the change matches the rest of the codebase's discipline and
  prevents bitrot if libwebp ever raises its dimension cap.

### Merged (PR #8 from Shnatsel)

- **`AnimationEncoder::add_frame_internal`** — RAII `Picture::new()`
  + `picture.as_mut_ptr()` instead of the bindgen-generated
  `WebPPicture::new()` helper.
- **`Decoder::decode_advanced`** — explicit `MaybeUninit::zeroed()` +
  `WebPInitDecoderConfig` instead of `WebPDecoderConfig::new()`. This
  closes the third of four bindgen `*::new()` helpers using
  `MaybeUninit::uninit` internally.
- **`StreamingEncoder` callback + RGB paths**`Picture::new()` +
  `MemWriter::new()` RAII; defensive null/zero-size guard on
  `slice::from_raw_parts(data, data_size)` in the writer callback.

### Testing

- 18 soundness regression tests (was 17). New
  `progress_hook_panic_is_caught_and_replayed` exercises the
  panic-through-C fix.
- 7 zencodec integration tests covering encoder roundtrip, lossless
  preservation, animation roundtrip, streaming decode, limits
  propagation, capabilities table, and `Limits``ResourceLimits`
  round-trip.
- 12-agent parallel soundness audit run during this release;
  findings drove the Security entries above.

## [0.3.2] - 2026-05-02

### Added

- **`zencodec` feature flag** with a new `pub mod zencodec` exposing
  `WebpEncoderConfig`, `WebpEncodeJob`, `WebpEncoder`,
  `WebpAnimationFrameEncoder`, `WebpDecoderConfig`, `WebpDecodeJob`,
  `WebpDecoder`, `WebpStreamingDecoder`, `WebpAnimationFrameDecoder`.
  Type names and method signatures mirror `zenwebp::zencodec` exactly,
  so the same caller code compiles unchanged when swapping crates —
  the only line that changes is the `use` import. See
  `examples/zencodec_swap.rs` for the swap pattern.
- All four executor traits are wired:
  - `zencodec::encode::Encoder` on `WebpEncoder` — RGBA / BGRA / RGB
    input, ICC / EXIF / XMP metadata, generic-quality / generic-effort
    calibration, `Limits` propagation.
  - `zencodec::encode::AnimationFrameEncoder` on
    `WebpAnimationFrameEncoder` — accepts per-frame `PixelSlice`s,
    converts cumulative-timestamp on the trait side to webpx's
    timestamp-per-frame model, embeds metadata after assembly.
  - `zencodec::decode::Decode` on `WebpDecoder` — full single-image
    decode with caller-preferred descriptor selection.
  - `zencodec::decode::AnimationFrameDecoder` on
    `WebpAnimationFrameDecoder` — frame-by-frame and sink-based
    rendering, with `frame_count` / `loop_count` accessors.
  - `zencodec::decode::StreamingDecode` on `WebpStreamingDecoder`    buffered approach (full input → single batch). A row-batch
    implementation against libwebp's true incremental decoder is a
    future iteration.
- `From<webpx::Limits> for zencodec::ResourceLimits` and reverse.
  The two types were designed to mirror each other field-for-field;
  webpx's `max_metadata_bytes` is the only field without a zencodec
  counterpart and drops on the round trip.

### Documentation

- README points at the new example; lib-level rustdoc explains the
  swap pattern.

## [0.3.0] - 2026-05-02

### Security

- `Encoder::new_rgba` / `new_bgra` / `new_rgb` / `new_bgr` no longer
  panic with an arithmetic-overflow trap when constructed with
  `width >= 2^30` (RGBA/BGRA) or `width >= 2^30 + change` (RGB/BGR) —
  `width * 4` / `width * 3` overflowed `u32` *inside the constructor*,
  before the caller had a chance to validate dimensions. The fuzz
  campaign added in 0.2.3 (`dim_extremes`) caught it within seconds
  on its first run. Fix: `saturating_mul` on the byte stride; the
  oversize stride now flows through `validate_buffer_size_stride`
  and produces a clean `Error::InvalidInput` rather than a panic.

### Changed (breaking — caller may need to opt out of caps)

- **`Limits::default()` now applies opinionated production caps.**
  Previously `Limits::default()` returned `Limits::none()` (unbounded);
  callers who construct `DecoderConfig::default()` or
  `AnimationDecoder::with_options(...)` against untrusted input were
  silently relying on libwebp's intrinsic caps only. New default:
  `max_pixels = 64 MiP`, `max_total_pixels = 256 MiP`,
  `max_width = max_height = 16383`, `max_input_bytes = 64 MiB`,
  `max_frames = 4096`, `max_animation_ms = 5 min`,
  `max_metadata_bytes = 4 MiB`, `max_output_bytes = 256 MiB`.
  Code that needs the unbounded behavior must switch to
  `Limits::none()` explicitly.

### Testing

- New `examples/leak_test` harness exercises every webpx surface
  (encode, decode, streaming, animation, mux) in a tight loop. Used
  with `heaptrack` (`just leak-test`) or AddressSanitizer with
  `detect_leaks=1` (`just leak-test-asan`) to verify the FFI RAII
  layer cleans up libwebp's internal allocations on every path,
  including error returns. Wired into the ASan CI job. Local runs
  show zero webpx-attributable leaks; the sole reported leak is
  std's per-thread `stack_overflow::thread_info` allocation,
  released by OS process teardown but not visible to heaptrack
  (suppression added to the LSan list).
- `tests/soundness.rs` gained reproduction tests covering bug-history
  fixes that lacked direct repros: encoder constructor overflow
  panics on huge widths, `from_pixels_stride` truncation,
  `YuvPlanes::new_checked` rejection of out-of-range dims,
  `max_metadata_bytes` enforcement, `max_frames` enforcement against
  declared frame counts, `max_animation_ms` enforcement on
  cumulative timestamps, and `max_pixels` enforcement on
  oversize canvases. 17 soundness tests total.

### Documentation

- README and lib-level rustdoc explain that `Limits::default()` is
  now the safe baseline for untrusted input — callers no longer need
  to remember to apply caps explicitly. The "decoding untrusted
  input" example shows the new pattern (`Limits::default().with_*`).

## [0.2.3] - 2026-05-02

### Testing

- Four new fuzz targets focused on numeric input boundaries:
  `stride_extremes` (every encoder / decoder / streaming entry point
  with strides drawn from `i32::MAX ± N`, `u32::MAX`, and arbitrary),
  `dim_extremes` (width / height at libwebp's 16383 cap and beyond),
  `limits_boundaries` (`Limits` values at u32 / u64 boundaries with
  monotonicity probe), and `yuv_planes` (Y/U/V/A plane sizes /
  strides for `Encoder::new_yuv`). Wired into the cron fuzz matrix.

### Changed (internal — no public API change)

- Introduced an internal `crate::ffi` module with RAII wrappers for
  every libwebp resource that webpx owns. `Demux<'a>` and
  `ChunkIter<'_>` (used by the mux metadata path), `Picture` (used by
  every encode entry point), and `MemWriter` (the encode output
  buffer) auto-release on drop, replacing the bespoke
  `WebPPictureFree` / `WebPMemoryWriterClear` /
  `WebPDemuxReleaseChunkIterator` calls that used to be duplicated at
  every error branch — six sites in `encode.rs` alone, two in `mux.rs`.
  The audit surface for resource cleanup shrinks to one wrapper per
  resource. The wrappers are `pub(crate)`; this is purely an internal
  refactor and the public API is unchanged.
- Stride bounds checks for libwebp's `i32` parameters now live in one
  place: `crate::ffi::validate::stride_fits_i32`. Replaces five
  hand-rolled `if stride > i32::MAX` checks in `encode.rs` and
  `streaming.rs`.

### Documentation

- README, lib-level rustdoc, and the deprecation notes on
  `compat::webp` and `compat::webp_animation` now characterize
  webpx-vs-zenwebp performance honestly: libwebp can be up to ~35 %
  faster on specific photos but up to ~2.5× slower on others, and
  encoded-size differences are at most ~0.02 % (noise). Replaces the
  prior "comparable and sometimes faster" framing.
- Added native `wasm32-unknown-unknown` support to zenwebp's pros
  (webpx requires emscripten because libwebp is C).
- Added libwebp's MIPS DSP-R2 / DSP-ASE assembly paths to webpx's
  narrow set of remaining advantages.

## [0.2.2] - 2026-05-01

### Fixed

- `tests/integration.rs`, `tests/soundness.rs`, `tests/fuzz_regression.rs`,
  and several examples (`quick_encode`, `quick_encode_lossless`,
  `real_image_test`, `wasm_demo`, `mem_formula`) failed to compile
  under `--no-default-features --features decode` (or the symmetric
  `encode`-only build). Tests are now gated on the features they
  exercise; examples carry `required-features` entries; the
  feature-combo CI matrix gained `--tests --examples` builds and
  more single-feature combos so this kind of regression fails fast.
- `src/types.rs` and `src/streaming.rs` exposed encode-side helpers
  (`EncodePixel`, `WebPData::from_raw`, `StreamingEncoder`,
  `AnimationEncoder`) without an `encode` cfg and decode-side
  helpers (`DecodePixel`, `StreamingDecoder`,
  `DecoderConfig::check_still_image`) without a `decode` cfg.
  `cargo build --no-default-features --features <single>` is now
  warning-free.
- `StreamingDecoder::new` rejects `ColorMode::Yuv420` and
  `ColorMode::Yuva420` with `Error::InvalidInput` upfront. The
  prior code mapped them to `MODE_YUV` / `MODE_YUVA` and called
  `WebPINewRGB`, which returns NULL because that FFI only
  constructs RGB-family decoders — callers saw a misleading
  `Error::OutOfMemory`. `with_buffer` already had this rejection;
  `new` now matches.
- `mux::get_chunk` always calls `WebPDemuxReleaseChunkIterator` when
  `WebPDemuxGetChunk` returned non-zero, including the
  empty-chunk / null-bytes branch where the iterator was previously
  leaked. Refactored into a small `inspect_chunk` helper so the
  release path is single-pass.

### Documentation

- README, lib-level rustdoc, and the deprecation notes on
  `compat::webp` and `compat::webp_animation` now position
  **[`zenwebp`]https://github.com/imazen/zenwebp** as the
  recommended default for any new project. `zenwebp` is equally or
  more capable than `webpx` on every axis (full feature parity:
  lossy / lossless encode and decode, animation, alpha, ICC / EXIF /
  XMP metadata, streaming, presets, limits), with comparable and
  sometimes faster runtime performance and compression, built with
  `#![forbid(unsafe_code)]`. The security argument is concrete:
  libwebp has a documented high-severity CVE history (CVE-2023-4863
  was an actively-exploited 0-click heap overflow patched out of
  band in every major browser), and every libwebp wrapper that has
  been audited — `webpx` included — has shipped soundness bugs
  (0.1.x is yanked; 0.2.0 + 0.2.1 closed multiple FFI issues
  across two audit passes). `zenwebp`'s pure-safe-Rust
  implementation does not have that exposure.
- Dropped "safe Rust" / "safe bindings" framing from the webpx
  positioning (this crate is FFI to libwebp; it inherits whatever
  soundness properties the underlying C provides). Replaced with
  "ergonomic Rust API".

## [0.2.1] - 2026-05-01

### Security

Closes a class of OOB issues found in a second-pass audit: caller-supplied
`u32` / `usize` strides cast to `i32` for libwebp without an upper-bound
check. A stride `>= 2^31` wrapped to a negative `i32`, and libwebp's row
pointer arithmetic walked backwards through process memory. Reachable
from safe Rust without an `unsafe` block by constructing a `&[u8]` of
≥ 2 GiB on 64-bit and passing a stride above `i32::MAX`. Affected sites:
`Encoder::new_rgba_stride` / `new_bgra_stride` / `new_rgb_stride` /
`new_bgr_stride` / `new_argb_stride` / `new_yuv` (every plane stride),
and `StreamingDecoder::with_buffer`.

Also fixed: `Encoder::from_img` and `Encoder::from_pixels_stride`
truncated `(stride * bpp) as u32` when the source `usize` stride
exceeded `u32::MAX / bpp`. Truncation produced a stride smaller than
the actual byte stride, which `validate_buffer_size_stride` could not
catch — libwebp then encoded with a wrong row layout. Not OOB on the
input slice (which still covered the buffer) but a silent
wrong-output bug; saturating cast added so oversized strides hit the
`i32::MAX` upper-bound check instead.

### Changed

- All `MaybeUninit::<libwebp_sys::*>::uninit()` sites switched to
  `MaybeUninit::<libwebp_sys::*>::zeroed()`. libwebp's `*Init*`
  functions only set their documented fields; the `pad` /
  `padding` arrays they leave alone are now initialized to zero
  rather than uninitialized. Resolves a latent UB pattern (reading
  uninit bytes during `assume_init` is technically UB even when
  those bytes are never observed).

### Deprecated

- The compatibility shim modules `compat::webp` and
  `compat::webp_animation` are now `#[deprecated]`. Both swallow
  error variants the main API surfaces (compat `Encoder::encode`
  returns an empty buffer on failure; compat `Decoder::decode`
  collapses every error to `None`) and neither exposes
  [`Limits`]crate::Limits for untrusted-input decoding. They will
  be retained for at least one minor release.

### Documentation

- `Encoder::new_argb` rustdoc now spells out the
  little-endian-only assumption: the `0xAARRGGBB` numeric layout
  matches libwebp's expected `[B, G, R, A]` byte order on
  little-endian targets (every CI target). Big-endian callers
  should treat this path as unsupported.

### Testing

- `tests/soundness.rs` gains a `stride_overflow` module with four
  regression tests covering ARGB, RGBA, YUV, and streaming
  `with_buffer` stride bounds.


### Added

- `webpx::Limits` — opt-in resource policy modeled on
  `zencodec::ResourceLimits`. Fields: `max_pixels` (per-frame),
  `max_total_pixels` (across all animation frames),
  `max_width`, `max_height`, `max_input_bytes`, `max_frames`,
  `max_animation_ms`, `max_metadata_bytes`, `max_output_bytes`.
  All `Option<T>`; `None` means no webpx-side cap.
  Auto-enforced fields (`max_pixels`, `max_total_pixels`,
  `max_width`, `max_height`, `max_frames`, `max_input_bytes`,
  `max_animation_ms`, `max_metadata_bytes`) are checked at
  parse time on the `_with_limits` decoder / animation /
  mux entry points; see the [`Limits`] docs for the per-field
  enforcement matrix.
- `webpx::LimitExceeded` — error variant carrying the actual value
  and the limit that was exceeded. Returned via the new
  `Error::LimitExceeded` variant, also implements
  `core::error::Error` standalone.
- `DecoderConfig::limits(Limits)` and `::get_limits()` — apply the
  policy at parse time. `max_width`, `max_height`, `max_pixels`, and
  `max_total_pixels` are checked via `WebPGetFeatures` before
  libwebp allocates the output buffer; if `scale()` is also set,
  the post-scale dimensions are checked too.
- `AnimationDecoder::with_options_limits(data, mode, threads, &Limits)`
  + `AnimationDecoder::get_limits()` — applies limits against the
  canvas dimensions, declared `frame_count`, and input size before
  any decode work. `max_total_pixels` catches the W × H × N
  animation-bomb case that per-frame `max_pixels` misses; the
  decoder also enforces `max_animation_ms` against the cumulative
  frame timestamp inside `decode_all`. Closes [#4].
- `mux::get_icc_profile_with_limits` / `get_exif_with_limits` /
  `get_xmp_with_limits` — apply `Limits::max_metadata_bytes` on
  top of the existing 256 MiB internal hard cap.

### Changed (breaking)

- `EncoderConfig` and `DecoderConfig` are now `#[non_exhaustive]`.
  They have `pub(crate)` fields so external struct-literal
  construction was already disallowed; the marker documents that
  these structs will grow (`limits`, future config knobs) without
  another minor bump.
- `error::EncodingError`, `error::DecodingError`, `error::MuxError`
  and `types::ImageInfo` are now `#[non_exhaustive]`. Match arms must
  add `_ =>` and struct construction must go through a constructor.
  This closes the door on adding new libwebp error variants or new
  bitstream-info fields requiring another minor bump.
- `Error` gains a `LimitExceeded(LimitExceeded)` variant. Already
  `#[non_exhaustive]`, so adding the variant is a non-breaking
  additive change for `match` arms with `_ =>`.

### Fixed

- The crate now compiles under every feature combination
  (`--no-default-features` with any subset of `decode` / `encode` /
  `std` / `streaming` / `animation` / `icc`). The compat layer's
  `Encoder` / `Decoder` types are now properly gated on the matching
  feature, and `EncoderConfig`'s encoding entry points are gated on
  `encode`. CI gains a feature-combo matrix to prevent regression.
  Closes [#1].

[#1]: https://github.com/imazen/webpx/issues/1
[#4]: https://github.com/imazen/webpx/issues/4

## [0.2.0] - 2026-05-01

### Security

Soundness and bounds-checking fixes from a multi-pass audit of the FFI layer
(initial review plus parallel sweeps for integer overflow, 32-bit-only
flaws, additional `unsafe` review, and unbounded-resource exposure). Full
technical detail and reproducers will be published in a coordinated security
advisory once 0.2.0 is on crates.io. Versions 0.1.0–0.1.4 will be yanked at
that time. Users on those versions should upgrade.

### Added

- `YuvPlanes::new_checked(width, height, with_alpha) -> Option<Self>`  non-panicking constructor that rejects out-of-range dimensions.
- `validate_yuv_planes` (internal) — bounds-checks Y/U/V/A planes
  against stride × rows before pointer-casting into libwebp.
- Hard cap (`MAX_METADATA_CHUNK_BYTES`, 256 MiB) on ICCP/EXIF/XMP chunk
  sizes returned by `get_icc_profile` / `get_exif` / `get_xmp` to bound
  attacker-controlled allocations from the demuxer.

### Changed (breaking)

- `StreamingDecoder` is now `StreamingDecoder<'a>`. The `'a` parameter
  ties a buffer-backed decoder to the buffer's lifetime so the borrow
  checker rejects use-after-free patterns. `StreamingDecoder::new()`
  returns `StreamingDecoder<'static>`, so call sites that don't use
  `with_buffer` compile unchanged. `StreamingDecoder` no longer
  auto-implements `UnwindSafe` / `RefUnwindSafe` as a side effect.
- `AnimationDecoder::with_options` now returns `Err` for color modes
  that libwebp's animation decoder cannot satisfy
  (`ColorMode::Rgb`, `Bgr`, `Argb`). Previously the constructor
  accepted these and `WebPAnimDecoderNew` later returned NULL with
  no explanation.
- `Encoder::new_yuv` now returns an error if the supplied
  `YuvPlanesRef` has plane lengths shorter than `stride × rows`,
  if the strides are below `width`/`uv_width`, or if `u_stride !=
  v_stride` (libwebp uses a single uv_stride field).
- `AnimationDecoder::decode_all` now caps its initial `Vec::with_capacity`
  reservation at 4096 frames; declared frame counts above that are
  appended via `push()` rather than reserved up-front.

### Fixed

- Encoder zero-copy ARGB and YUV-with-alpha paths now keep their input
  buffers strictly read-only.
- `AnimationDecoder::next_frame` derives the frame buffer length from
  the configured color mode rather than a hard-coded value.
- `ImageInfo::from_webp` no longer treats `WebPBitstreamFeatures` as
  initialized when `WebPGetFeatures` reports an error.
- `decode_advanced` calls `WebPFreeDecBuffer` defensively on the error
  path and uses libwebp's reported allocation size for the output slice.
- All `width × height × bpp`, `stride × rows`, and `slice.len() × bpp`
  computations that consume user- or libwebp-supplied dimensions now
  use `saturating_mul`. Previously the unchecked products could wrap
  on 32-bit `usize` (i686 is in the CI matrix) and bypass downstream
  length guards.
- `StreamingDecoder::get_partial` and `::finish` reject negative
  strides / dimensions returned by libwebp so `as usize` cannot wrap
  into a multi-gigabyte read.
- `decode_yuv` validates `y_stride`, `uv_stride`, `height`, and
  `uv_height` are non-negative before computing plane sizes.
- `decode_into` rejects strides that would not fit in libwebp's `i32`
  stride parameter.
- `AnimationDecoder::decode_all` uses `saturating_sub` when computing
  per-frame durations from declared timestamps, preventing wrap on
  hostile non-monotonic timestamps.
- `YuvPlanes::new` validates dimensions before allocating; the
  panicking constructor now panics with a clear message rather than
  via the underlying `vec!` macro on capacity overflow.