Skip to content

Commit

Permalink
Add webp-encoder
Browse files Browse the repository at this point in the history
  • Loading branch information
cycraig committed Sep 7, 2022
1 parent 25182df commit 2021195
Show file tree
Hide file tree
Showing 8 changed files with 279 additions and 13 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/rust.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ jobs:
strategy:
matrix:
rust: ["1.56.1", stable, beta, nightly]
features: [gif, jpeg, png, tiff, ico, pnm, tga, webp, bmp, hdr, dxt, dds, farbfeld, openexr, jpeg_rayon, '']
features: [gif, jpeg, png, tiff, ico, pnm, tga, webp, bmp, hdr, dxt, dds, farbfeld, openexr, jpeg_rayon, webp-encoder, '']
steps:
- uses: actions/checkout@v2
- uses: actions-rs/toolchain@v1
Expand Down Expand Up @@ -45,7 +45,7 @@ jobs:
runs-on: ubuntu-20.04
strategy:
matrix:
features: [gif, jpeg, png, tiff, ico, pnm, tga, webp, bmp, hdr, dxt, dds, farbfeld, openexr, jpeg_rayon, '']
features: [gif, jpeg, png, tiff, ico, pnm, tga, webp, bmp, hdr, dxt, dds, farbfeld, openexr, jpeg_rayon, webp-encoder, '']

# we are using the cross project for cross compilation to mips:
# https://github.com/cross-rs/cross
Expand Down
7 changes: 6 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ dav1d = { version = "0.6.0", optional = true }
dcv-color-primitives = { version = "0.4.0", optional = true }
color_quant = "1.1"
exr = { version = "1.5.0", optional = true }
libwebp = { package = "webp", version = "0.2.2", default-features = false, optional = true }

[dev-dependencies]
crc32fast = "1.2.0"
Expand All @@ -64,14 +65,18 @@ default = ["gif", "jpeg", "ico", "png", "pnm", "tga", "tiff", "webp", "bmp", "hd
ico = ["bmp", "png"]
pnm = []
tga = []
webp = []
bmp = []
hdr = ["scoped_threadpool"]
dxt = []
dds = ["dxt"]
farbfeld = []
openexr = ["exr"]

# Enables WebP decoder support.
webp = []
# Non-default, not included in `webp`. Requires native dependency libwebp.
webp-encoder = ["libwebp"]

# Enables multi-threading.
# Requires latest stable Rust.
jpeg_rayon = ["jpeg/rayon"]
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ All image processing functions provided operate on types that implement the `Gen
| BMP | Yes | Rgb8, Rgba8, Gray8, GrayA8 |
| ICO | Yes | Yes |
| TIFF | Baseline(no fax support) + LZW + PackBits | Rgb8, Rgba8, Gray8 |
| WebP | Yes | No |
| WebP | Yes | Rgb8, Rgba8 |
| AVIF | Only 8-bit | Lossy |
| PNM | PBM, PGM, PPM, standard PAM | Yes |
| DDS | DXT1, DXT3, DXT5 | No |
Expand Down
238 changes: 238 additions & 0 deletions src/codecs/webp/encoder.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,238 @@
//! 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::{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, stride) = match color {
ColorType::Rgb8 => (PixelLayout::Rgb, 3),
ColorType::Rgba8 => (PixelLayout::Rgba, 4),
_ => {
return Err(ImageError::Unsupported(
UnsupportedError::from_format_and_kind(
ImageFormat::WebP.into(),
UnsupportedErrorKind::Color(color.into()),
),
))
}
};

// Validate dimensions upfront to avoid panics.
let expected_len = stride * (width * height) as u64;
if expected_len > data.len() as u64 {
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
}
}
}
25 changes: 19 additions & 6 deletions src/codecs/webp/mod.rs
Original file line number Diff line number Diff line change
@@ -1,15 +1,28 @@
//! Decoding of WebP Images
//! Decoding and Encoding of WebP Images
#[cfg(feature = "webp-encoder")]
pub use self::encoder::{WebPEncoder, WebPQuality};

#[cfg(feature = "webp-encoder")]
mod encoder;

#[cfg(feature = "webp")]
pub use self::decoder::WebPDecoder;

#[cfg(feature = "webp")]
mod decoder;
mod loop_filter;
mod transform;

#[cfg(feature = "webp")]
mod extended;
#[cfg(feature = "webp")]
mod huffman;
#[cfg(feature = "webp")]
mod loop_filter;
#[cfg(feature = "webp")]
mod lossless;
#[cfg(feature = "webp")]
mod lossless_transform;
#[cfg(feature = "webp")]
mod transform;

mod extended;

#[cfg(feature = "webp")]
pub mod vp8;
8 changes: 7 additions & 1 deletion src/image.rs
Original file line number Diff line number Diff line change
Expand Up @@ -217,7 +217,7 @@ impl ImageFormat {
ImageFormat::Pnm => true,
ImageFormat::Farbfeld => true,
ImageFormat::Avif => true,
ImageFormat::WebP => false,
ImageFormat::WebP => true,
ImageFormat::Hdr => false,
ImageFormat::OpenExr => true,
ImageFormat::Dds => false,
Expand Down Expand Up @@ -302,6 +302,10 @@ pub enum ImageOutputFormat {
/// An image in AVIF Format
Avif,

#[cfg(feature = "webp-encoder")]
/// An image in WebP Format.
WebP,

/// A value for signalling an error: An unsupported format was requested
// Note: When TryFrom is stabilized, this value should not be needed, and
// a TryInto<ImageOutputFormat> should be used instead of an Into<ImageOutputFormat>.
Expand Down Expand Up @@ -334,6 +338,8 @@ impl From<ImageFormat> for ImageOutputFormat {

#[cfg(feature = "avif-encoder")]
ImageFormat::Avif => ImageOutputFormat::Avif,
#[cfg(feature = "webp-encoder")]
ImageFormat::WebP => ImageOutputFormat::WebP,

f => ImageOutputFormat::Unsupported(format!("{:?}", f)),
}
Expand Down
4 changes: 4 additions & 0 deletions src/io/free_functions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,10 @@ pub(crate) fn write_buffer_impl<W: std::io::Write + Seek>(
ImageOutputFormat::Avif => {
avif::AvifEncoder::new(buffered_write).write_image(buf, width, height, color)
}
#[cfg(feature = "webp-encoder")]
ImageOutputFormat::WebP => {
webp::WebPEncoder::new(buffered_write).write_image(buf, width, height, color)
}

image::ImageOutputFormat::Unsupported(msg) => Err(ImageError::Unsupported(
UnsupportedError::from_format_and_kind(
Expand Down
4 changes: 2 additions & 2 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -185,7 +185,7 @@ pub mod flat;
/// | BMP | Yes | Rgb8, Rgba8, Gray8, GrayA8 |
/// | ICO | Yes | Yes |
/// | TIFF | Baseline(no fax support) + LZW + PackBits | Rgb8, Rgba8, Gray8 |
/// | WebP | Yes | No |
/// | WebP | Yes | Rgb8, Rgba8 |
/// | AVIF | Only 8-bit | Lossy |
/// | PNM | PBM, PGM, PPM, standard PAM | Yes |
/// | DDS | DXT1, DXT3, DXT5 | No |
Expand Down Expand Up @@ -244,7 +244,7 @@ pub mod codecs {
pub mod tga;
#[cfg(feature = "tiff")]
pub mod tiff;
#[cfg(feature = "webp")]
#[cfg(any(feature = "webp", feature = "webp-encoder"))]
pub mod webp;
}

Expand Down

0 comments on commit 2021195

Please sign in to comment.