Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add WebP encoding support #1784

Merged
merged 3 commits into from Sep 22, 2022
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/rust.yml
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
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
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 |
HeroicKatora marked this conversation as resolved.
Show resolved Hide resolved
| 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
@@ -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;
HeroicKatora marked this conversation as resolved.
Show resolved Hide resolved

/// 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 {
HeroicKatora marked this conversation as resolved.
Show resolved Hide resolved
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",
HeroicKatora marked this conversation as resolved.
Show resolved Hide resolved
)));
}

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
@@ -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
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
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
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