Skip to main content

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}