tiny_webp/lib.rs
1//! Encodes RGB and RGBA pixel buffers as lossy WebP files.
2//!
3//! Version 0.1.0 writes one VP8 key frame in a RIFF WebP container. It uses DC
4//! prediction, the Y2 transform path, one token partition, and a fixed
5//! quantizer chosen from [`Options::quality`]. The encoder keeps exact alpha
6//! from non-opaque RGBA input in an uncompressed `ALPH` chunk.
7//!
8//! The library is `no_std` and allocates through `alloc`. It reads and writes
9//! byte slices in memory. Callers own file and stream I/O.
10//!
11//! [`Options`] is `#[non_exhaustive]`, so build one from its default and
12//! assign the fields that change.
13//!
14//! ```
15//! let mut opts = tiny_webp::Options::default();
16//! opts.quality = 90;
17//! opts.alpha = tiny_webp::Alpha::Discard;
18//!
19//! let rgba = [128u8; 16];
20//! let webp = tiny_webp::encode_rgba(&rgba, 2, 2, &opts)?;
21//! assert_eq!(&webp[..4], b"RIFF");
22//! # Ok::<(), tiny_webp::Error>(())
23//! ```
24
25#![no_std]
26#![forbid(unsafe_code)]
27#![warn(missing_docs)]
28#![deny(clippy::float_arithmetic)]
29
30extern crate alloc;
31#[cfg(test)]
32extern crate std;
33
34mod bool_coder;
35mod color;
36mod frame;
37#[cfg(test)]
38#[path = "../fixtures/generator.rs"]
39mod generator;
40#[cfg(test)]
41#[path = "../fixtures/png_writer.rs"]
42mod png_writer;
43pub(crate) mod prediction;
44mod quantize;
45mod residual;
46mod transform;
47
48use alloc::vec::Vec;
49use core::fmt;
50
51/// Largest width or height a WebP bitstream carries, in pixels.
52///
53/// The VP8 key frame header spends 14 bits on each dimension, so each side
54/// runs from 1 to this value.
55pub const MAX_DIMENSION: u32 = 16383;
56
57/// What the encoder does with the alpha plane of an RGBA input.
58#[non_exhaustive]
59#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
60pub enum Alpha {
61 /// Keep the plane and write it so it decodes byte for byte.
62 #[default]
63 Lossless,
64 /// Drop the plane and write an opaque image.
65 Discard,
66}
67
68/// The loop filter strength the encoder signals in the frame header.
69///
70/// A key frame encoder signals the filter and leaves the decoder to run it.
71#[non_exhaustive]
72#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
73pub enum Filter {
74 /// Signal level 0 and sharpness 0 at version 0.1.0.
75 #[default]
76 Auto,
77 /// Signal an exact level and sharpness.
78 Level {
79 /// Filter level, saturated at 63.
80 level: u8,
81 /// Filter sharpness, saturated at 7.
82 sharpness: u8,
83 },
84 /// Signal level 0, which leaves the decoder's filter idle.
85 Off,
86}
87
88/// Settings for one encode.
89///
90/// In version 0.1.0, [`Options::default`] uses quality 75 and keeps alpha
91/// lossless. Its [`Filter::Auto`] setting signals loop filter level 0. The
92/// default writes `VP8X` only when transparency needs it or
93/// [`Options::force_vp8x`] requests it.
94#[non_exhaustive]
95#[derive(Clone, Copy, Debug, PartialEq, Eq)]
96pub struct Options {
97 /// Quality on cwebp's 0 to 100 scale, with larger values saturated at 100.
98 pub quality: u8,
99 /// Treatment of the alpha plane.
100 pub alpha: Alpha,
101 /// Write a `VP8X` container header even for an opaque image.
102 ///
103 /// An opaque image otherwise gets a bare `VP8 ` chunk. Set this for a
104 /// consumer that wants one container shape for every output.
105 pub force_vp8x: bool,
106 /// Loop filter strength.
107 pub filter: Filter,
108}
109
110impl Default for Options {
111 fn default() -> Self {
112 Self {
113 quality: 75,
114 alpha: Alpha::Lossless,
115 force_vp8x: false,
116 filter: Filter::Auto,
117 }
118 }
119}
120
121/// A call the encoder refused.
122#[non_exhaustive]
123#[derive(Clone, Copy, Debug, PartialEq, Eq)]
124pub enum Error {
125 /// A width or a height outside 1 through [`MAX_DIMENSION`].
126 DimensionsOutOfRange {
127 /// The width the caller passed.
128 width: u32,
129 /// The height the caller passed.
130 height: u32,
131 },
132 /// A pixel buffer whose length disagrees with the dimensions.
133 BufferSizeMismatch {
134 /// Bytes the dimensions ask for.
135 expected: usize,
136 /// Bytes the caller passed.
137 actual: usize,
138 },
139}
140
141impl fmt::Display for Error {
142 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
143 match self {
144 Self::DimensionsOutOfRange { width, height } => {
145 write!(
146 f,
147 "dimensions {width}x{height} fall outside 1..={MAX_DIMENSION}"
148 )
149 }
150 Self::BufferSizeMismatch { expected, actual } => {
151 write!(
152 f,
153 "buffer holds {actual} bytes and the dimensions ask for {expected}"
154 )
155 }
156 }
157 }
158}
159
160impl core::error::Error for Error {}
161
162/// Encodes an RGBA buffer into the bytes of a WebP file.
163///
164/// `rgba` holds `width * height` pixels in row order, four bytes each, in the
165/// order red, green, blue, alpha.
166///
167/// # Errors
168///
169/// [`Error::DimensionsOutOfRange`] when a side falls outside 1 through
170/// [`MAX_DIMENSION`], carrying the values passed. [`Error::BufferSizeMismatch`]
171/// when `rgba` runs to a length other than `width * height * 4`. The dimension
172/// check runs first, so a call that gets both wrong reports the dimensions.
173pub fn encode_rgba(rgba: &[u8], width: u32, height: u32, opts: &Options) -> Result<Vec<u8>, Error> {
174 check(rgba.len(), width, height, 4)?;
175 Ok(frame::encode(
176 rgba,
177 width as usize,
178 height as usize,
179 4,
180 quantize::quantizer_index(opts.quality),
181 opts,
182 )
183 .webp)
184}
185
186/// Encodes an RGB buffer into the bytes of a WebP file.
187///
188/// `rgb` holds `width * height` pixels in row order, three bytes each, in the
189/// order red, green, blue. The output is opaque, and [`Options::alpha`] has
190/// nothing to act on.
191///
192/// # Errors
193///
194/// The same two errors as [`encode_rgba`], in the same order, with three
195/// bytes per pixel in the length the buffer check asks for.
196pub fn encode_rgb(rgb: &[u8], width: u32, height: u32, opts: &Options) -> Result<Vec<u8>, Error> {
197 check(rgb.len(), width, height, 3)?;
198 Ok(frame::encode(
199 rgb,
200 width as usize,
201 height as usize,
202 3,
203 quantize::quantizer_index(opts.quality),
204 opts,
205 )
206 .webp)
207}
208
209/// Holds the entry-point checks in one place so both spell the same order.
210fn check(actual: usize, width: u32, height: u32, bytes_per_pixel: u64) -> Result<(), Error> {
211 if width == 0 || height == 0 || width > MAX_DIMENSION || height > MAX_DIMENSION {
212 return Err(Error::DimensionsOutOfRange { width, height });
213 }
214 // 16383 * 16383 * 4 fits in usize on every supported target.
215 let expected = (u64::from(width) * u64::from(height) * bytes_per_pixel) as usize;
216 if actual != expected {
217 return Err(Error::BufferSizeMismatch { expected, actual });
218 }
219 Ok(())
220}