oxideav_webp/lib.rs
1//! # oxideav-webp
2//!
3//! Pure-Rust WebP image codec — clean-room scaffold built against
4//! RFC 9649 (WebP Image Format).
5//!
6//! Round 1 landed the **structural** RIFF/WEBP container walker
7//! ([`container::parse`]). Round 2 added typed field decoding for the
8//! `VP8X` extended-format header ([`vp8x::Vp8xHeader::parse`]). Round 3
9//! added typed field decoding for the §2.7.1.1 `ANIM` / §2.7.1.2 `ALPH`
10//! metadata chunks. Round 4 added typed field decoding for the
11//! per-frame §2.7.1.1 `ANMF` header. Round 5 added the **builder**
12//! side of the RIFF/WEBP container — the inverse of the walker — so
13//! external encoders can wrap a `VP8 ` / `VP8L` payload in a
14//! well-formed file. Round 6 adds a typed §2.5 `VP8 ` chunk handle
15//! ([`vp8_chunk::WebpLossyChunk`]) that lets container-layer callers
16//! route the VP8 payload to a downstream VP8 decoder **without**
17//! `oxideav-webp` taking a runtime dependency on `oxideav-vp8`.
18//!
19//! * [`alph::AlphHeader::parse`] — the `ALPH` info byte
20//! (`Rsv|P|F|C`).
21//! * [`alph::decode_alpha`] — the §2.7.1.2 alpha-bitstream decode
22//! (round 110): both compression methods (raw + headerless VP8L,
23//! the latter lifting alpha from the GREEN channel) and the four
24//! inverse filters (none / horizontal / vertical / gradient) with
25//! the documented left-most / top-most edge cases, producing the
26//! full-resolution alpha plane. [`decode_alpha_plane`] is the
27//! container-level entry point: walk the file, take dimensions from
28//! `VP8X` (or the `VP8 ` keyframe), find the `ALPH` chunk, decode.
29//! * [`anim::AnimHeader::parse`] — the `ANIM` 6-byte payload
30//! (BGRA background colour + u16 loop count).
31//! * [`anmf::AnmfHeader::parse`] — the `ANMF` 16-byte per-frame
32//! header (frame X / Y / width / height / duration plus
33//! `Reserved|B|D` info byte).
34//! * [`build::build_chunk`] — generic §2.3 chunk writer (FourCC +
35//! Size + payload + odd-size pad).
36//! * [`build::build_vp8x_chunk`] — §2.7.1 Figure 7 typed VP8X
37//! payload writer.
38//! * [`build::build_webp_file`] — §2.4 file writer for simple
39//! (`VP8 ` / `VP8L`) and extended (`VP8X` + `VP8 ` / `VP8L`)
40//! layouts.
41//! * [`vp8_chunk::WebpLossyChunk`] — typed §2.5 `VP8 ` chunk
42//! handle. Peeks the RFC 6386 §9.1 keyframe header (width /
43//! height / version / first_partition_size / scale fields) and
44//! exposes the chunk payload via [`vp8_chunk::WebpLossyChunk::bitstream`]
45//! for routing to an external VP8 decoder.
46//! * [`vp8l_chunk::WebpLosslessChunk`] — typed §2.6 `VP8L` chunk
47//! handle. Peeks the §3.4 / §7.1 5-byte VP8L image-header
48//! (`0x2F` signature + 14-bit `width-1` + 14-bit `height-1` +
49//! `alpha_is_used` bit + 3-bit `version`) and exposes the chunk
50//! payload via [`vp8l_chunk::WebpLosslessChunk::bitstream`] for
51//! routing to an external VP8L decoder.
52//! * [`vp8l_stream::TransformList`] — the §4 transform-presence loop
53//! (round 99): each present transform's leading fixed fields, stopping
54//! at the first §5 entropy-coded body.
55//! * [`vp8l_prefix::PrefixCode`] — the §6.2.1 prefix-code reader
56//! (round 104): reads a single canonical prefix code's lengths off
57//! the wire (simple or normal code length code) and decodes symbols
58//! one at a time. This is the first piece of the §5 / §6 entropy
59//! machinery the §4 transform bodies and the main image stream both
60//! consume.
61//! * [`meta_prefix::MetaPrefixHeader`] — the §5.2.3 color-cache info,
62//! §6.2.2 meta-prefix dispatch, and §6.2 5-prefix-code-group reader
63//! (round 106). Surfaces either a fully-built single
64//! [`meta_prefix::PrefixCodeGroup`] (the common case: single
65//! meta-Huffman group, or any non-ARGB role) or, when an ARGB image
66//! selects an entropy image, the entropy-image dimensions plus the
67//! bit position at which the §5.2-encoded entropy image starts (for
68//! the next round to resume from once §5.2 LZ77 + color-cache decode
69//! lands).
70//! * [`vp8l_decode::decode_image`] — the §5.2 LZ77 backward-reference +
71//! §5.2.3 color-cache per-pixel ARGB decode loop (round 107). Runs
72//! the §6.2.3 GREEN symbol dispatch (literal / LZ77 length+distance /
73//! color-cache code) over a single [`meta_prefix::PrefixCodeGroup`]
74//! and produces a [`vp8l_decode::DecodedImage`] of ARGB pixels in
75//! scan-line order (before any §4 inverse transform). Includes the
76//! §5.2.2 prefix→value transform, the 120-element distance map, and
77//! the §5.2.3 `0x1e35a7bd` color cache.
78//! * [`vp8l_decode::decode_argb`] — the §6.2.2 multi-group ARGB decode
79//! (round 108). Reads the round-106 [`meta_prefix::MetaPrefixHeader`]
80//! for the ARGB role and, when the meta-prefix bit selects multiple
81//! groups, decodes the §6.2.2 *entropy image*
82//! ([`vp8l_decode::decode_entropy_image`] →
83//! [`vp8l_decode::MetaPrefixIndex`]), derives
84//! `num_prefix_groups = max(entropy image) + 1`, reads that many
85//! prefix-code groups, and runs the §6.2.3 loop selecting a group per
86//! pixel block via
87//! `meta_index[(y >> prefix_bits) * block_width + (x >> prefix_bits)]`.
88//! Single-group images degrade to the round-107 path. Per §6.2.2 each
89//! block's meta-prefix code is the red+green channels of its
90//! entropy-image pixel (`(argb >> 8) & 0xffff`).
91//! * [`vp8l_transform::decode_lossless`] — the §4 inverse-transform
92//! passes (round 109). Reads the §4 transform list (each transform's
93//! fixed fields **and** its §5-encoded body), decodes the main ARGB
94//! image at the (color-indexing-subsampled) width, then applies the
95//! four inverse transforms in reverse read order: §4.1 predictor (14
96//! prediction modes + border rules over the block grid), §4.2 color
97//! (per-block `ColorTransformElement` add-back), §4.3 subtract-green
98//! (add green into red/blue), and §4.4 color-indexing (palette lookup
99//! plus ≤16-color pixel un-bundling). The container-level entry point,
100//! [`decode_lossless_image`], walks the file, extracts the `VP8L`
101//! chunk, and decodes to a [`vp8l_decode::DecodedImage`]. Bit-exact
102//! against the `lossless-1x1`, `lossless-color-indexing-paletted`, and
103//! `lossless-32x32-rgba` (SUBTRACT_GREEN + PREDICTOR + CROSS_COLOR +
104//! color cache) fixture PNGs.
105//!
106//! * [`vp8_decode::decode_lossy_rgba`] — the §2.5 `VP8 ` (lossy) decode
107//! path (round 124). Routes the `VP8 ` chunk payload to the
108//! `oxideav-vp8` sibling crate's [`oxideav_vp8::decode_vp8`] entry
109//! point, which reconstructs the loop-filtered I420 key-frame, then
110//! converts it to interleaved RGBA via nearest-neighbour chroma
111//! up-sampling and the RFC 6386 §9.2 ITU-R BT.601 full-range YCbCr→RGB
112//! matrix.
113//! * [`decode_webp_image`] / [`decode_webp`] — the top-level still-image
114//! entry points (round 111). They walk the container, decode a §2.6 /
115//! §3.4 `VP8L` lossless image (simple or `VP8X`-extended) through the
116//! full §4–§6 chain, optionally override its alpha from a §2.7.1.2
117//! `ALPH` chunk, and return interleaved 8-bit `[R, G, B, A]` pixels
118//! ([`DecodedWebp`]) — the `oxideav_core::PixelFormat::Rgba` layout
119//! the workspace's image crates share. As of round 124 a §2.5 `VP8 `
120//! lossy file is also decoded (via `oxideav-vp8`), with a §2.7.1.2
121//! `ALPH` chunk layering the alpha plane over the opaque VP8 picture.
122//!
123//! Both the §2.5 `VP8 ` lossy and §2.6 `VP8L` lossless image-data paths
124//! now decode end-to-end (the lossy path through the `oxideav-vp8`
125//! sibling crate). The §2.7.1.2 ALPH alpha bitstream is also decoded
126//! end-to-end ([`alph::decode_alpha`] / [`decode_alpha_plane`]). VP8 /
127//! VP8L bitstream *encode* remains framing-only — the builders take an
128//! externally pre-computed codec payload.
129
130#![warn(missing_debug_implementations)]
131// Opt-in `std::simd` acceleration of the hottest pixel-repack /
132// inverse-transform loops. Nightly-only because `portable_simd` is
133// still an unstable feature; every SIMD path has a stable scalar
134// fallback that produces byte-identical output. See `BENCHMARKS.md`
135// and the `simd` cargo feature in `Cargo.toml`.
136#![cfg_attr(feature = "simd", feature(portable_simd))]
137
138pub mod alph;
139pub mod anim;
140pub mod anim_encode;
141pub mod anmf;
142pub mod build;
143pub mod container;
144pub mod decoder;
145pub mod demux;
146pub mod encoder;
147pub mod encoder_anim;
148pub mod encoder_vp8;
149pub mod error;
150pub mod meta_prefix;
151#[cfg(feature = "registry")]
152pub mod registry;
153pub mod riff;
154pub mod vp8_chunk;
155pub mod vp8_decode;
156pub mod vp8l;
157pub mod vp8l_chunk;
158pub mod vp8l_decode;
159pub mod vp8l_encode;
160pub mod vp8l_prefix;
161pub mod vp8l_stream;
162pub mod vp8l_transform;
163pub mod vp8x;
164
165#[cfg(feature = "registry")]
166use oxideav_core::RuntimeContext;
167
168/// Streaming [`oxideav_core::Decoder`] implementation — re-export of the
169/// in-crate [`registry::WebpDecoder`] under the published crate-root
170/// path per the published 0.1.2 surface.
171#[cfg(feature = "registry")]
172pub use registry::WebpDecoder;
173
174/// Crate-local error type.
175#[derive(Debug, Clone, PartialEq, Eq)]
176pub enum Error {
177 /// A code path that has not been wired up yet in this round.
178 NotImplemented,
179 /// The file is well-formed but carries an image kind this crate does
180 /// not decode yet. Currently this is the §2.5 `VP8 ` lossy
181 /// bitstream — routed out via [`extract_lossy_chunk`] to a downstream
182 /// VP8 decoder rather than decoded here.
183 Unsupported(UnsupportedKind),
184 /// The RIFF/WEBP container walker rejected the input.
185 Container(container::ContainerError),
186 /// The §2.7.1 VP8X chunk parser rejected the input.
187 Vp8x(vp8x::Vp8xError),
188 /// The §2.7.1.2 ALPH info-byte parser rejected the input.
189 Alph(alph::AlphError),
190 /// The §2.7.1.1 ANIM payload parser rejected the input.
191 Anim(anim::AnimError),
192 /// The §2.7.1.1 ANMF per-frame header parser rejected the input.
193 Anmf(anmf::AnmfError),
194 /// The §2.3 / §2.4 / §2.7.1 RIFF/WEBP builders rejected the input.
195 Build(build::BuildError),
196 /// The §2.5 typed `VP8 ` chunk handle rejected the chunk payload.
197 Lossy(vp8_chunk::WebpLossyError),
198 /// The §2.5 `VP8 ` lossy bitstream decode (delegated to the
199 /// `oxideav-vp8` sibling crate) rejected the payload.
200 ///
201 /// Wraps `oxideav-vp8`'s published [`oxideav_vp8::DecodeError`]. (Once
202 /// vp8 publishes its `Vp8Error` umbrella — currently on vp8 master
203 /// but not on crates.io — this can widen to that type.)
204 Vp8(oxideav_vp8::DecodeError),
205 /// The §2.6 typed `VP8L` chunk handle rejected the chunk payload.
206 Lossless(vp8l_chunk::WebpLosslessError),
207 /// The §4 VP8L transform-list reader rejected the bitstream.
208 Vp8lTransform(vp8l_stream::TransformListError),
209 /// The §6.2.1 VP8L prefix-code reader rejected the bitstream.
210 Vp8lPrefix(vp8l_prefix::PrefixError),
211 /// The §5.2.3 / §6.2.2 VP8L meta-prefix header reader rejected the
212 /// bitstream.
213 Vp8lMetaPrefix(meta_prefix::MetaPrefixError),
214 /// The §5.2 VP8L per-pixel ARGB decode loop rejected the bitstream.
215 Vp8lDecode(vp8l_decode::DecodeError),
216 /// The §3.7 / §3.8 VP8L lossless encoder rejected the input.
217 Vp8lEncode(vp8l_encode::EncodeError),
218}
219
220/// Which image kind [`decode_webp`] declined to decode.
221#[derive(Debug, Clone, Copy, PartialEq, Eq)]
222pub enum UnsupportedKind {
223 /// The file's image data is a §2.5 `VP8 ` lossy bitstream that the
224 /// caller has chosen to route out-of-crate.
225 ///
226 /// As of round 124 the default still-image decode path decodes `VP8 `
227 /// lossy via the `oxideav-vp8` sibling crate, so this variant is no
228 /// longer produced by [`decode_webp`] / [`decode_webp_image`]. It is
229 /// retained for callers that explicitly route the raw VP8 bitstream
230 /// elsewhere via [`extract_lossy_chunk`].
231 LossyVp8,
232 /// The file carries neither a `VP8L` nor a `VP8 ` image-data chunk
233 /// (e.g. an animation: the pixels live inside per-frame `ANMF`
234 /// sub-RIFFs, which this still-image entry point does not assemble).
235 NoImageData,
236}
237
238impl core::fmt::Display for UnsupportedKind {
239 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
240 match self {
241 Self::LossyVp8 => f.write_str("VP8 lossy bitstream (route to a VP8 decoder)"),
242 Self::NoImageData => {
243 f.write_str("no VP8L/VP8 image-data chunk (animation or header-only)")
244 }
245 }
246 }
247}
248
249impl core::fmt::Display for Error {
250 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
251 match self {
252 Self::NotImplemented => f.write_str("oxideav-webp: pixel decode not implemented yet"),
253 Self::Unsupported(k) => write!(f, "oxideav-webp: unsupported image kind: {k}"),
254 Self::Container(e) => write!(f, "oxideav-webp container: {e}"),
255 Self::Vp8x(e) => write!(f, "oxideav-webp vp8x: {e}"),
256 Self::Alph(e) => write!(f, "oxideav-webp alph: {e}"),
257 Self::Anim(e) => write!(f, "oxideav-webp anim: {e}"),
258 Self::Anmf(e) => write!(f, "oxideav-webp anmf: {e}"),
259 Self::Build(e) => write!(f, "oxideav-webp build: {e}"),
260 Self::Lossy(e) => write!(f, "oxideav-webp lossy: {e}"),
261 Self::Vp8(e) => write!(f, "oxideav-webp vp8: {e}"),
262 Self::Lossless(e) => write!(f, "oxideav-webp lossless: {e}"),
263 Self::Vp8lTransform(e) => write!(f, "oxideav-webp vp8l-transform: {e}"),
264 Self::Vp8lPrefix(e) => write!(f, "oxideav-webp vp8l-prefix: {e}"),
265 Self::Vp8lMetaPrefix(e) => write!(f, "oxideav-webp vp8l-meta-prefix: {e}"),
266 Self::Vp8lDecode(e) => write!(f, "oxideav-webp vp8l-decode: {e}"),
267 Self::Vp8lEncode(e) => write!(f, "oxideav-webp vp8l-encode: {e}"),
268 }
269 }
270}
271
272impl std::error::Error for Error {}
273
274impl From<container::ContainerError> for Error {
275 fn from(e: container::ContainerError) -> Self {
276 Self::Container(e)
277 }
278}
279
280impl From<vp8x::Vp8xError> for Error {
281 fn from(e: vp8x::Vp8xError) -> Self {
282 Self::Vp8x(e)
283 }
284}
285
286impl From<alph::AlphError> for Error {
287 fn from(e: alph::AlphError) -> Self {
288 Self::Alph(e)
289 }
290}
291
292impl From<anim::AnimError> for Error {
293 fn from(e: anim::AnimError) -> Self {
294 Self::Anim(e)
295 }
296}
297
298impl From<anmf::AnmfError> for Error {
299 fn from(e: anmf::AnmfError) -> Self {
300 Self::Anmf(e)
301 }
302}
303
304impl From<build::BuildError> for Error {
305 fn from(e: build::BuildError) -> Self {
306 Self::Build(e)
307 }
308}
309
310impl From<vp8_chunk::WebpLossyError> for Error {
311 fn from(e: vp8_chunk::WebpLossyError) -> Self {
312 Self::Lossy(e)
313 }
314}
315
316impl From<oxideav_vp8::DecodeError> for Error {
317 fn from(e: oxideav_vp8::DecodeError) -> Self {
318 Self::Vp8(e)
319 }
320}
321
322impl From<vp8l_chunk::WebpLosslessError> for Error {
323 fn from(e: vp8l_chunk::WebpLosslessError) -> Self {
324 Self::Lossless(e)
325 }
326}
327
328impl From<vp8l_stream::TransformListError> for Error {
329 fn from(e: vp8l_stream::TransformListError) -> Self {
330 Self::Vp8lTransform(e)
331 }
332}
333
334impl From<vp8l_prefix::PrefixError> for Error {
335 fn from(e: vp8l_prefix::PrefixError) -> Self {
336 Self::Vp8lPrefix(e)
337 }
338}
339
340impl From<meta_prefix::MetaPrefixError> for Error {
341 fn from(e: meta_prefix::MetaPrefixError) -> Self {
342 Self::Vp8lMetaPrefix(e)
343 }
344}
345
346impl From<vp8l_decode::DecodeError> for Error {
347 fn from(e: vp8l_decode::DecodeError) -> Self {
348 Self::Vp8lDecode(e)
349 }
350}
351
352impl From<vp8l_encode::EncodeError> for Error {
353 fn from(e: vp8l_encode::EncodeError) -> Self {
354 Self::Vp8lEncode(e)
355 }
356}
357
358/// Walk a `RIFF/WEBP` container per RFC 9649 §2.3–§2.7 and return
359/// the structural chunk list. This is the round-1 surface: it does
360/// not decode any payload.
361pub fn parse_container(bytes: &[u8]) -> Result<container::WebpContainer, Error> {
362 container::parse(bytes).map_err(Into::into)
363}
364
365/// Decode the §2.7.1 `VP8X` chunk payload to a typed
366/// [`vp8x::Vp8xHeader`].
367///
368/// The argument is the **payload** of a `VP8X` chunk — exactly the
369/// 10 bytes following the 8-byte chunk header. The recommended call
370/// pattern is to walk the container first, locate the chunk whose
371/// FourCC is [`container::fourcc::VP8X`], borrow its payload via
372/// [`container::WebpChunk::payload`], and hand that slice to this
373/// function.
374pub fn parse_vp8x_header(payload: &[u8]) -> Result<vp8x::Vp8xHeader, Error> {
375 vp8x::Vp8xHeader::parse(payload).map_err(Into::into)
376}
377
378/// Decode the §2.7.1.2 `ALPH` chunk info byte to a typed
379/// [`alph::AlphHeader`].
380///
381/// The argument is the **payload** of an `ALPH` chunk — i.e. the
382/// slice returned by [`container::WebpChunk::payload`] for a chunk
383/// whose FourCC is [`container::fourcc::ALPH`]. Only the first byte
384/// is consumed by this layer; the rest of the payload is the alpha
385/// bitstream proper, which is decoded by [`alph::decode_alpha`].
386pub fn parse_alph_header(payload: &[u8]) -> Result<alph::AlphHeader, Error> {
387 alph::AlphHeader::parse(payload).map_err(Into::into)
388}
389
390/// Walk a `RIFF/WEBP` buffer and, if it carries a §2.7.1.2 `ALPH`
391/// chunk, fully decode the alpha bitstream to a `width * height` plane
392/// of 8-bit alpha values in scan order.
393///
394/// The alpha-plane dimensions are taken from the file in this priority
395/// order, matching how a still image carries its canvas size:
396///
397/// 1. the §2.7.1 `VP8X` canvas dimensions, if a `VP8X` chunk exists;
398/// 2. otherwise the §2.5 `VP8 ` keyframe dimensions (a simple-lossy
399/// file with an `ALPH` chunk but no `VP8X`).
400///
401/// Returns `Ok(None)` if the file is well-formed but carries no `ALPH`
402/// chunk. The decode covers both §2.7.1.2 compression methods
403/// (raw + VP8L-lossless) and all four filtering methods — see
404/// [`alph::decode_alpha`].
405///
406/// This handles the **still-image** alpha path. Per-frame (`ANMF`)
407/// alpha planes are addressed by walking the `ANMF` frame data with
408/// [`alph::decode_alpha`] directly, using the frame dimensions.
409pub fn decode_alpha_plane(bytes: &[u8]) -> Result<Option<Vec<u8>>, Error> {
410 let c = container::parse(bytes)?;
411 let alph_chunk = match c.first_chunk_with_fourcc(container::fourcc::ALPH) {
412 Some(chunk) => chunk,
413 None => return Ok(None),
414 };
415
416 // Dimensions: VP8X canvas first, else the VP8 keyframe header.
417 let (width, height) = if let Some(vp8x) = c.first_chunk_with_fourcc(container::fourcc::VP8X) {
418 let hdr = vp8x::Vp8xHeader::parse(vp8x.payload(bytes))?;
419 (hdr.canvas_width, hdr.canvas_height)
420 } else if let Some(vp8) = c.first_chunk_with_fourcc(container::fourcc::VP8) {
421 let lossy = vp8_chunk::WebpLossyChunk::from_chunk(bytes, vp8)?;
422 (u32::from(lossy.width()), u32::from(lossy.height()))
423 } else {
424 // No dimension source — an ALPH with neither VP8X nor VP8 is
425 // not a shape RFC 9649 §2.5/§2.7 describes for still images.
426 return Err(Error::Alph(alph::AlphError::EmptyPayload));
427 };
428
429 let plane = alph::decode_alpha(alph_chunk.payload(bytes), width, height)?;
430 Ok(Some(plane))
431}
432
433/// Decode the §2.7.1.1 `ANIM` chunk payload to a typed
434/// [`anim::AnimHeader`].
435///
436/// The argument is the 6-byte chunk payload — the BGRA background
437/// colour followed by the little-endian u16 loop count.
438pub fn parse_anim_header(payload: &[u8]) -> Result<anim::AnimHeader, Error> {
439 anim::AnimHeader::parse(payload).map_err(Into::into)
440}
441
442/// Decode the §2.7.1.1 `ANMF` per-frame header to a typed
443/// [`anmf::AnmfHeader`].
444///
445/// The argument is the **payload** of an `ANMF` chunk — the slice
446/// returned by [`container::WebpChunk::payload`] for a chunk whose
447/// FourCC is [`container::fourcc::ANMF`]. Only the first 16 bytes
448/// are consumed; the remainder is the per-frame `Frame Data`
449/// sub-RIFF, which is not decoded here.
450pub fn parse_anmf_header(payload: &[u8]) -> Result<anmf::AnmfHeader, Error> {
451 anmf::AnmfHeader::parse(payload).map_err(Into::into)
452}
453
454/// Assemble a `RIFF/WEBP` file around a single bitstream payload per
455/// RFC 9649 §2.4 + §2.5 / §2.6 / §2.7. Convenience wrapper over
456/// [`build::build_webp_file`] returning the crate-wide [`Error`].
457pub fn build_webp_file(
458 payload: &[u8],
459 image_kind: build::ImageKind,
460 canvas_width: u32,
461 canvas_height: u32,
462) -> Result<Vec<u8>, Error> {
463 build::build_webp_file(payload, image_kind, canvas_width, canvas_height).map_err(Into::into)
464}
465
466/// Build the 10-byte §2.7.1 `VP8X` chunk payload (flags + reserved +
467/// canvas dims). Convenience wrapper over [`build::build_vp8x_chunk`]
468/// returning the crate-wide [`Error`].
469pub fn build_vp8x_chunk(
470 canvas_width: u32,
471 canvas_height: u32,
472 flags: build::Vp8xFlags,
473) -> Result<Vec<u8>, Error> {
474 build::build_vp8x_chunk(canvas_width, canvas_height, flags).map_err(Into::into)
475}
476
477/// Walk a `RIFF/WEBP` buffer and, if it carries a §2.5 simple-lossy
478/// `VP8 ` chunk (or a §2.7 extended-lossy file with a `VP8 ` chunk
479/// alongside `VP8X`), return a typed [`vp8_chunk::WebpLossyChunk`]
480/// handle whose [`bitstream`](vp8_chunk::WebpLossyChunk::bitstream)
481/// slice can be routed to an external VP8 decoder.
482///
483/// Returns `Ok(None)` if the file is well-formed but carries no
484/// `VP8 ` chunk (e.g. a `VP8L`-only simple-lossless file).
485///
486/// The returned handle borrows out of `bytes`, so the slice must
487/// outlive the handle.
488///
489/// This is the round-6 routing API — `oxideav-webp` deliberately
490/// does **not** take a runtime dependency on `oxideav-vp8`; the
491/// caller picks which VP8 decoder consumes the borrowed payload.
492pub fn extract_lossy_chunk(bytes: &[u8]) -> Result<Option<vp8_chunk::WebpLossyChunk<'_>>, Error> {
493 let c = container::parse(bytes)?;
494 vp8_chunk::extract_lossy(bytes, &c).map_err(Into::into)
495}
496
497/// Walk a `RIFF/WEBP` buffer and, if it carries a §2.6 simple-lossless
498/// `VP8L` chunk (or a §2.7 extended-lossless file with a `VP8L` chunk
499/// alongside `VP8X`), return a typed [`vp8l_chunk::WebpLosslessChunk`]
500/// handle whose [`bitstream`](vp8l_chunk::WebpLosslessChunk::bitstream)
501/// slice can be routed to an external VP8L decoder.
502///
503/// Returns `Ok(None)` if the file is well-formed but carries no
504/// `VP8L` chunk (e.g. a `VP8 `-only simple-lossy file).
505///
506/// The returned handle borrows out of `bytes`, so the slice must
507/// outlive the handle.
508///
509/// This is the round-7 routing API — `oxideav-webp` deliberately
510/// does **not** take a runtime dependency on a VP8L decoder; the
511/// caller picks which lossless-WebP decoder consumes the borrowed
512/// payload.
513pub fn extract_lossless_chunk(
514 bytes: &[u8],
515) -> Result<Option<vp8l_chunk::WebpLosslessChunk<'_>>, Error> {
516 let c = container::parse(bytes)?;
517 vp8l_chunk::extract_lossless(bytes, &c).map_err(Into::into)
518}
519
520/// Walk a `RIFF/WEBP` buffer, extract its §2.6 / §3.4 `VP8L` chunk,
521/// and read the §4 transform-presence list that follows the 5-byte
522/// VP8L image-header.
523///
524/// Returns `Ok(None)` if the file carries no `VP8L` chunk. Otherwise
525/// returns the parsed [`vp8l_stream::TransformList`] — the transforms
526/// in read order plus the bit position where the §5 entropy-coded
527/// image data (or the first transform's §5 body) begins.
528///
529/// This is the round-99 surface: it reads each transform's leading
530/// fixed-size fields (predictor / color `size_bits`, color-indexing
531/// `color_table_size`) but does **not** decode the §5 entropy-coded
532/// transform bodies or image data — those are returned-to boundaries
533/// for the next layer.
534pub fn read_vp8l_transform_list(bytes: &[u8]) -> Result<Option<vp8l_stream::TransformList>, Error> {
535 let c = container::parse(bytes)?;
536 let chunk = match vp8l_chunk::extract_lossless(bytes, &c)? {
537 Some(chunk) => chunk,
538 None => return Ok(None),
539 };
540 let mut reader = vp8l_stream::BitReader::new_after_image_header(chunk.bitstream());
541 let list = vp8l_stream::TransformList::read(&mut reader)?;
542 Ok(Some(list))
543}
544
545/// Walk a `RIFF/WEBP` buffer, extract its §2.6 / §3.4 `VP8L` chunk, and
546/// fully decode it to ARGB pixels.
547///
548/// This runs the round-108 §5/§6 entropy decode of the main ARGB image
549/// then applies the round-109 §4 inverse-transform chain
550/// ([`vp8l_transform::decode_lossless`]): predictor, color, subtract-green,
551/// and color-indexing, applied in reverse of the order the transforms
552/// were read.
553///
554/// Returns `Ok(None)` if the file carries no `VP8L` chunk. Otherwise the
555/// returned [`vp8l_decode::DecodedImage`] holds `width * height` ARGB
556/// pixels in scan-line order, each `(alpha << 24) | (red << 16) |
557/// (green << 8) | blue`.
558pub fn decode_lossless_image(bytes: &[u8]) -> Result<Option<vp8l_decode::DecodedImage>, Error> {
559 let c = container::parse(bytes)?;
560 let chunk = match vp8l_chunk::extract_lossless(bytes, &c)? {
561 Some(chunk) => chunk,
562 None => return Ok(None),
563 };
564 let width = chunk.width();
565 let height = chunk.height();
566 let image = vp8l_transform::decode_lossless(chunk.bitstream(), width, height)?;
567 Ok(Some(image))
568}
569
570/// §3.4 still-image dimension ceiling (16384 = `1 << 14`), the per-side
571/// maximum a §2.6 `VP8L` image header can encode (the 14-bit
572/// `width - 1` / `height - 1` fields plus one). Used as the eager-
573/// allocation bound on the §2.7.1.1 animation canvas: a `VP8X` canvas
574/// dimension above this can never be fully covered by a spec-valid
575/// `ANMF` sub-frame (each sub-frame is itself a `VP8L` image), so it is
576/// rejected before the full-canvas buffer is allocated rather than
577/// trusting the much larger §2.7.1 24-bit-per-side / 2^32-1-product cap.
578const MAX_DECODE_DIMENSION: u32 = 1 << 14;
579
580/// A fully decoded still WebP image: 8-bit RGBA pixels plus dimensions.
581///
582/// `rgba` is `width * height * 4` bytes in scan-line (top-to-bottom,
583/// left-to-right) order, each pixel laid out `[R, G, B, A]`. This is the
584/// canonical interleaved-RGBA surface
585/// (`oxideav_core::PixelFormat::Rgba`) the workspace's image crates
586/// emit, so a `VideoFrame` wrapper is a single 1-plane copy away.
587#[derive(Debug, Clone, PartialEq, Eq)]
588pub struct DecodedWebp {
589 /// Image width in pixels (the §2.7.1 `VP8X` canvas width, or the
590 /// §3.4 `VP8L` image width for a simple-lossless file).
591 pub width: u32,
592 /// Image height in pixels.
593 pub height: u32,
594 /// `width * height * 4` interleaved `[R, G, B, A]` bytes, scan order.
595 pub rgba: Vec<u8>,
596}
597
598/// Decode a still WebP file to a typed [`DecodedWebp`] (RGBA + dims).
599///
600/// Handles the two cases this crate can fully decode today:
601///
602/// 1. **Simple lossless** — a §2.6 `VP8L` chunk (optionally fronted by a
603/// §2.7.1 `VP8X` header): decoded to ARGB via
604/// [`vp8l_transform::decode_lossless`], with alpha carried inside the
605/// `VP8L` bitstream itself.
606/// 2. **Extended lossless** — a §2.7 `VP8X` file whose image data is a
607/// `VP8L` chunk. If the (spec-discouraged, per RFC 9649 §2.7.1.2) case
608/// of an accompanying §2.7.1.2 `ALPH` chunk is present, its decoded
609/// alpha plane overrides the per-pixel alpha channel.
610///
611/// As of round 124 a §2.5 `VP8 ` lossy bitstream is also decoded here —
612/// the `VP8 ` chunk payload is routed to the `oxideav-vp8` sibling crate
613/// ([`vp8_decode::decode_lossy_rgba`]) and a §2.7.1.2 `ALPH` chunk, when
614/// present, overrides the opaque alpha channel. (The standalone
615/// [`extract_lossy_chunk`] routing API remains available for callers that
616/// want the raw VP8 bitstream slice.)
617///
618/// Animations and header-only files (no `VP8L`/`VP8 ` chunk) return
619/// [`Error::Unsupported`]`(`[`UnsupportedKind::NoImageData`]`)`.
620pub fn decode_webp_image(bytes: &[u8]) -> Result<DecodedWebp, Error> {
621 let c = container::parse(bytes)?;
622
623 // §2.6 / §3.4: the VP8L lossless image. If absent, fall back to the
624 // §2.5 `VP8 ` lossy path (decoded via the `oxideav-vp8` sibling crate),
625 // and only then to "no image data".
626 let vp8l = vp8l_chunk::extract_lossless(bytes, &c)?;
627 let Some(chunk) = vp8l else {
628 // No VP8L. A §2.5 `VP8 ` lossy chunk is decoded through
629 // `oxideav-vp8` (round 124); anything else has no still-image
630 // pixel data.
631 if let Some(vp8) = c.first_chunk_with_fourcc(container::fourcc::VP8) {
632 return decode_lossy_image(bytes, &c, vp8);
633 }
634 return Err(Error::Unsupported(UnsupportedKind::NoImageData));
635 };
636
637 let width = chunk.width();
638 let height = chunk.height();
639 let mut image = vp8l_transform::decode_lossless(chunk.bitstream(), width, height)?;
640
641 // §2.7.1.2: an ALPH chunk alongside a VP8L image is discouraged by
642 // the spec ("A frame containing a 'VP8L' Chunk SHOULD NOT contain
643 // this chunk"), but is not forbidden. When present, its decoded alpha
644 // plane overrides the VP8L per-pixel alpha. The plane dimensions come
645 // from the VP8X canvas, which for a well-formed file equals the VP8L
646 // image dimensions.
647 if let Some(alph) = c.first_chunk_with_fourcc(container::fourcc::ALPH) {
648 let plane = alph::decode_alpha(alph.payload(bytes), width, height)?;
649 let pixels = image.pixels_mut();
650 if plane.len() == pixels.len() {
651 for (px, &a) in pixels.iter_mut().zip(plane.iter()) {
652 *px = (*px & 0x00ff_ffff) | (u32::from(a) << 24);
653 }
654 }
655 }
656
657 Ok(DecodedWebp {
658 width,
659 height,
660 rgba: argb_to_rgba(image.pixels()),
661 })
662}
663
664/// Decode a §2.5 `VP8 ` lossy chunk (simple-lossy or `VP8X`-extended
665/// lossy) to a [`DecodedWebp`].
666///
667/// The `VP8 ` payload is routed to the `oxideav-vp8` sibling crate's
668/// [`oxideav_vp8::decode_vp8`] entry point (round 124) via
669/// [`vp8_decode::decode_lossy_rgba`], which reconstructs the I420
670/// key-frame and converts it to interleaved RGBA. A §2.7.1.2 `ALPH`
671/// chunk alongside the `VP8 ` image (the §2.7 extended-lossy + alpha
672/// shape, e.g. `lossy-with-alpha-128x128.webp`) overrides the
673/// opaque-filled alpha channel with the decoded alpha plane.
674fn decode_lossy_image(
675 bytes: &[u8],
676 c: &container::WebpContainer,
677 vp8: &container::WebpChunk,
678) -> Result<DecodedWebp, Error> {
679 let (width, height, mut rgba) = vp8_decode::decode_lossy_rgba(vp8.payload(bytes))?;
680
681 // §2.7.1.2: an ALPH chunk alongside a VP8 lossy image carries the
682 // alpha plane (the VP8 bitstream itself is opaque YUV). Override the
683 // opaque-filled alpha with the decoded plane when the dimensions match.
684 if let Some(alph) = c.first_chunk_with_fourcc(container::fourcc::ALPH) {
685 let plane = alph::decode_alpha(alph.payload(bytes), width, height)?;
686 if plane.len() == (width as usize) * (height as usize) {
687 for (px, &a) in rgba.chunks_exact_mut(4).zip(plane.iter()) {
688 px[3] = a;
689 }
690 }
691 }
692
693 Ok(DecodedWebp {
694 width,
695 height,
696 rgba,
697 })
698}
699
700// ─────────────────────── Published-shape decode API ───────────────────────
701//
702// The free `decode_webp` path the published crates.io releases exposed —
703// the flat, `image`-crate-compatible RGBA surface downstream consumers
704// depend on. The `WebpImage` / `WebpFrame` /
705// `WebpFileMetadata` / `WebpError` shapes here are the published shapes;
706// the round-115 `DecodedWebp` / `decode_webp_image` / `decode_lossless_image`
707// helpers above are the rebuild's own low-level surface and stay as
708// additional API.
709
710/// A fully decoded WebP file: one frame for a still image, N frames for an
711/// animation, plus the file-level metadata and animation parameters.
712///
713/// This is the published-API decode result. The single most important
714/// consumer property is the flat-buffer shape of each [`WebpFrame::rgba`]:
715/// `width * height * 4` tightly packed `[R, G, B, A]` bytes, no per-row
716/// stride padding, so it drops straight into
717/// `image::ImageBuffer::from_raw(width, height, rgba)`.
718#[derive(Debug, Clone, PartialEq, Eq)]
719pub struct WebpImage {
720 /// Canvas width in pixels (the §2.7.1 `VP8X` canvas width for an
721 /// extended file, or the §3.4 / RFC 6386 §9.1 image-header width
722 /// for a simple-lossless / simple-lossy file). Matches the first
723 /// frame's width for a single-frame image.
724 pub width: u32,
725 /// Canvas height in pixels — see [`Self::width`] for the spec
726 /// citation.
727 pub height: u32,
728 /// Decoded frames. A still image yields exactly one frame; an
729 /// animation yields one per `ANMF` chunk (animation decode is not
730 /// rebuilt yet — see [`decode_webp`]).
731 pub frames: Vec<WebpFrame>,
732 /// File-level metadata (ICC / Exif / XMP), each `None` when absent.
733 pub metadata: WebpFileMetadata,
734 /// The §2.7.1.1 `ANIM` background colour as `[R, G, B, A]`, or `None`
735 /// for a non-animated file.
736 pub anim_background_rgba: Option<[u8; 4]>,
737 /// The §2.7.1.1 `ANIM` loop count (`0` = loop forever), or `None` for
738 /// a non-animated file.
739 pub anim_loop_count: Option<u16>,
740}
741
742/// A single decoded WebP frame: a flat RGBA pixel buffer plus its size
743/// and (for animations) its display duration.
744#[derive(Debug, Clone, PartialEq, Eq)]
745pub struct WebpFrame {
746 /// `width * height * 4` interleaved `[R, G, B, A]` bytes in scan-line
747 /// (top-to-bottom, left-to-right) order, no stride padding.
748 pub rgba: Vec<u8>,
749 /// Frame width in pixels.
750 pub width: u32,
751 /// Frame height in pixels.
752 pub height: u32,
753 /// Per-frame display duration in milliseconds (the §2.7.1.1 `ANMF`
754 /// frame delay). `0` for a still image.
755 pub duration_ms: u32,
756}
757
758/// File-level metadata chunks, each carrying the raw chunk payload bytes
759/// when present.
760#[derive(Debug, Clone, Default, PartialEq, Eq)]
761pub struct WebpFileMetadata {
762 /// §2.7.1.4 `ICCP` ICC color-profile payload, if present.
763 pub icc: Option<Vec<u8>>,
764 /// §2.7.1.5 `EXIF` Exif payload, if present.
765 pub exif: Option<Vec<u8>>,
766 /// §2.7.1.5 `XMP ` XMP payload, if present.
767 pub xmp: Option<Vec<u8>>,
768}
769
770/// Borrowed file-level metadata for the encode side — the §2.7.1.4 `ICCP`,
771/// §2.7.1.5 `EXIF`, and §2.7.1.5 `XMP ` payloads to embed, each `None` to
772/// omit the corresponding chunk.
773///
774/// This is the borrowed form: the slices are not copied until the encoder
775/// frames them. The owned counterpart is [`WebpMetadataOwned`]. The default
776/// is all-`None` — embed no metadata — so a `VP8L` encode with
777/// `WebpMetadata::default()` emits the simple (non-`VP8X`) layout.
778#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
779pub struct WebpMetadata<'a> {
780 /// §2.7.1.4 `ICCP` ICC color-profile payload to embed, if any.
781 pub icc: Option<&'a [u8]>,
782 /// §2.7.1.5 `EXIF` Exif payload to embed, if any.
783 pub exif: Option<&'a [u8]>,
784 /// §2.7.1.5 `XMP ` XMP payload to embed, if any.
785 pub xmp: Option<&'a [u8]>,
786}
787
788impl<'a> WebpMetadata<'a> {
789 /// True if every field is `None` — encoding can stay on the simple
790 /// (non-`VP8X`) layout when no alpha is present either.
791 pub fn is_empty(&self) -> bool {
792 self.icc.is_none() && self.exif.is_none() && self.xmp.is_none()
793 }
794}
795
796/// Owned file-level metadata — the registry-side counterpart of the borrowed
797/// [`WebpMetadata`].
798///
799/// Carries owned `Vec<u8>` payloads so it can be stored on an encoder /
800/// codec-parameters struct without borrowing the caller's buffers. Convert
801/// to the borrowed form for an encode call with [`Self::as_borrowed`].
802#[derive(Debug, Clone, Default, PartialEq, Eq)]
803pub struct WebpMetadataOwned {
804 /// §2.7.1.4 `ICCP` ICC color-profile payload to embed, if any.
805 pub icc: Option<Vec<u8>>,
806 /// §2.7.1.5 `EXIF` Exif payload to embed, if any.
807 pub exif: Option<Vec<u8>>,
808 /// §2.7.1.5 `XMP ` XMP payload to embed, if any.
809 pub xmp: Option<Vec<u8>>,
810}
811
812impl WebpMetadataOwned {
813 /// Borrow this owned metadata as a [`WebpMetadata`] for an encode call.
814 pub fn as_borrowed(&self) -> WebpMetadata<'_> {
815 WebpMetadata {
816 icc: self.icc.as_deref(),
817 exif: self.exif.as_deref(),
818 xmp: self.xmp.as_deref(),
819 }
820 }
821
822 /// True if every field is `None`.
823 pub fn is_empty(&self) -> bool {
824 self.icc.is_none() && self.exif.is_none() && self.xmp.is_none()
825 }
826}
827
828impl From<WebpMetadataOwned> for WebpFileMetadata {
829 fn from(m: WebpMetadataOwned) -> Self {
830 WebpFileMetadata {
831 icc: m.icc,
832 exif: m.exif,
833 xmp: m.xmp,
834 }
835 }
836}
837
838/// The published-API error type for the flat [`decode_webp`] /
839/// [`extract_metadata`] decode paths.
840///
841/// This is intentionally coarse-grained — the stable shape downstream
842/// consumers match on. The internal [`Error`] enum (with its per-module
843/// variants) remains the richer surface for the low-level
844/// [`decode_webp_image`] / [`decode_lossless_image`] helpers; it maps into
845/// `WebpError` via the `From<Error>` impl.
846#[derive(Debug, Clone, PartialEq, Eq)]
847pub enum WebpError {
848 /// The input is not a well-formed WebP file (bad magic, malformed
849 /// chunk structure, a sub-decoder rejected the bitstream, …).
850 InvalidData,
851 /// The file is well-formed but carries an image kind this build does
852 /// not decode yet — currently the §2.5 `VP8 ` lossy bitstream and
853 /// animation frame assembly.
854 Unsupported,
855 /// The input ended before a complete image could be read.
856 Eof,
857 /// More input is required to complete the decode (streaming callers).
858 NeedMore,
859}
860
861impl core::fmt::Display for WebpError {
862 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
863 let s = match self {
864 Self::InvalidData => "oxideav-webp: invalid WebP data",
865 Self::Unsupported => "oxideav-webp: unsupported WebP feature",
866 Self::Eof => "oxideav-webp: unexpected end of input",
867 Self::NeedMore => "oxideav-webp: more input required",
868 };
869 f.write_str(s)
870 }
871}
872
873impl std::error::Error for WebpError {}
874
875impl WebpError {
876 /// Build an `InvalidData` variant from any string-like message.
877 ///
878 /// The 0.1.2 published `WebpError::InvalidData` carried a `String`
879 /// payload; the current rebuild collapses every malformed-bitstream
880 /// failure to the unit variant so the historical *constructor* shape
881 /// `WebpError::invalid("message")` keeps compiling. The message
882 /// itself is discarded; callers that need the underlying diagnostic
883 /// can match on the richer in-crate [`Error`] instead.
884 pub fn invalid<S: Into<String>>(_msg: S) -> Self {
885 // The message is intentionally dropped — see the doc comment for
886 // why the unit-variant rebuild surfaces the constructor only.
887 Self::InvalidData
888 }
889
890 /// Build an `Unsupported` variant from any string-like message — the
891 /// constructor counterpart of [`Self::invalid`].
892 pub fn unsupported<S: Into<String>>(_msg: S) -> Self {
893 Self::Unsupported
894 }
895}
896
897/// Map the rich internal [`Error`] onto the coarse published [`WebpError`].
898///
899/// `Unsupported` / `NotImplemented` collapse to [`WebpError::Unsupported`];
900/// every other variant (a malformed container or a sub-decoder rejecting
901/// the bitstream) is [`WebpError::InvalidData`].
902impl From<Error> for WebpError {
903 fn from(e: Error) -> Self {
904 match e {
905 Error::Unsupported(_) | Error::NotImplemented => WebpError::Unsupported,
906 _ => WebpError::InvalidData,
907 }
908 }
909}
910
911/// Map an `oxideav-vp8` decode failure onto the coarse published
912/// [`WebpError`].
913///
914/// The `oxideav-vp8` decoder refuses an inter-frame
915/// ([`oxideav_vp8::DecodeError::Unsupported`]), which collapses to
916/// [`WebpError::Unsupported`]. Every other decode failure — a malformed
917/// frame header, truncated partition, bad token stream — is a bitstream
918/// problem and maps to [`WebpError::InvalidData`].
919///
920/// Note: the published surface specifies a `From<oxideav_vp8::Vp8Error>` adapter
921/// (the umbrella type), but `Vp8Error` is not yet published on crates.io
922/// (it landed on vp8 master after the v0.2.0 tag). This `DecodeError`
923/// adapter covers the live decode path against the published 0.2.0 API;
924/// the `Vp8Error` adapter is a follow-up once vp8 publishes it.
925impl From<oxideav_vp8::DecodeError> for WebpError {
926 fn from(e: oxideav_vp8::DecodeError) -> Self {
927 match e {
928 oxideav_vp8::DecodeError::Unsupported(_) => WebpError::Unsupported,
929 _ => WebpError::InvalidData,
930 }
931 }
932}
933
934/// Map the `oxideav-vp8` umbrella [`oxideav_vp8::Vp8Error`] onto the
935/// coarse published [`WebpError`].
936///
937/// The four variants share names with [`WebpError`] so the mapping is a
938/// straight 1-to-1 collapse — the `String` payloads on
939/// `InvalidData` / `Unsupported` are dropped (the unit-variant rebuild
940/// surfaces the variant only). Wired up against `oxideav-vp8 0.2.1`
941/// (the release that first exports `Vp8Error` at the crate root).
942///
943/// The
944/// compile-time signature assertion lives in
945/// `tests/api_compat_0_1_2.rs::crate_root_webp_error_from_vp8_error`.
946impl From<oxideav_vp8::Vp8Error> for WebpError {
947 fn from(e: oxideav_vp8::Vp8Error) -> Self {
948 match e {
949 oxideav_vp8::Vp8Error::InvalidData(_) => WebpError::InvalidData,
950 oxideav_vp8::Vp8Error::Unsupported(_) => WebpError::Unsupported,
951 oxideav_vp8::Vp8Error::Eof => WebpError::Eof,
952 oxideav_vp8::Vp8Error::NeedMore => WebpError::NeedMore,
953 }
954 }
955}
956
957/// Decode a WebP file to the published flat-RGBA [`WebpImage`] shape.
958///
959/// This is the `image`-crate-compatible entry point: every returned
960/// [`WebpFrame::rgba`] is `width * height * 4` tightly packed `[R, G, B, A]`
961/// bytes (no stride padding), so a frame wraps zero-copy as
962/// `image::ImageBuffer::from_raw(frame.width, frame.height, frame.rgba)`.
963///
964/// Supported today (built on the rebuilt §4–§6 VP8L decoder + the
965/// `oxideav-vp8` lossy decoder):
966///
967/// * **Simple / extended lossless** (`VP8L`, optionally `VP8X`-fronted,
968/// with optional `ALPH`-over-`VP8L` alpha) → a single-frame `WebpImage`.
969/// * **Simple / extended lossy** (`VP8 `, optionally `VP8X`-fronted, with
970/// optional `ALPH`-over-`VP8 ` alpha) → a single-frame `WebpImage`,
971/// decoded through `oxideav-vp8` (round 124).
972///
973/// Not yet rebuilt (returns [`WebpError::Unsupported`], never a fake
974/// decode):
975///
976/// * **Animation** — `ANMF` frame assembly with `VP8 ` lossy sub-chunks
977/// (lossless `ANMF` frames decode).
978///
979/// The low-level [`decode_webp_image`] / [`decode_lossless_image`] helpers
980/// expose the rebuild's internal [`DecodedWebp`] / [`vp8l_decode::DecodedImage`]
981/// surfaces for callers that want them.
982pub fn decode_webp(bytes: &[u8]) -> Result<WebpImage, WebpError> {
983 // Parse the container once so we can read both pixels and metadata.
984 let c = container::parse(bytes).map_err(|_| WebpError::InvalidData)?;
985
986 // Animated file: an ANIM chunk introduces a sequence of ANMF frames.
987 // Decode every frame's VP8L-lossless bitstream into a separate WebpFrame.
988 if c.first_chunk_with_fourcc(container::fourcc::ANIM).is_some() {
989 return decode_animation(bytes, &c);
990 }
991
992 // Pixel data: the rebuilt path decodes VP8L (simple or extended).
993 // VP8 lossy and animation are reported Unsupported, not faked.
994 let decoded = decode_webp_image(bytes).map_err(WebpError::from)?;
995
996 let frame = WebpFrame {
997 rgba: decoded.rgba,
998 width: decoded.width,
999 height: decoded.height,
1000 duration_ms: 0,
1001 };
1002
1003 let metadata = metadata_from_container(bytes, &c);
1004
1005 // Non-animated file: ANIM fields are None. (Animation assembly is not
1006 // rebuilt yet, so any animated file already errored Unsupported above
1007 // via the NoImageData path.)
1008 Ok(WebpImage {
1009 width: frame.width,
1010 height: frame.height,
1011 frames: vec![frame],
1012 metadata,
1013 anim_background_rgba: None,
1014 anim_loop_count: None,
1015 })
1016}
1017
1018/// Decode an animated WebP (`ANIM` + per-frame `ANMF`) to a multi-frame
1019/// [`WebpImage`], compositing each frame's sub-rectangle onto a shared
1020/// canvas per RFC 9649 §2.7.1.1.
1021///
1022/// Each §2.7.1.1 `ANMF` chunk carries a 16-byte header
1023/// ([`anmf::AnmfHeader`]) followed by its "Frame Data" — a padded §2.3
1024/// sub-RIFF holding the frame's bitstream. This decoder handles the
1025/// §2.6 `VP8L` lossless sub-chunk (the path the in-crate animation encoder
1026/// produces); an `ANMF` carrying only a §2.5 `VP8 ` lossy sub-chunk is
1027/// [`WebpError::Unsupported`] (the VP8 lossy decoder is not rebuilt yet).
1028///
1029/// **Canvas compositing (round 127):** the canvas is sized from the
1030/// §2.7.1 `VP8X` chunk and initialised to the §2.7.1.1 `ANIM`
1031/// `Background Color`. Each frame's pixels are then placed at its
1032/// `(Frame X, Frame Y)` offset with the §2.7.1.1 disposal/blending
1033/// rules:
1034///
1035/// * Before drawing a frame, the **previous** frame's disposal method
1036/// is applied (only to that previous frame's sub-rectangle). `None`
1037/// leaves the canvas as is; `Background` fills the previous rect with
1038/// the `ANIM` background colour.
1039/// * The current frame is then drawn into its rect using its blending
1040/// method: `Overwrite` replaces the rect's pixels byte-for-byte;
1041/// `AlphaBlend` composites RGBA over the existing canvas using the
1042/// §2.7.1.1 "Alpha-blending" formula
1043/// `blend.A = src.A + dst.A * (1 - src.A / 255)` (8-bit integer
1044/// approximation, no gamma linearisation).
1045///
1046/// The per-frame `Frame Duration` populates each
1047/// [`WebpFrame::duration_ms`]; `width` / `height` on each returned
1048/// frame are the **canvas** dimensions (every frame is a full-canvas
1049/// snapshot after rendering — what an animation player would display).
1050/// The §2.7.1.1 `ANIM` background colour and loop count populate
1051/// [`WebpImage::anim_background_rgba`] / [`WebpImage::anim_loop_count`].
1052fn decode_animation(bytes: &[u8], c: &container::WebpContainer) -> Result<WebpImage, WebpError> {
1053 // §2.7.1.1 ANIM: global background colour + loop count.
1054 let anim_chunk = c
1055 .first_chunk_with_fourcc(container::fourcc::ANIM)
1056 .ok_or(WebpError::InvalidData)?;
1057 let anim =
1058 anim::AnimHeader::parse(anim_chunk.payload(bytes)).map_err(|_| WebpError::InvalidData)?;
1059 let bg = anim.background_color;
1060
1061 // §2.7.1 VP8X canvas dimensions — the canvas every ANMF composites onto.
1062 let vp8x_chunk = c
1063 .first_chunk_with_fourcc(container::fourcc::VP8X)
1064 .ok_or(WebpError::InvalidData)?;
1065 let vp8x =
1066 vp8x::Vp8xHeader::parse(vp8x_chunk.payload(bytes)).map_err(|_| WebpError::InvalidData)?;
1067 let canvas_w = vp8x.canvas_width;
1068 let canvas_h = vp8x.canvas_height;
1069
1070 // §2.7.1 permits a `VP8X` canvas up to 2^24 per side (product capped at
1071 // 2^32 - 1). That spec cap is far larger than is safe to pre-allocate:
1072 // a ~480-byte file declaring a 16 777 154 × 64 canvas demands a ~4 GiB
1073 // buffer below before a single frame is decoded. Every §2.7.1.1 `ANMF`
1074 // sub-frame is itself a §2.6 `VP8L` image, whose §3.4 dimensions are
1075 // capped at 16384 per side — so a canvas exceeding that in either
1076 // dimension can never be fully covered by a spec-valid frame and is
1077 // rejected here rather than eagerly allocated. This bounds the canvas
1078 // buffer at the §3.4 still-image ceiling (16384 × 16384 × 4 = 1 GiB).
1079 if canvas_w > MAX_DECODE_DIMENSION || canvas_h > MAX_DECODE_DIMENSION {
1080 return Err(WebpError::InvalidData);
1081 }
1082
1083 // Initialise canvas to the ANIM background colour (RGBA, scan order).
1084 let bg_rgba = [bg.red, bg.green, bg.blue, bg.alpha];
1085 let canvas_bytes = canvas_w
1086 .checked_mul(canvas_h)
1087 .and_then(|n| n.checked_mul(4))
1088 .ok_or(WebpError::InvalidData)? as usize;
1089 let mut canvas: Vec<u8> = Vec::with_capacity(canvas_bytes);
1090 for _ in 0..(canvas_bytes / 4) {
1091 canvas.extend_from_slice(&bg_rgba);
1092 }
1093
1094 // Track the previous frame's rect + dispose for the §2.7.1.1
1095 // "Before rendering each frame, the previous frame's Disposal method
1096 // is applied" rule.
1097 let mut prev_rect: Option<(u32, u32, u32, u32, anmf::DisposalMethod)> = None;
1098
1099 let mut frames = Vec::new();
1100 for anmf_chunk in c.chunks_with_fourcc(container::fourcc::ANMF) {
1101 let payload = anmf_chunk.payload(bytes);
1102 let header = anmf::AnmfHeader::parse(payload).map_err(|_| WebpError::InvalidData)?;
1103 let frame_data = &payload[header.frame_data_offset()..];
1104
1105 // The Frame Data sub-RIFF is a flat list of §2.3 padded chunks. Find
1106 // the VP8L bitstream sub-chunk (lossy VP8 is not decoded here).
1107 let vp8l = find_subchunk(frame_data, container::fourcc::VP8L);
1108 let Some(vp8l_payload) = vp8l else {
1109 // A VP8 lossy sub-chunk is recognized-but-unsupported.
1110 if find_subchunk(frame_data, container::fourcc::VP8).is_some() {
1111 return Err(WebpError::Unsupported);
1112 }
1113 return Err(WebpError::InvalidData);
1114 };
1115
1116 let chunk = vp8l_chunk::WebpLosslessChunk::from_payload(vp8l_payload)
1117 .map_err(|e| WebpError::from(Error::from(e)))?;
1118 let sub_w = chunk.width();
1119 let sub_h = chunk.height();
1120 let image = vp8l_transform::decode_lossless(chunk.bitstream(), sub_w, sub_h)
1121 .map_err(|e| WebpError::from(Error::from(e)))?;
1122
1123 // An optional ALPH sub-chunk overrides the VP8L per-pixel alpha.
1124 let mut pixels = image;
1125 if let Some(alph_payload) = find_subchunk(frame_data, container::fourcc::ALPH) {
1126 if let Ok(plane) = alph::decode_alpha(alph_payload, sub_w, sub_h) {
1127 let px = pixels.pixels_mut();
1128 if plane.len() == px.len() {
1129 for (p, &a) in px.iter_mut().zip(plane.iter()) {
1130 *p = (*p & 0x00ff_ffff) | (u32::from(a) << 24);
1131 }
1132 }
1133 }
1134 }
1135 let sub_rgba = argb_to_rgba(pixels.pixels());
1136
1137 // §2.7.1.1: "Before rendering each frame, the previous frame's
1138 // Disposal method is applied" — clears the previous rect to bg
1139 // for dispose=Background; no-op for dispose=None.
1140 if let Some((px, py, pw, ph, anmf::DisposalMethod::Background)) = prev_rect {
1141 fill_canvas_rect(&mut canvas, canvas_w, px, py, pw, ph, bg_rgba);
1142 }
1143
1144 // §2.7.1.1: the frame must fit inside the canvas. Reject any
1145 // frame that overflows the canvas — that's a malformed file.
1146 let right = header.x.checked_add(sub_w).ok_or(WebpError::InvalidData)?;
1147 let bottom = header.y.checked_add(sub_h).ok_or(WebpError::InvalidData)?;
1148 if right > canvas_w || bottom > canvas_h {
1149 return Err(WebpError::InvalidData);
1150 }
1151
1152 // Draw the current frame into its rect using its blending method.
1153 match header.blend {
1154 anmf::BlendingMethod::Overwrite => {
1155 blit_rect_overwrite(
1156 &mut canvas,
1157 canvas_w,
1158 header.x,
1159 header.y,
1160 sub_w,
1161 sub_h,
1162 &sub_rgba,
1163 );
1164 }
1165 anmf::BlendingMethod::AlphaBlend => {
1166 blit_rect_alpha_blend(
1167 &mut canvas,
1168 canvas_w,
1169 header.x,
1170 header.y,
1171 sub_w,
1172 sub_h,
1173 &sub_rgba,
1174 );
1175 }
1176 }
1177
1178 // Snapshot the full canvas as this frame's display state.
1179 frames.push(WebpFrame {
1180 rgba: canvas.clone(),
1181 width: canvas_w,
1182 height: canvas_h,
1183 duration_ms: header.duration_ms,
1184 });
1185
1186 prev_rect = Some((header.x, header.y, sub_w, sub_h, header.dispose));
1187 }
1188
1189 if frames.is_empty() {
1190 return Err(WebpError::InvalidData);
1191 }
1192
1193 Ok(WebpImage {
1194 width: canvas_w,
1195 height: canvas_h,
1196 frames,
1197 metadata: metadata_from_container(bytes, c),
1198 anim_background_rgba: Some([bg.red, bg.green, bg.blue, bg.alpha]),
1199 anim_loop_count: Some(anim.loop_count),
1200 })
1201}
1202
1203/// Fill an axis-aligned rectangle of `canvas` with `rgba`. Bounds are
1204/// pre-validated by the caller (`x + w <= canvas_w`).
1205fn fill_canvas_rect(
1206 canvas: &mut [u8],
1207 canvas_w: u32,
1208 x: u32,
1209 y: u32,
1210 w: u32,
1211 h: u32,
1212 rgba: [u8; 4],
1213) {
1214 let canvas_w = canvas_w as usize;
1215 let cw_bytes = canvas_w * 4;
1216 let x = x as usize;
1217 let y = y as usize;
1218 let w = w as usize;
1219 let h = h as usize;
1220 for row in 0..h {
1221 let off = (y + row) * cw_bytes + x * 4;
1222 for col in 0..w {
1223 canvas[off + col * 4] = rgba[0];
1224 canvas[off + col * 4 + 1] = rgba[1];
1225 canvas[off + col * 4 + 2] = rgba[2];
1226 canvas[off + col * 4 + 3] = rgba[3];
1227 }
1228 }
1229}
1230
1231/// Copy `src` (flat `w*h*4` RGBA) into `canvas` at `(x, y)`, replacing the
1232/// destination pixels byte-for-byte (§2.7.1.1 blending method `1`).
1233fn blit_rect_overwrite(
1234 canvas: &mut [u8],
1235 canvas_w: u32,
1236 x: u32,
1237 y: u32,
1238 w: u32,
1239 h: u32,
1240 src: &[u8],
1241) {
1242 let canvas_w = canvas_w as usize;
1243 let cw_bytes = canvas_w * 4;
1244 let x = x as usize;
1245 let y = y as usize;
1246 let w = w as usize;
1247 let h = h as usize;
1248 let sw_bytes = w * 4;
1249 for row in 0..h {
1250 let src_off = row * sw_bytes;
1251 let dst_off = (y + row) * cw_bytes + x * 4;
1252 canvas[dst_off..dst_off + sw_bytes].copy_from_slice(&src[src_off..src_off + sw_bytes]);
1253 }
1254}
1255
1256/// Composite `src` over `canvas` at `(x, y)` per the §2.7.1.1
1257/// "Alpha-blending" formula (8-bit integer approximation, sRGB space,
1258/// no gamma linearisation — matching the spec's stated 8-bit formula).
1259fn blit_rect_alpha_blend(
1260 canvas: &mut [u8],
1261 canvas_w: u32,
1262 x: u32,
1263 y: u32,
1264 w: u32,
1265 h: u32,
1266 src: &[u8],
1267) {
1268 let canvas_w = canvas_w as usize;
1269 let cw_bytes = canvas_w * 4;
1270 let x = x as usize;
1271 let y = y as usize;
1272 let w = w as usize;
1273 let h = h as usize;
1274 for row in 0..h {
1275 for col in 0..w {
1276 let src_off = (row * w + col) * 4;
1277 let dst_off = (y + row) * cw_bytes + (x + col) * 4;
1278 let sr = src[src_off] as u32;
1279 let sg = src[src_off + 1] as u32;
1280 let sb = src[src_off + 2] as u32;
1281 let sa = src[src_off + 3] as u32;
1282 // Fast path: fully-opaque source → equivalent to overwrite
1283 // (matches "If the current frame does not have an alpha
1284 // channel, assume the alpha value is 255, effectively
1285 // replacing the rectangle").
1286 if sa == 255 {
1287 canvas[dst_off] = sr as u8;
1288 canvas[dst_off + 1] = sg as u8;
1289 canvas[dst_off + 2] = sb as u8;
1290 canvas[dst_off + 3] = 255;
1291 continue;
1292 }
1293 // Fully-transparent source → leave dst unchanged.
1294 if sa == 0 {
1295 continue;
1296 }
1297 let dr = canvas[dst_off] as u32;
1298 let dg = canvas[dst_off + 1] as u32;
1299 let db = canvas[dst_off + 2] as u32;
1300 let da = canvas[dst_off + 3] as u32;
1301 // §2.7.1.1: blend.A = src.A + dst.A * (1 - src.A / 255)
1302 // Done in 8-bit fixed point: dst_factor = dst.A * (255 - src.A) / 255
1303 let dst_factor = (da * (255 - sa) + 127) / 255;
1304 let out_a = sa + dst_factor;
1305 // blend.RGB = (src.RGB * src.A + dst.RGB * dst.A
1306 // * (1 - src.A / 255)) / blend.A
1307 // out_a == 0 path: both src and dst are fully transparent → RGB
1308 // is undefined and the spec sets blend.RGB := 0; checked_div
1309 // returns None, which we fold into a 0 RGB output.
1310 let out_r = (sr * sa + dr * dst_factor + out_a / 2)
1311 .checked_div(out_a)
1312 .unwrap_or(0);
1313 let out_g = (sg * sa + dg * dst_factor + out_a / 2)
1314 .checked_div(out_a)
1315 .unwrap_or(0);
1316 let out_b = (sb * sa + db * dst_factor + out_a / 2)
1317 .checked_div(out_a)
1318 .unwrap_or(0);
1319 canvas[dst_off] = out_r.min(255) as u8;
1320 canvas[dst_off + 1] = out_g.min(255) as u8;
1321 canvas[dst_off + 2] = out_b.min(255) as u8;
1322 canvas[dst_off + 3] = out_a.min(255) as u8;
1323 }
1324 }
1325}
1326
1327/// Walk a flat §2.3 sub-chunk list (the `ANMF` "Frame Data" sub-RIFF — no
1328/// outer `RIFF`/`WEBP` header) and return the payload of the first chunk with
1329/// `target` FourCC. Returns `None` on a truncated header or no match.
1330fn find_subchunk(mut data: &[u8], target: container::FourCc) -> Option<&[u8]> {
1331 while data.len() >= 8 {
1332 let fourcc: container::FourCc = data[0..4].try_into().ok()?;
1333 let size = u32::from_le_bytes(data[4..8].try_into().ok()?) as usize;
1334 let payload_start = 8usize;
1335 let payload_end = payload_start.checked_add(size)?;
1336 if payload_end > data.len() {
1337 return None;
1338 }
1339 if fourcc == target {
1340 return Some(&data[payload_start..payload_end]);
1341 }
1342 // §2.3: odd Size is followed by one pad byte not counted in Size.
1343 let advance = payload_end + (size & 1);
1344 if advance > data.len() {
1345 return None;
1346 }
1347 data = &data[advance..];
1348 }
1349 None
1350}
1351
1352/// Read the file-level metadata chunks (ICC / Exif / XMP) without
1353/// decoding any pixels.
1354///
1355/// Walks the container and lifts the raw payloads of the §2.7.1.4 `ICCP`,
1356/// §2.7.1.5 `EXIF`, and §2.7.1.5 `XMP ` chunks (each `None` when absent).
1357pub fn extract_metadata(bytes: &[u8]) -> Result<WebpFileMetadata, WebpError> {
1358 let c = container::parse(bytes).map_err(|_| WebpError::InvalidData)?;
1359 Ok(metadata_from_container(bytes, &c))
1360}
1361
1362/// Lift the ICC / Exif / XMP payloads out of an already-parsed container.
1363fn metadata_from_container(bytes: &[u8], c: &container::WebpContainer) -> WebpFileMetadata {
1364 let payload_of = |fourcc| {
1365 c.first_chunk_with_fourcc(fourcc)
1366 .map(|chunk| chunk.payload(bytes).to_vec())
1367 };
1368 WebpFileMetadata {
1369 icc: payload_of(container::fourcc::ICCP),
1370 exif: payload_of(container::fourcc::EXIF),
1371 xmp: payload_of(container::fourcc::XMP),
1372 }
1373}
1374
1375/// Encode an interleaved 8-bit RGBA image to a complete RIFF/WEBP file
1376/// carrying a §2.6 simple-lossless `VP8L` chunk.
1377///
1378/// `rgba` is `width * height * 4` bytes in scan-line (top-to-bottom,
1379/// left-to-right) order, each pixel `[R, G, B, A]` — exactly the
1380/// [`DecodedWebp::rgba`] layout [`decode_webp_image`] returns. The encoded
1381/// file decodes back to the same bytes through [`decode_webp`], a
1382/// pixel-exact round trip.
1383///
1384/// This is the round-115 encoder, extended in round 119 with §5.2.2 LZ77
1385/// backward-reference matching and in round 120 with the §3.5.3 / §3.8.2
1386/// subtract-green forward transform. The encoder evaluates the no-transform
1387/// and subtract-green paths per image and emits whichever is smaller; the
1388/// LZ77 matcher runs in both. Still pass-through: §3.8.2 predictor / color
1389/// / color-indexing transforms and §3.8.3 color cache. The §3.7.2 canonical
1390/// prefix codes are built per-image from the pixel frequencies. See
1391/// [`vp8l_encode::encode_webp_lossless`].
1392pub fn encode_webp_lossless(rgba: &[u8], width: u32, height: u32) -> Result<Vec<u8>, Error> {
1393 vp8l_encode::encode_webp_lossless(rgba, width, height).map_err(Into::into)
1394}
1395
1396// ─────────────────────── Published-shape VP8L encode API ───────────────────────
1397//
1398// The published-0.1.5 lossless-encode public names, mapped onto the
1399// round-115 in-crate VP8L encoder. `encode_vp8l_argb` / `_with` produce a
1400// **bare** VP8L bitstream (no RIFF wrapper); `encode_vp8l_argb_with_metadata`
1401// produces a complete `.webp`, auto-promoting to the §2.7 `VP8X` layout when
1402// alpha or any metadata field is set.
1403
1404/// Encode an ARGB image to a **bare** §2.6 / §3.4 `VP8L` bitstream — the
1405/// chunk payload (image-header + image stream), with **no** RIFF/WEBP
1406/// wrapper.
1407///
1408/// `argb` is `width * height` packed ARGB values in scan-line order, each
1409/// `(alpha << 24) | (red << 16) | (green << 8) | blue` — the same layout
1410/// [`vp8l_decode::DecodedImage::pixels`] produces. The §3.4 `alpha_is_used`
1411/// header bit is auto-detected (set iff any pixel's alpha is not `0xff`);
1412/// use [`encode_vp8l_argb_with`] to set it explicitly.
1413///
1414/// Wrapping the returned bytes in `build::build_webp_file(.., ImageKind::Lossless, ..)`
1415/// (or `build::build_chunk(fourcc::VP8L, ..)`) yields a complete `.webp` that
1416/// decodes back to the input pixels exactly via [`decode_webp`].
1417pub fn encode_vp8l_argb(argb: &[u32], width: u32, height: u32) -> Result<Vec<u8>, WebpError> {
1418 vp8l_encode::encode_vp8l_argb(argb, width, height)
1419 .map_err(Error::from)
1420 .map_err(WebpError::from)
1421}
1422
1423/// Encode an ARGB image to a bare §2.6 / §3.4 `VP8L` bitstream with the
1424/// §3.4 `alpha_is_used` header bit set **explicitly** by the caller.
1425///
1426/// The fixed (non-RDO) form of [`encode_vp8l_argb`]: `has_alpha` becomes the
1427/// header bit verbatim instead of being scanned from the pixels. The alpha
1428/// values are carried in the §3.7.3 ARGB literals regardless of the bit, so
1429/// the round trip is exact either way.
1430pub fn encode_vp8l_argb_with(
1431 argb: &[u32],
1432 width: u32,
1433 height: u32,
1434 has_alpha: bool,
1435) -> Result<Vec<u8>, WebpError> {
1436 vp8l_encode::encode_vp8l_argb_with(argb, width, height, has_alpha)
1437 .map_err(Error::from)
1438 .map_err(WebpError::from)
1439}
1440
1441/// Encode an ARGB image to a complete `.webp` file carrying a §2.6 `VP8L`
1442/// lossless bitstream, embedding any supplied file-level metadata.
1443///
1444/// `argb` is `width * height` packed ARGB values in scan-line order. The
1445/// output layout is chosen automatically:
1446///
1447/// * **Simple `VP8L`** (`RIFF`/`WEBP` + `VP8L`) when `has_alpha` is `false`
1448/// **and** `meta` is empty — the smallest spec-conformant still image.
1449/// * **Extended `VP8X`** (`RIFF`/`WEBP` + `VP8X` + `ICCP` + `VP8L` +
1450/// `EXIF` + `XMP `) when `has_alpha` is `true` **or** any metadata
1451/// field is set. The §2.7.1 `VP8X` flag octet declares exactly the
1452/// features present (`L`/`I`/`E`/`X`), and the metadata chunks are emitted
1453/// in the §2.7 order (`ICCP` before the image, `EXIF`/`XMP ` after).
1454///
1455/// The bitstream's own §3.4 `alpha_is_used` header bit is set from
1456/// `has_alpha`. Decoding the result through [`decode_webp`] reproduces the
1457/// input pixels exactly; [`extract_metadata`] reads back the embedded
1458/// ICC / Exif / XMP payloads.
1459pub fn encode_vp8l_argb_with_metadata(
1460 width: u32,
1461 height: u32,
1462 argb: &[u32],
1463 has_alpha: bool,
1464 meta: &WebpMetadata<'_>,
1465) -> Result<Vec<u8>, WebpError> {
1466 // Bare VP8L bitstream (image-header + image stream).
1467 let payload = encode_vp8l_argb_with(argb, width, height, has_alpha)?;
1468
1469 // Simple layout when there is nothing to declare in a VP8X.
1470 if !has_alpha && meta.is_empty() {
1471 return build::build_webp_file(&payload, build::ImageKind::Lossless, width, height)
1472 .map_err(Error::from)
1473 .map_err(WebpError::from);
1474 }
1475
1476 // Extended layout: VP8X header declaring exactly the present features,
1477 // then ICCP, the VP8L image, EXIF, XMP — the §2.7 order.
1478 let flags = build::Vp8xFlags {
1479 has_iccp: meta.icc.is_some(),
1480 has_alpha,
1481 has_exif: meta.exif.is_some(),
1482 has_xmp: meta.xmp.is_some(),
1483 has_animation: false,
1484 };
1485 let vp8x_payload = build::build_vp8x_chunk(width, height, flags)
1486 .map_err(Error::from)
1487 .map_err(WebpError::from)?;
1488
1489 let mut body = Vec::new();
1490 let mut push_chunk = |fourcc, payload: &[u8]| -> Result<(), WebpError> {
1491 let chunk = build::build_chunk(fourcc, payload)
1492 .map_err(Error::from)
1493 .map_err(WebpError::from)?;
1494 body.extend_from_slice(&chunk);
1495 Ok(())
1496 };
1497
1498 push_chunk(container::fourcc::VP8X, &vp8x_payload)?;
1499 if let Some(icc) = meta.icc {
1500 push_chunk(container::fourcc::ICCP, icc)?;
1501 }
1502 push_chunk(container::fourcc::VP8L, &payload)?;
1503 if let Some(exif) = meta.exif {
1504 push_chunk(container::fourcc::EXIF, exif)?;
1505 }
1506 if let Some(xmp) = meta.xmp {
1507 push_chunk(container::fourcc::XMP, xmp)?;
1508 }
1509
1510 // §2.4 file framing around the assembled body.
1511 let file_size = (body.len() as u64) + 4;
1512 if file_size > u64::from(u32::MAX) {
1513 return Err(WebpError::InvalidData);
1514 }
1515 let mut out = Vec::with_capacity(12 + body.len());
1516 out.extend_from_slice(&container::fourcc::RIFF);
1517 out.extend_from_slice(&(file_size as u32).to_le_bytes());
1518 out.extend_from_slice(&container::fourcc::WEBP);
1519 out.extend_from_slice(&body);
1520 Ok(out)
1521}
1522
1523// ─────────────────────── Published-shape animation encode API ───────────────────────
1524//
1525// The published-0.1.5 `build_animated_webp` surface, rebuilt on top of the
1526// in-crate VP8L encoder + the §2.7.1.1 ANIM / ANMF container framing. Only the
1527// VP8L-lossless path (`AnimFrameMode::Lossless`) is wired up; `Auto` / `Delta`
1528// return `WebpError::Unsupported` (the VP8 lossy + delta paths are blocked on
1529// `oxideav-vp8`, workspace task #1041).
1530
1531#[doc(inline)]
1532pub use anim_encode::{
1533 build_animated_webp, build_animated_webp_with_options, AnimEncoderOptions, AnimFrame,
1534 AnimFrameMode, DeltaConfig, DownsampleKernel,
1535};
1536
1537/// Stable codec identifier the VP8L lossless encoder registers under in the
1538/// codec registry — the published `"webp_vp8l"` name.
1539pub const CODEC_ID_VP8L: &str = "webp_vp8l";
1540
1541/// Stable codec identifier the VP8 lossy encoder registers under in the
1542/// codec registry — the published `"webp_vp8"` name. The encoder itself
1543/// is blocked on the `oxideav-vp8` Phase-2 lossy encoder (workspace task
1544/// #1041); the id is reserved so consumers can look it up today and the
1545/// registry slots in the factory once the encoder lands.
1546pub const CODEC_ID_VP8: &str = "webp_vp8";
1547
1548// `Result` is published at `oxideav_webp::error::Result` (see
1549// `crate::error`). It is NOT re-exported at the crate root because the
1550// crate's source uses `Result<T, E>` extensively with two type
1551// parameters (the std prelude form); shadowing that name at the root
1552// would break those call sites. The published-0.1.2 documented path is
1553// the qualified `oxideav_webp::error::Result`.
1554
1555/// Repack a scan-line-order ARGB pixel buffer (`(a<<24)|(r<<16)|(g<<8)|b`)
1556/// into interleaved 8-bit `[R, G, B, A]` bytes — the
1557/// `oxideav_core::PixelFormat::Rgba` layout.
1558fn argb_to_rgba(pixels: &[u32]) -> Vec<u8> {
1559 // Write four bytes per pixel into a pre-sized buffer via
1560 // `chunks_exact_mut(4)`, the same shape `Vp8lImage::to_rgba_scalar`
1561 // adopted: it drops the per-`push` capacity-check + bounds-check the
1562 // one-byte-at-a-time loop incurred and lets the compiler auto-
1563 // vectorise the strided channel stores. The produced bytes are
1564 // byte-for-byte identical to the prior push loop — `[R, G, B, A]`
1565 // order matching `oxideav_core::PixelFormat::Rgba`.
1566 let mut out = vec![0u8; pixels.len() * 4];
1567 for (chunk, &argb) in out.chunks_exact_mut(4).zip(pixels.iter()) {
1568 chunk[0] = (argb >> 16) as u8; // R
1569 chunk[1] = (argb >> 8) as u8; // G
1570 chunk[2] = argb as u8; // B
1571 chunk[3] = (argb >> 24) as u8; // A
1572 }
1573 out
1574}
1575
1576/// Install the WebP decoder factory and the `.webp` extension hint into
1577/// `ctx` per round 112.
1578///
1579/// Wraps [`registry::register`]; see that module for the full breakdown
1580/// of what lands in the codec / container sub-registries. The decoder
1581/// covers the §2.6 / §3.4 `VP8L` lossless image (simple or
1582/// `VP8X`-extended) with optional §2.7.1.2 `ALPH`-over-`VP8L` alpha
1583/// override, and (round 124) the §2.5 `VP8 ` lossy image decoded via the
1584/// `oxideav-vp8` sibling crate (with optional `ALPH`-over-`VP8 ` alpha).
1585/// The standalone [`extract_lossy_chunk`] routing API stays available for
1586/// callers that want the raw VP8 bitstream slice.
1587#[cfg(feature = "registry")]
1588pub fn register(ctx: &mut RuntimeContext) {
1589 registry::register(ctx);
1590}
1591
1592/// Install only the WebP **codec** factories into `ctx` — the
1593/// per-codec `Decoder` / `Encoder` impls under the `"webp"`, `"webp_vp8l"`,
1594/// and `"webp_vp8"` ids.
1595///
1596/// This is the `RuntimeContext`-typed crate-root form per the
1597/// published 0.1.2 surface; for callers driving the registry
1598/// piece-wise the lower-level
1599/// [`registry::register_codecs`]`(&mut ctx.codecs)` form is still
1600/// available.
1601#[cfg(feature = "registry")]
1602pub fn register_codecs(ctx: &mut RuntimeContext) {
1603 registry::register_codecs(&mut ctx.codecs);
1604}
1605
1606/// Install only the WebP **container** hooks into `ctx` — the `.webp`
1607/// file-extension mapping that lets a demuxer-discovery pass route a
1608/// `.webp` file back to the WebP codec id.
1609///
1610/// `RuntimeContext`-typed counterpart of
1611/// [`registry::register_containers`]`(&mut ctx.containers)`.
1612#[cfg(feature = "registry")]
1613pub fn register_containers(ctx: &mut RuntimeContext) {
1614 registry::register_containers(&mut ctx.containers);
1615}
1616
1617#[cfg(feature = "registry")]
1618oxideav_core::register!("webp", register);