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
|
//! Encoding of WebP images.
///
/// Uses the simple encoding API from the [libwebp] library.
///
/// [libwebp]: https://developers.google.com/speed/webp/docs/api#simple_encoding_api
use std::io::Write;
use libwebp::{Encoder, PixelLayout, WebPMemory};
use crate::error::{
EncodingError, ParameterError, ParameterErrorKind, UnsupportedError, UnsupportedErrorKind,
};
use crate::flat::SampleLayout;
use crate::{ColorType, ImageEncoder, ImageError, ImageFormat, ImageResult};
/// WebP Encoder.
pub struct WebPEncoder<W> {
inner: W,
quality: WebPQuality,
}
/// WebP encoder quality.
#[derive(Debug, Copy, Clone)]
pub struct WebPQuality(Quality);
#[derive(Debug, Copy, Clone)]
enum Quality {
Lossless,
Lossy(u8),
}
impl WebPQuality {
/// Minimum lossy quality value (0).
pub const MIN: u8 = 0;
/// Maximum lossy quality value (100).
pub const MAX: u8 = 100;
/// Default lossy quality (80), providing a balance of quality and file size.
pub const DEFAULT: u8 = 80;
/// Lossless encoding.
pub fn lossless() -> Self {
Self(Quality::Lossless)
}
/// Lossy encoding. 0 = low quality, small size; 100 = high quality, large size.
///
/// Values are clamped from 0 to 100.
pub fn lossy(quality: u8) -> Self {
Self(Quality::Lossy(quality.clamp(Self::MIN, Self::MAX)))
}
}
impl Default for WebPQuality {
fn default() -> Self {
Self::lossy(WebPQuality::DEFAULT)
}
}
impl<W: Write> WebPEncoder<W> {
/// Create a new encoder that writes its output to `w`.
///
/// Defaults to lossy encoding, see [`WebPQuality::DEFAULT`].
pub fn new(w: W) -> Self {
WebPEncoder::new_with_quality(w, WebPQuality::default())
}
/// Create a new encoder with the specified quality, that writes its output to `w`.
pub fn new_with_quality(w: W, quality: WebPQuality) -> Self {
Self { inner: w, quality }
}
/// Encode image data with the indicated color type.
///
/// The encoder requires image data be Rgb8 or Rgba8.
pub fn encode(
mut self,
data: &[u8],
width: u32,
height: u32,
color: ColorType,
) -> ImageResult<()> {
// TODO: convert color types internally?
let layout = match color {
ColorType::Rgb8 => PixelLayout::Rgb,
ColorType::Rgba8 => PixelLayout::Rgba,
_ => {
return Err(ImageError::Unsupported(
UnsupportedError::from_format_and_kind(
ImageFormat::WebP.into(),
UnsupportedErrorKind::Color(color.into()),
),
))
}
};
// Validate dimensions upfront to avoid panics.
if width == 0
|| height == 0
|| !SampleLayout::row_major_packed(color.channel_count(), width, height)
.fits(data.len())
{
return Err(ImageError::Parameter(ParameterError::from_kind(
ParameterErrorKind::DimensionMismatch,
)));
}
// Call the native libwebp library to encode the image.
let encoder = Encoder::new(data, layout, width, height);
let encoded: WebPMemory = match self.quality.0 {
Quality::Lossless => encoder.encode_lossless(),
Quality::Lossy(quality) => encoder.encode(quality as f32),
};
// The simple encoding API in libwebp does not return errors.
if encoded.is_empty() {
return Err(ImageError::Encoding(EncodingError::new(
ImageFormat::WebP.into(),
"encoding failed, output empty",
)));
}
self.inner.write_all(&encoded)?;
Ok(())
}
}
impl<W: Write> ImageEncoder for WebPEncoder<W> {
fn write_image(
self,
buf: &[u8],
width: u32,
height: u32,
color_type: ColorType,
) -> ImageResult<()> {
self.encode(buf, width, height, color_type)
}
}
#[cfg(test)]
mod tests {
use crate::codecs::webp::{WebPEncoder, WebPQuality};
use crate::{ColorType, ImageEncoder};
#[test]
fn webp_lossless_deterministic() {
// 1x1 8-bit image buffer containing a single red pixel.
let rgb: &[u8] = &[255, 0, 0];
let rgba: &[u8] = &[255, 0, 0, 128];
for (color, img, expected) in [
(
ColorType::Rgb8,
rgb,
[
82, 73, 70, 70, 28, 0, 0, 0, 87, 69, 66, 80, 86, 80, 56, 76, 15, 0, 0, 0, 47,
0, 0, 0, 0, 7, 16, 253, 143, 254, 7, 34, 162, 255, 1, 0,
],
),
(
ColorType::Rgba8,
rgba,
[
82, 73, 70, 70, 28, 0, 0, 0, 87, 69, 66, 80, 86, 80, 56, 76, 15, 0, 0, 0, 47,
0, 0, 0, 16, 7, 16, 253, 143, 2, 6, 34, 162, 255, 1, 0,
],
),
] {
// Encode it into a memory buffer.
let mut encoded_img = Vec::new();
{
let encoder =
WebPEncoder::new_with_quality(&mut encoded_img, WebPQuality::lossless());
encoder
.write_image(&img, 1, 1, color)
.expect("image encoding failed");
}
// WebP encoding should be deterministic.
assert_eq!(encoded_img, expected);
}
}
#[derive(Debug, Clone)]
struct MockImage {
width: u32,
height: u32,
color: ColorType,
data: Vec<u8>,
}
impl quickcheck::Arbitrary for MockImage {
fn arbitrary(g: &mut quickcheck::Gen) -> Self {
// Limit to small, non-empty images <= 512x512.
let width = u32::arbitrary(g) % 512 + 1;
let height = u32::arbitrary(g) % 512 + 1;
let (color, stride) = if bool::arbitrary(g) {
(ColorType::Rgb8, 3)
} else {
(ColorType::Rgba8, 4)
};
let size = width * height * stride;
let data: Vec<u8> = (0..size).map(|_| u8::arbitrary(g)).collect();
MockImage {
width,
height,
color,
data,
}
}
}
quickcheck! {
fn fuzz_webp_valid_image(image: MockImage, quality: u8) -> bool {
// Check valid images do not panic.
let mut buffer = Vec::<u8>::new();
for webp_quality in [WebPQuality::lossless(), WebPQuality::lossy(quality)] {
buffer.clear();
let encoder = WebPEncoder::new_with_quality(&mut buffer, webp_quality);
if !encoder
.write_image(&image.data, image.width, image.height, image.color)
.is_ok() {
return false;
}
}
true
}
fn fuzz_webp_no_panic(data: Vec<u8>, width: u8, height: u8, quality: u8) -> bool {
// Check random (usually invalid) parameters do not panic.
let mut buffer = Vec::<u8>::new();
for color in [ColorType::Rgb8, ColorType::Rgba8] {
for webp_quality in [WebPQuality::lossless(), WebPQuality::lossy(quality)] {
buffer.clear();
let encoder = WebPEncoder::new_with_quality(&mut buffer, webp_quality);
// Ignore errors.
let _ = encoder
.write_image(&data, width as u32, height as u32, color);
}
}
true
}
}
}
|