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 all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
7 changes: 5 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,14 +26,17 @@ 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 |
| AVIF | Only 8-bit | Lossy |
| WebP | Yes | Rgb8, Rgba8 \* |
| AVIF | Only 8-bit \*\* | Lossy |
| PNM | PBM, PGM, PPM, standard PAM | Yes |
| DDS | DXT1, DXT3, DXT5 | No |
| TGA | Yes | Rgb8, Rgba8, Bgr8, Bgra8, Gray8, GrayA8 |
| OpenEXR | Rgb32F, Rgba32F (no dwa compression) | Rgb32F, Rgba32F (no dwa compression) |
| farbfeld | Yes | Yes |

- \* Requires the `webp-encoder` feature, uses the libwebp C library.
- \*\* Requires the `avif-decoder` feature, uses the libdav1d C library.

### The [`ImageDecoder`](https://docs.rs/image/*/image/trait.ImageDecoder.html) and [`ImageDecoderRect`](https://docs.rs/image/*/image/trait.ImageDecoderRect.html) Traits

All image format decoders implement the `ImageDecoder` trait which provide
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::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;
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 = 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 !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",
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
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