From 2b6953a3954fc8196118cef2eb65ce8b9955ae58 Mon Sep 17 00:00:00 2001 From: ophir Date: Sun, 24 May 2020 14:32:49 +0200 Subject: [PATCH] Change the JPEG encoder to work on a GenericImageView instead of a byte slice This change allows users to encode jpeg images from other sources than memory, thus finally allowing the encoding of images that do not fit in memory. This also allows users to make the encoder work in a streaming fashion. (see #1219) This also finally allows encoding images from other pixel formats than 8-bit RGB. The conversion to 8bit YCbCr (used in JPEG) is done on the fly during encoding. --- src/color.rs | 15 ++- src/dynimage.rs | 5 +- src/jpeg/encoder.rs | 244 ++++++++++++++++++++++++-------------------- 3 files changed, 151 insertions(+), 113 deletions(-) diff --git a/src/color.rs b/src/color.rs index 9a9e301a5c..4e48c25b73 100644 --- a/src/color.rs +++ b/src/color.rs @@ -1,6 +1,7 @@ -use num_traits::{NumCast, ToPrimitive, Zero}; use std::ops::{Index, IndexMut}; +use num_traits::{NumCast, ToPrimitive, Zero}; + use crate::traits::{Pixel, Primitive}; /// An enumeration over supported color types and bit depths @@ -57,6 +58,16 @@ impl ColorType { } } + /// Returns false if the color scheme is grayscale, true otherwise. + pub fn has_color(self) -> bool { + use ColorType::*; + match self { + L8 | L16 | La8 | La16 => false, + Rgb8 | Bgr8 | Rgb16 | Rgba8 | Bgra8 | Rgba16 => true, + __NonExhaustive(marker) => match marker._private {}, + } + } + /// Returns the number of bits contained in a pixel of `ColorType` ```c``` (which will always be /// a multiple of 8). pub fn bits_per_pixel(self) -> u16 { @@ -1126,7 +1137,7 @@ impl Invert for Bgr { #[cfg(test)] mod tests { - use super::{Luma, LumaA, Pixel, Rgb, Rgba, Bgr, Bgra}; + use super::{Bgr, Bgra, Luma, LumaA, Pixel, Rgb, Rgba}; #[test] fn test_apply_with_alpha_rgba() { diff --git a/src/dynimage.rs b/src/dynimage.rs index 48e57161b2..3376fa2153 100644 --- a/src/dynimage.rs +++ b/src/dynimage.rs @@ -738,9 +738,8 @@ impl DynamicImage { } #[cfg(feature = "jpeg")] image::ImageOutputFormat::Jpeg(quality) => { - let mut j = jpeg::JPEGEncoder::new_with_quality(w, quality); - - j.encode(&bytes, width, height, color)?; + let j = jpeg::JPEGEncoder::new_with_quality(w, quality); + j.write_image(&bytes, width, height, color)?; Ok(()) } diff --git a/src/jpeg/encoder.rs b/src/jpeg/encoder.rs index 5848544b5c..060413b917 100644 --- a/src/jpeg/encoder.rs +++ b/src/jpeg/encoder.rs @@ -1,17 +1,18 @@ #![allow(clippy::too_many_arguments)] +use std::convert::TryFrom; +use std::io::{self, Write}; + use byteorder::{BigEndian, WriteBytesExt}; -use crate::error::{ImageError, ImageResult, ParameterError, ParameterErrorKind, UnsupportedError, UnsupportedErrorKind}; -use crate::math::utils::clamp; use num_iter::range_step; -use std::io::{self, Write}; -use crate::color; +use crate::{Bgr, Bgra, ColorType, GenericImageView, ImageBuffer, Luma, LumaA, Pixel, Rgb, Rgba}; +use crate::error::{ImageError, ImageResult, ParameterError, ParameterErrorKind, UnsupportedError, UnsupportedErrorKind}; use crate::image::{ImageEncoder, ImageFormat}; +use crate::math::utils::clamp; use super::entropy::build_huff_lut; use super::transform; -use std::convert::TryFrom; // Markers // Baseline DCT @@ -424,7 +425,7 @@ impl<'a, W: Write> JPEGEncoder<'a, W> { self.pixel_density = pixel_density; } - /// Encodes the image ```image``` + /// Encodes the image stored in the raw byte buffer ```image``` /// that has dimensions ```width``` and ```height``` /// and ```ColorType``` ```c``` /// @@ -434,9 +435,50 @@ impl<'a, W: Write> JPEGEncoder<'a, W> { image: &[u8], width: u32, height: u32, - c: color::ColorType, + color_type: ColorType, ) -> ImageResult<()> { - let n = c.channel_count(); + match color_type { + ColorType::L8 => { + let image: ImageBuffer, _> = ImageBuffer::from_raw(width, height, image).unwrap(); + self.encode_image(&image) + }, + ColorType::La8 => { + let image: ImageBuffer, _> = ImageBuffer::from_raw(width, height, image).unwrap(); + self.encode_image(&image) + }, + ColorType::Rgb8 => { + let image: ImageBuffer, _> = ImageBuffer::from_raw(width, height, image).unwrap(); + self.encode_image(&image) + }, + ColorType::Rgba8 => { + let image: ImageBuffer, _> = ImageBuffer::from_raw(width, height, image).unwrap(); + self.encode_image(&image) + }, + ColorType::Bgr8 => { + let image: ImageBuffer, _> = ImageBuffer::from_raw(width, height, image).unwrap(); + self.encode_image(&image) + }, + ColorType::Bgra8 => { + let image: ImageBuffer, _> = ImageBuffer::from_raw(width, height, image).unwrap(); + self.encode_image(&image) + }, + _ => { + return Err(ImageError::Unsupported( + UnsupportedError::from_format_and_kind( + ImageFormat::Jpeg.into(), + UnsupportedErrorKind::Color(color_type.into()), + ), + )) + }, + } + } + + /// Encodes the given image + pub fn encode_image( + &mut self, + image: &I, + ) -> ImageResult<()> { + let n = I::Pixel::CHANNEL_COUNT; let num_components = if n == 1 || n == 2 { 1 } else { 3 }; self.writer.write_segment(SOI, None)?; @@ -449,12 +491,12 @@ impl<'a, W: Write> JPEGEncoder<'a, W> { build_frame_header( &mut buf, 8, - u16::try_from(width).map_err(|_| { + u16::try_from(image.width()).map_err(|_| { ImageError::Parameter(ParameterError::from_kind( ParameterErrorKind::DimensionMismatch, )) })?, - u16::try_from(height).map_err(|_| { + u16::try_from(image.height()).map_err(|_| { ImageError::Parameter(ParameterError::from_kind( ParameterErrorKind::DimensionMismatch, )) @@ -512,49 +554,29 @@ impl<'a, W: Write> JPEGEncoder<'a, W> { build_scan_header(&mut buf, &self.components[..num_components]); self.writer.write_segment(SOS, Some(&buf))?; - match c { - color::ColorType::Rgb8 => { - self.encode_rgb(image, width as usize, height as usize, 3)? - } - color::ColorType::Rgba8 => { - self.encode_rgb(image, width as usize, height as usize, 4)? - } - color::ColorType::L8 => { - self.encode_gray(image, width as usize, height as usize, 1)? - } - color::ColorType::La8 => { - self.encode_gray(image, width as usize, height as usize, 2)? - } - _ => { - return Err(ImageError::Unsupported( - UnsupportedError::from_format_and_kind( - ImageFormat::Jpeg.into(), - UnsupportedErrorKind::Color(c.into()), - ), - )) - } - }; + + if I::Pixel::COLOR_TYPE.has_color() { + self.encode_rgb(image) + } else { + self.encode_gray(image) + }?; self.writer.pad_byte()?; self.writer.write_segment(EOI, None)?; Ok(()) } - fn encode_gray( + fn encode_gray( &mut self, - image: &[u8], - width: usize, - height: usize, - bpp: usize, + image: &I, ) -> io::Result<()> { let mut yblock = [0u8; 64]; let mut y_dcprev = 0; let mut dct_yblock = [0i32; 64]; - for y in range_step(0, height, 8) { - for x in range_step(0, width, 8) { - // RGB -> YCbCr - copy_blocks_gray(image, x, y, width, bpp, &mut yblock); + for y in range_step(0, image.height(), 8) { + for x in range_step(0, image.width(), 8) { + copy_blocks_gray(image, x, y, &mut yblock); // Level shift and fdct // Coeffs are scaled by 8 @@ -575,12 +597,9 @@ impl<'a, W: Write> JPEGEncoder<'a, W> { Ok(()) } - fn encode_rgb( + fn encode_rgb( &mut self, - image: &[u8], - width: usize, - height: usize, - bpp: usize, + image: &I, ) -> io::Result<()> { let mut y_dcprev = 0; let mut cb_dcprev = 0; @@ -594,15 +613,13 @@ impl<'a, W: Write> JPEGEncoder<'a, W> { let mut cb_block = [0u8; 64]; let mut cr_block = [0u8; 64]; - for y in range_step(0, height, 8) { - for x in range_step(0, width, 8) { + for y in range_step(0, image.height(), 8) { + for x in range_step(0, image.width(), 8) { // RGB -> YCbCr copy_blocks_ycbcr( image, x, y, - width, - bpp, &mut yblock, &mut cb_block, &mut cr_block, @@ -647,7 +664,7 @@ impl<'a, W: Write> ImageEncoder for JPEGEncoder<'a, W> { buf: &[u8], width: u32, height: u32, - color_type: color::ColorType, + color_type: ColorType, ) -> ImageResult<()> { self.encode(buf, width, height, color_type) } @@ -771,82 +788,78 @@ fn encode_coefficient(coefficient: i32) -> (u8, u16) { (num_bits, val) } -fn rgb_to_ycbcr(r: u8, g: u8, b: u8) -> (u8, u8, u8) { - let r = f32::from(r); - let g = f32::from(g); - let b = f32::from(b); +#[inline] +fn rgb_to_ycbcr(pixel: P) -> (u8, u8, u8) { + use num_traits::{cast::ToPrimitive, bounds::Bounded}; + let p = pixel.to_rgb().0; + let max: f32 = P::Subpixel::max_value().to_f32().unwrap(); + let r: f32 = p[0].to_f32().unwrap(); + let g: f32 = p[1].to_f32().unwrap(); + let b: f32 = p[2].to_f32().unwrap(); - let y = 0.299f32 * r + 0.587f32 * g + 0.114f32 * b; - let cb = -0.1687f32 * r - 0.3313f32 * g + 0.5f32 * b + 128f32; - let cr = 0.5f32 * r - 0.4187f32 * g - 0.0813f32 * b + 128f32; + let y = 65.481 / max * r + 128.553 / max * g + 24.933 / max * b; + let cb = -37.797 / max * r - 74.203 / max * g + 112.0 / max * b + 128.; + let cr = 112. / max * r - 93.786 / max * g - 18.214 / max * b + 128.; (y as u8, cb as u8, cr as u8) } -fn value_at(s: &[u8], index: usize) -> u8 { - if index < s.len() { - s[index] - } else { - s[s.len() - 1] - } + +/// Returns the pixel at (x,y) if (x,y) is in the image, +/// otherwise the closest pixel in the image +fn pixel_at_or_near(source: &I, x: u32, y: u32) -> I::Pixel { + let x = x.min(source.width() - 1); + let y = y.min(source.height() - 1); + source.get_pixel(x, y) } -fn copy_blocks_ycbcr( - source: &[u8], - x0: usize, - y0: usize, - width: usize, - bpp: usize, +fn copy_blocks_ycbcr( + source: &I, + x0: u32, + y0: u32, yb: &mut [u8; 64], cbb: &mut [u8; 64], crb: &mut [u8; 64], ) { - for y in 0usize..8 { - let ystride = (y0 + y) * bpp * width; - - for x in 0usize..8 { - let xstride = x0 * bpp + x * bpp; - - let r = value_at(source, ystride + xstride); - let g = value_at(source, ystride + xstride + 1); - let b = value_at(source, ystride + xstride + 2); - - let (yc, cb, cr) = rgb_to_ycbcr(r, g, b); - - yb[y * 8 + x] = yc; - cbb[y * 8 + x] = cb; - crb[y * 8 + x] = cr; + for y in 0..8 { + for x in 0..8 { + let pixel = pixel_at_or_near(source, x + x0, y + y0); + let (yc, cb, cr) = rgb_to_ycbcr(pixel); + + yb[(y * 8 + x) as usize] = yc; + cbb[(y * 8 + x) as usize] = cb; + crb[(y * 8 + x) as usize] = cr; } } } -fn copy_blocks_gray( - source: &[u8], - x0: usize, - y0: usize, - width: usize, - bpp: usize, +fn copy_blocks_gray( + source: &I, + x0: u32, + y0: u32, gb: &mut [u8; 64], ) { - for y in 0usize..8 { - let ystride = (y0 + y) * bpp * width; - - for x in 0usize..8 { - let xstride = x0 * bpp + x * bpp; - gb[y * 8 + x] = value_at(source, ystride + xstride); + use num_traits::cast::ToPrimitive; + for y in 0..8 { + for x in 0..8 { + let pixel = pixel_at_or_near(source, x0 + x, y0 + y); + let luma = pixel.to_luma().0[0]; + gb[(y * 8 + x) as usize] = luma.to_u8().unwrap(); } } } #[cfg(test)] mod tests { - use super::super::JpegDecoder; - use super::{JPEGEncoder, PixelDensity, build_jfif_header}; - use crate::color::ColorType; - use crate::image::ImageDecoder; use std::io::Cursor; - use crate::ImageError; + + use crate::{Bgra, ImageBuffer, ImageEncoder, ImageError}; + use crate::color::ColorType; use crate::error::ParameterErrorKind::DimensionMismatch; + use crate::image::ImageDecoder; + + use super::{build_jfif_header, JPEGEncoder, PixelDensity}; + use super::super::JpegDecoder; fn decode(encoded: &[u8]) -> Vec { let decoder = JpegDecoder::new(Cursor::new(encoded)) @@ -865,9 +878,9 @@ mod tests { // encode it into a memory buffer let mut encoded_img = Vec::new(); { - let mut encoder = JPEGEncoder::new_with_quality(&mut encoded_img, 100); + let encoder = JPEGEncoder::new_with_quality(&mut encoded_img, 100); encoder - .encode(&img, 1, 1, ColorType::Rgb8) + .write_image(&img, 1, 1, ColorType::Rgb8) .expect("Could not encode image"); } @@ -891,9 +904,9 @@ mod tests { // encode it into a memory buffer let mut encoded_img = Vec::new(); { - let mut encoder = JPEGEncoder::new_with_quality(&mut encoded_img, 100); + let encoder = JPEGEncoder::new_with_quality(&mut encoded_img, 100); encoder - .encode(&img, 2, 2, ColorType::L8) + .write_image(&img[..], 2, 2, ColorType::L8) .expect("Could not encode image"); } @@ -933,8 +946,8 @@ mod tests { let img = [0; 65_536]; // Try to encode an image that is too large let mut encoded = Vec::new(); - let mut encoder = JPEGEncoder::new_with_quality(&mut encoded, 100); - let result = encoder.encode(&img, 65_536, 1, ColorType::L8); + let encoder = JPEGEncoder::new_with_quality(&mut encoded, 100); + let result = encoder.write_image(&img, 65_536, 1, ColorType::L8); match result { Err(ImageError::Parameter(err)) => { assert_eq!(err.kind(), DimensionMismatch) @@ -945,4 +958,19 @@ mod tests { } } } + + #[test] + fn test_bgra16() { + // Test encoding an RGBA 16-bit image. + // Jpeg is RGB 8-bit, so the conversion should be done on the fly + let mut encoded = Vec::new(); + let image: ImageBuffer, _> = ImageBuffer::from_raw( + 1, 1, vec![0, u16::MAX / 2, u16::MAX, u16::MAX]).unwrap(); + let mut encoder = JPEGEncoder::new_with_quality(&mut encoded, 100); + encoder.encode_image(&image).unwrap(); + let decoded = decode(&encoded); + assert!(decoded[0] > 200, "bad red channel in {:?}", &decoded); + assert!(100 < decoded[1] && decoded[1] < 150, "bad green channel in {:?}", &decoded); + assert!(decoded[2] < 50, "bad blue channel in {:?}", &decoded); + } }