diff --git a/README.md b/README.md index b304e43973..af4acd930c 100644 --- a/README.md +++ b/README.md @@ -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 | Lossy(Rgb only) + Lossless | No | +| WebP | Yes | No | | AVIF | Only 8-bit | Lossy | | PNM | PBM, PGM, PPM, standard PAM | Yes | | DDS | DXT1, DXT3, DXT5 | No | diff --git a/src/codecs/webp/decoder.rs b/src/codecs/webp/decoder.rs index 8707a5cb22..035b02cb88 100644 --- a/src/codecs/webp/decoder.rs +++ b/src/codecs/webp/decoder.rs @@ -1,28 +1,27 @@ use byteorder::{LittleEndian, ReadBytesExt}; use std::convert::TryFrom; -use std::io::{self, Cursor, Read}; +use std::io::{self, Cursor, Error, Read}; use std::marker::PhantomData; use std::{error, fmt, mem}; -use crate::error::{ - DecodingError, ImageError, ImageResult, UnsupportedError, UnsupportedErrorKind, -}; +use crate::error::{DecodingError, ImageError, ImageResult}; use crate::image::{ImageDecoder, ImageFormat}; +use crate::{color, AnimationDecoder, Frames}; -use crate::color; +use super::lossless::{LosslessDecoder, LosslessFrame}; +use super::vp8::{Frame as VP8Frame, Vp8Decoder}; -use super::lossless::LosslessDecoder; -use super::lossless::LosslessFrame; -use super::vp8::Frame as VP8Frame; -use super::vp8::Vp8Decoder; +use super::extended::{read_extended_header, ExtendedImage}; /// All errors that can occur when attempting to parse a WEBP container #[derive(Debug, Clone, Copy)] -enum DecoderError { +pub(crate) enum DecoderError { /// RIFF's "RIFF" signature not found or invalid RiffSignatureInvalid([u8; 4]), /// WebP's "WEBP" signature not found or invalid WebpSignatureInvalid([u8; 4]), + /// Chunk Header was incorrect or invalid in its usage + ChunkHeaderInvalid([u8; 4]), } impl fmt::Display for DecoderError { @@ -47,6 +46,10 @@ impl fmt::Display for DecoderError { "Invalid WebP signature: {}", SignatureWriter(*webp) )), + DecoderError::ChunkHeaderInvalid(header) => f.write_fmt(format_args!( + "Invalid Chunk header: {}", + SignatureWriter(*header) + )), } } } @@ -59,24 +62,77 @@ impl From for ImageError { impl error::Error for DecoderError {} -enum Frame { +/// All possible RIFF chunks in a WebP image file +#[allow(clippy::upper_case_acronyms)] +#[derive(Debug, Clone, Copy, PartialEq)] +pub(crate) enum WebPRiffChunk { + RIFF, + WEBP, + VP8, + VP8L, + VP8X, + ANIM, + ANMF, + ALPH, + ICCP, + EXIF, + XMP, +} + +impl WebPRiffChunk { + pub(crate) fn from_fourcc(chunk_fourcc: [u8; 4]) -> ImageResult { + match &chunk_fourcc { + b"RIFF" => Ok(Self::RIFF), + b"WEBP" => Ok(Self::WEBP), + b"VP8 " => Ok(Self::VP8), + b"VP8L" => Ok(Self::VP8L), + b"VP8X" => Ok(Self::VP8X), + b"ANIM" => Ok(Self::ANIM), + b"ANMF" => Ok(Self::ANMF), + b"ALPH" => Ok(Self::ALPH), + b"ICCP" => Ok(Self::ICCP), + b"EXIF" => Ok(Self::EXIF), + b"XMP " => Ok(Self::XMP), + _ => Err(DecoderError::ChunkHeaderInvalid(chunk_fourcc).into()), + } + } + + pub(crate) fn to_fourcc(&self) -> [u8; 4] { + match self { + Self::RIFF => *b"RIFF", + Self::WEBP => *b"WEBP", + Self::VP8 => *b"VP8 ", + Self::VP8L => *b"VP8L", + Self::VP8X => *b"VP8X", + Self::ANIM => *b"ANIM", + Self::ANMF => *b"ANMF", + Self::ALPH => *b"ALPH", + Self::ICCP => *b"ICCP", + Self::EXIF => *b"EXIF", + Self::XMP => *b"XMP ", + } + } +} + +enum WebPImage { Lossy(VP8Frame), Lossless(LosslessFrame), + Extended(ExtendedImage), } /// WebP Image format decoder. Currently only supports lossy RGB images or lossless RGBA images. pub struct WebPDecoder { r: R, - frame: Frame, + image: WebPImage, } impl WebPDecoder { /// Create a new WebPDecoder from the Reader ```r```. /// This function takes ownership of the Reader. pub fn new(r: R) -> ImageResult> { - let frame = Frame::Lossy(Default::default()); + let image = WebPImage::Lossy(Default::default()); - let mut decoder = WebPDecoder { r, frame }; + let mut decoder = WebPDecoder { r, image }; decoder.read_data()?; Ok(decoder) } @@ -101,78 +157,100 @@ impl WebPDecoder { } //reads the chunk header, decodes the frame and returns the inner decoder - fn read_frame(&mut self) -> ImageResult { - loop { - let mut chunk = [0; 4]; - self.r.read_exact(&mut chunk)?; - - match &chunk { - b"VP8 " => { - let m = read_len_cursor(&mut self.r)?; - - let mut vp8_decoder = Vp8Decoder::new(m); - let frame = vp8_decoder.decode_frame()?; - - return Ok(Frame::Lossy(frame.clone())); - } - b"VP8L" => { - let m = read_len_cursor(&mut self.r)?; - - let mut lossless_decoder = LosslessDecoder::new(m); - let frame = lossless_decoder.decode_frame()?; - - return Ok(Frame::Lossless(frame.clone())); - } - b"ALPH" | b"ANIM" | b"ANMF" => { - // Alpha and Animation isn't supported - return Err(ImageError::Unsupported( - UnsupportedError::from_format_and_kind( - ImageFormat::WebP.into(), - UnsupportedErrorKind::GenericFeature( - chunk.iter().map(|&b| b as char).collect(), - ), - ), - )); - } - _ => { - let mut len = u64::from(self.r.read_u32::()?); - - if len % 2 != 0 { - // RIFF chunks containing an uneven number of bytes append - // an extra 0x00 at the end of the chunk - // - // The addition cannot overflow since we have a u64 that was created from a u32 - len += 1; - } - - io::copy(&mut self.r.by_ref().take(len), &mut io::sink())?; - } + fn read_frame(&mut self) -> ImageResult { + let chunk = read_chunk(&mut self.r)?; + + match chunk { + Some((cursor, WebPRiffChunk::VP8)) => { + let mut vp8_decoder = Vp8Decoder::new(cursor); + let frame = vp8_decoder.decode_frame()?; + + Ok(WebPImage::Lossy(frame.clone())) + } + Some((cursor, WebPRiffChunk::VP8L)) => { + let mut lossless_decoder = LosslessDecoder::new(cursor); + let frame = lossless_decoder.decode_frame()?; + + Ok(WebPImage::Lossless(frame.clone())) + } + Some((mut cursor, WebPRiffChunk::VP8X)) => { + let info = read_extended_header(&mut cursor)?; + + let image = ExtendedImage::read_extended_chunks(&mut self.r, info)?; + + Ok(WebPImage::Extended(image)) } + None => Err(ImageError::IoError(Error::from( + io::ErrorKind::UnexpectedEof, + ))), + Some((_, chunk)) => Err(DecoderError::ChunkHeaderInvalid(chunk.to_fourcc()).into()), } } fn read_data(&mut self) -> ImageResult<()> { let _size = self.read_riff_header()?; - let frame = self.read_frame()?; + let image = self.read_frame()?; - self.frame = frame; + self.image = image; Ok(()) } } -fn read_len_cursor(r: &mut R) -> ImageResult>> +pub(crate) fn read_len_cursor(r: &mut R) -> ImageResult>> where R: Read, { - let len = r.read_u32::()?; + let mut len = u64::from(r.read_u32::()?); + + if len % 2 == 1 { + // RIFF chunks containing an uneven number of bytes append + // an extra 0x00 at the end of the chunk + // + // The addition cannot overflow since we have a u64 that was created from a u32 + len += 1; + } let mut framedata = Vec::new(); - r.by_ref().take(len as u64).read_to_end(&mut framedata)?; + r.by_ref().take(len).read_to_end(&mut framedata)?; + + //remove padding byte + if len % 2 == 1 { + framedata.pop(); + } + Ok(io::Cursor::new(framedata)) } +/// Reads a chunk +/// Returns an error if the chunk header is not a valid webp header or some other reading error +/// Returns None if and only if we hit end of file reading the four character code of the chunk +pub(crate) fn read_chunk(r: &mut R) -> ImageResult>, WebPRiffChunk)>> +where + R: Read, +{ + let mut chunk_fourcc = [0; 4]; + let result = r.read_exact(&mut chunk_fourcc); + + match result { + Ok(()) => {} + Err(err) => { + if err.kind() == io::ErrorKind::UnexpectedEof { + return Ok(None); + } else { + return Err(err.into()); + } + } + } + + let chunk = WebPRiffChunk::from_fourcc(chunk_fourcc)?; + + let cursor = read_len_cursor(r)?; + + Ok(Some((cursor, chunk))) +} + /// Wrapper struct around a `Cursor>` pub struct WebpReader(Cursor>, PhantomData); impl Read for WebpReader { @@ -193,52 +271,75 @@ impl<'a, R: 'a + Read> ImageDecoder<'a> for WebPDecoder { type Reader = WebpReader; fn dimensions(&self) -> (u32, u32) { - match &self.frame { - Frame::Lossy(vp8_frame) => (u32::from(vp8_frame.width), u32::from(vp8_frame.height)), - Frame::Lossless(lossless_frame) => ( + match &self.image { + WebPImage::Lossy(vp8_frame) => { + (u32::from(vp8_frame.width), u32::from(vp8_frame.height)) + } + WebPImage::Lossless(lossless_frame) => ( u32::from(lossless_frame.width), u32::from(lossless_frame.height), ), + WebPImage::Extended(extended) => extended.dimensions(), } } fn color_type(&self) -> color::ColorType { - match &self.frame { - Frame::Lossy(_) => color::ColorType::Rgb8, - Frame::Lossless(_) => color::ColorType::Rgba8, + match &self.image { + WebPImage::Lossy(_) => color::ColorType::Rgb8, + WebPImage::Lossless(_) => color::ColorType::Rgba8, + WebPImage::Extended(extended) => extended.color_type(), } } fn into_reader(self) -> ImageResult { - match &self.frame { - Frame::Lossy(vp8_frame) => { + match &self.image { + WebPImage::Lossy(vp8_frame) => { let mut data = vec![0; vp8_frame.get_buf_size()]; vp8_frame.fill_rgb(data.as_mut_slice()); Ok(WebpReader(Cursor::new(data), PhantomData)) } - Frame::Lossless(lossless_frame) => { + WebPImage::Lossless(lossless_frame) => { let mut data = vec![0; lossless_frame.get_buf_size()]; lossless_frame.fill_rgba(data.as_mut_slice()); Ok(WebpReader(Cursor::new(data), PhantomData)) } + WebPImage::Extended(extended) => { + let mut data = vec![0; extended.get_buf_size()]; + extended.fill_buf(data.as_mut_slice()); + Ok(WebpReader(Cursor::new(data), PhantomData)) + } } } fn read_image(self, buf: &mut [u8]) -> ImageResult<()> { assert_eq!(u64::try_from(buf.len()), Ok(self.total_bytes())); - match &self.frame { - Frame::Lossy(vp8_frame) => { + match &self.image { + WebPImage::Lossy(vp8_frame) => { vp8_frame.fill_rgb(buf); } - Frame::Lossless(lossless_frame) => { + WebPImage::Lossless(lossless_frame) => { lossless_frame.fill_rgba(buf); } + WebPImage::Extended(extended) => { + extended.fill_buf(buf); + } } Ok(()) } } +impl<'a, R: 'a + Read> AnimationDecoder<'a> for WebPDecoder { + fn into_frames(self) -> Frames<'a> { + match self.image { + WebPImage::Lossy(_) | WebPImage::Lossless(_) => { + Frames::new(Box::new(std::iter::empty())) + } + WebPImage::Extended(extended_image) => extended_image.into_frames(), + } + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/src/codecs/webp/extended.rs b/src/codecs/webp/extended.rs new file mode 100644 index 0000000000..e57f3b562d --- /dev/null +++ b/src/codecs/webp/extended.rs @@ -0,0 +1,741 @@ +use std::convert::TryInto; +use std::io::{self, Error, Read}; +use std::{error, fmt}; + +use super::decoder::{read_chunk, DecoderError::ChunkHeaderInvalid, WebPRiffChunk}; +use super::lossless::{LosslessDecoder, LosslessFrame}; +use super::vp8::{Frame as VP8Frame, Vp8Decoder}; +use crate::error::DecodingError; +use crate::image::ImageFormat; +use crate::{color, Delay, Frame, Frames, ImageError, ImageResult, Rgba, RgbaImage}; +use byteorder::{LittleEndian, ReadBytesExt}; + +//all errors that can occur while parsing extended chunks in a WebP file +#[derive(Debug, Clone, Copy)] +enum DecoderError { + // Some bits were invalid + InfoBitsInvalid { name: &'static str, value: u32 }, + // Alpha chunk doesn't match the frame's size + AlphaChunkSizeMismatch, + // Image is too large, either for the platform's pointer size or generally + ImageTooLarge, + // Frame would go out of the canvas + FrameOutsideImage, +} + +impl fmt::Display for DecoderError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + DecoderError::InfoBitsInvalid { name, value } => f.write_fmt(format_args!( + "Info bits `{}` invalid, received value: {}", + name, value + )), + DecoderError::AlphaChunkSizeMismatch => { + f.write_str("Alpha chunk doesn't match the size of the frame") + } + DecoderError::ImageTooLarge => f.write_str("Image is too large to be decoded"), + DecoderError::FrameOutsideImage => { + f.write_str("Frame is too large and would go outside the image") + } + } + } +} + +impl From for ImageError { + fn from(e: DecoderError) -> ImageError { + ImageError::Decoding(DecodingError::new(ImageFormat::WebP.into(), e)) + } +} + +impl error::Error for DecoderError {} + +#[derive(Debug, Copy, Clone)] +pub(crate) struct WebPExtendedInfo { + _icc_profile: bool, + alpha: bool, + _exif_metadata: bool, + _xmp_metadata: bool, + _animation: bool, + canvas_width: u32, + canvas_height: u32, +} + +#[derive(Debug)] +enum ExtendedImageData { + Animation { + frames: Vec, + anim_info: WebPAnimatedInfo, + }, + Static(WebPStatic), +} + +#[derive(Debug)] +pub(crate) struct ExtendedImage { + info: WebPExtendedInfo, + image: ExtendedImageData, +} + +impl ExtendedImage { + pub(crate) fn dimensions(&self) -> (u32, u32) { + (self.info.canvas_width, self.info.canvas_height) + } + + pub(crate) fn color_type(&self) -> color::ColorType { + if self.info.alpha { + color::ColorType::Rgba8 + } else { + color::ColorType::Rgb8 + } + } + + pub(crate) fn into_frames<'a>(self) -> Frames<'a> { + struct FrameIterator { + image: ExtendedImage, + index: usize, + canvas: RgbaImage, + } + + impl Iterator for FrameIterator { + type Item = ImageResult; + + fn next(&mut self) -> Option { + if let ExtendedImageData::Animation { frames, anim_info } = &self.image.image { + let frame = frames.get(self.index); + match frame { + Some(anim_image) => { + self.index += 1; + ExtendedImage::draw_subimage( + &mut self.canvas, + anim_image, + anim_info.background_color, + ) + } + None => None, + } + } else { + None + } + } + } + + let width = self.info.canvas_width; + let height = self.info.canvas_height; + let background_color = + if let ExtendedImageData::Animation { ref anim_info, .. } = self.image { + anim_info.background_color + } else { + Rgba([0, 0, 0, 0]) + }; + + let frame_iter = FrameIterator { + image: self, + index: 0, + canvas: RgbaImage::from_pixel(width, height, background_color), + }; + + Frames::new(Box::new(frame_iter)) + } + + pub(crate) fn read_extended_chunks( + reader: &mut R, + info: WebPExtendedInfo, + ) -> ImageResult { + let mut anim_info: Option = None; + let mut anim_frames: Vec = Vec::new(); + let mut static_frame: Option = None; + + //go until end of file and while chunk headers are valid + while let Some((mut cursor, chunk)) = read_chunk(reader)? { + match chunk { + WebPRiffChunk::ICCP | WebPRiffChunk::EXIF | WebPRiffChunk::XMP => { + //ignore these chunks + } + WebPRiffChunk::ANIM => { + if anim_info.is_none() { + anim_info = Some(Self::read_anim_info(&mut cursor)?); + } + } + WebPRiffChunk::ANMF => { + let frame = read_anim_frame(cursor, info.canvas_width, info.canvas_height)?; + anim_frames.push(frame); + } + WebPRiffChunk::ALPH => { + if static_frame.is_none() { + let alpha_chunk = + read_alpha_chunk(&mut cursor, info.canvas_width, info.canvas_height)?; + + let vp8_frame = read_lossy(reader)?; + + let img = WebPStatic::from_alpha_lossy(alpha_chunk, vp8_frame)?; + + static_frame = Some(img); + } + } + WebPRiffChunk::VP8 => { + if static_frame.is_none() { + let vp8_frame = read_lossy(&mut cursor)?; + + let img = WebPStatic::from_lossy(vp8_frame)?; + + static_frame = Some(img); + } + } + WebPRiffChunk::VP8L => { + if static_frame.is_none() { + let mut lossless_decoder = LosslessDecoder::new(cursor); + let frame = lossless_decoder.decode_frame()?; + let image = WebPStatic::Lossless(frame.clone()); + + static_frame = Some(image); + } + } + _ => return Err(ChunkHeaderInvalid(chunk.to_fourcc()).into()), + } + } + + let image = if let Some(info) = anim_info { + if anim_frames.len() == 0 { + return Err(ImageError::IoError(Error::from( + io::ErrorKind::UnexpectedEof, + ))); + } + ExtendedImageData::Animation { + frames: anim_frames, + anim_info: info, + } + } else if let Some(frame) = static_frame { + ExtendedImageData::Static(frame) + } else { + //reached end of file too early before image data was reached + return Err(ImageError::IoError(Error::from( + io::ErrorKind::UnexpectedEof, + ))); + }; + + let image = ExtendedImage { image, info }; + + Ok(image) + } + + fn read_anim_info(reader: &mut R) -> ImageResult { + let mut colors: [u8; 4] = [0; 4]; + reader.read_exact(&mut colors)?; + + //background color is [blue, green, red, alpha] + let background_color = Rgba([colors[2], colors[1], colors[0], colors[3]]); + + let loop_count = reader.read_u16::()?; + + let info = WebPAnimatedInfo { + background_color, + _loop_count: loop_count, + }; + + Ok(info) + } + + fn draw_subimage( + canvas: &mut RgbaImage, + anim_image: &AnimatedFrame, + background_color: Rgba, + ) -> Option> { + let mut buffer = vec![0; (anim_image.width * anim_image.height * 4) as usize]; + anim_image.image.fill_buf(&mut buffer); + + for x in 0..anim_image.width { + for y in 0..anim_image.height { + let canvas_index: (u32, u32) = (x + anim_image.offset_x, y + anim_image.offset_y); + let index: usize = (y * 4 * anim_image.width + x * 4).try_into().unwrap(); + canvas[canvas_index] = if anim_image.use_alpha_blending { + let buffer: [u8; 4] = buffer[index..][..4].try_into().unwrap(); + ExtendedImage::do_alpha_blending(buffer, canvas[canvas_index]) + } else { + Rgba([ + buffer[index], + buffer[index + 1], + buffer[index + 2], + buffer[index + 3], + ]) + }; + } + } + + let delay = Delay::from_numer_denom_ms(anim_image.duration, 1); + let img = canvas.clone(); + let frame = Frame::from_parts(img, 0, 0, delay); + + if anim_image.dispose { + for x in 0..anim_image.width { + for y in 0..anim_image.height { + let canvas_index = (x + anim_image.offset_x, y + anim_image.offset_y); + canvas[canvas_index] = background_color; + } + } + } + + Some(Ok(frame)) + } + + fn do_alpha_blending(buffer: [u8; 4], canvas: Rgba) -> Rgba { + let canvas_alpha = f64::from(canvas[3]); + let buffer_alpha = f64::from(buffer[3]); + let blend_alpha_f64 = buffer_alpha + canvas_alpha * (1.0 - buffer_alpha / 255.0); + //value should be between 0 and 255, this truncates the fractional part + let blend_alpha: u8 = blend_alpha_f64 as u8; + + let blend_rgb: [u8; 3] = if blend_alpha == 0 { + [0, 0, 0] + } else { + let mut rgb = [0u8; 3]; + for i in 0..3 { + let canvas_f64 = f64::from(canvas[i]); + let buffer_f64 = f64::from(buffer[i]); + + let val = (buffer_f64 * buffer_alpha + + canvas_f64 * canvas_alpha * (1.0 - buffer_alpha / 255.0)) + / blend_alpha_f64; + //value should be between 0 and 255, this truncates the fractional part + rgb[i] = val as u8; + } + + rgb + }; + + Rgba([blend_rgb[0], blend_rgb[1], blend_rgb[2], blend_alpha]) + } + + pub(crate) fn fill_buf(&self, buf: &mut [u8]) { + match &self.image { + ExtendedImageData::Animation { frames, .. } => { + //will always have at least one frame + frames[0].image.fill_buf(buf); + } + ExtendedImageData::Static(image) => { + image.fill_buf(buf); + } + } + } + + pub(crate) fn get_buf_size(&self) -> usize { + match &self.image { + ExtendedImageData::Animation { frames, .. } => { + //will always have at least one frame + frames[0].image.get_buf_size() + } + ExtendedImageData::Static(image) => image.get_buf_size(), + } + } +} + +#[derive(Debug)] +enum WebPStatic { + Lossy(RgbaImage), + Lossless(LosslessFrame), +} + +impl WebPStatic { + pub(crate) fn from_alpha_lossy( + alpha: AlphaChunk, + vp8_frame: VP8Frame, + ) -> ImageResult { + if alpha.data.len() != usize::from(vp8_frame.width) * usize::from(vp8_frame.height) { + return Err(DecoderError::AlphaChunkSizeMismatch.into()); + } + + let size = usize::from(vp8_frame.width).checked_mul(usize::from(vp8_frame.height) * 4); + let mut image_vec = match size { + Some(size) => vec![0u8; size], + None => return Err(DecoderError::ImageTooLarge.into()), + }; + + vp8_frame.fill_rgba(&mut image_vec); + + for y in 0..vp8_frame.height { + for x in 0..vp8_frame.width { + let predictor: u8 = WebPStatic::get_predictor( + x.into(), + y.into(), + vp8_frame.width.into(), + alpha.filtering_method, + &image_vec, + ); + let predictor = u16::from(predictor); + + let alpha_index = usize::from(y) * usize::from(vp8_frame.width) + usize::from(x); + let alpha_val = alpha.data[alpha_index]; + let alpha: u8 = ((predictor + u16::from(alpha_val)) % 256) + .try_into() + .unwrap(); + + let alpha_index = alpha_index * 4 + 3; + image_vec[alpha_index] = alpha; + } + } + + let image = RgbaImage::from_vec(vp8_frame.width.into(), vp8_frame.height.into(), image_vec) + .unwrap(); + + Ok(WebPStatic::Lossy(image)) + } + + fn get_predictor( + x: usize, + y: usize, + width: usize, + filtering_method: FilteringMethod, + image_slice: &[u8], + ) -> u8 { + match filtering_method { + FilteringMethod::None => 0, + FilteringMethod::Horizontal => { + if x == 0 && y == 0 { + 0 + } else if x == 0 { + let index = (y - 1) * width + x; + image_slice[index * 4 + 3] + } else { + let index = y * width + x - 1; + image_slice[index * 4 + 3] + } + } + FilteringMethod::Vertical => { + if x == 0 && y == 0 { + 0 + } else if y == 0 { + let index = y * width + x - 1; + image_slice[index * 4 + 3] + } else { + let index = (y - 1) * width + x; + image_slice[index * 4 + 3] + } + } + FilteringMethod::Gradient => { + let (left, top, top_left) = match (x, y) { + (0, 0) => (0, 0, 0), + (0, y) => { + let above_index = (y - 1) * width + x; + let val = image_slice[above_index * 4 + 3]; + (val, val, val) + } + (x, 0) => { + let before_index = y * width + x - 1; + let val = image_slice[before_index * 4 + 3]; + (val, val, val) + } + (x, y) => { + let left_index = y * width + x - 1; + let left = image_slice[left_index * 4 + 3]; + let top_index = (y - 1) * width + x; + let top = image_slice[top_index * 4 + 3]; + let top_left_index = (y - 1) * width + x - 1; + let top_left = image_slice[top_left_index * 4 + 3]; + + (left, top, top_left) + } + }; + + let combination = i16::from(left) + i16::from(top) - i16::from(top_left); + i16::clamp(combination, 0, 255).try_into().unwrap() + } + } + } + + pub(crate) fn from_lossy(vp8_frame: VP8Frame) -> ImageResult { + let mut image = RgbaImage::from_pixel( + vp8_frame.width.into(), + vp8_frame.height.into(), + Rgba([0, 0, 0, 255]), + ); + + vp8_frame.fill_rgba(&mut image); + + Ok(WebPStatic::Lossy(image)) + } + + pub(crate) fn fill_buf(&self, buf: &mut [u8]) { + match self { + WebPStatic::Lossy(image) => { + buf.copy_from_slice(&**image); + } + WebPStatic::Lossless(lossless) => { + lossless.fill_rgba(buf); + } + } + } + + pub(crate) fn get_buf_size(&self) -> usize { + match self { + WebPStatic::Lossy(lossy) => lossy.len(), + WebPStatic::Lossless(lossless) => lossless.get_buf_size(), + } + } +} + +#[derive(Debug)] +struct WebPAnimatedInfo { + background_color: Rgba, + _loop_count: u16, +} + +#[derive(Debug)] +struct AnimatedFrame { + offset_x: u32, + offset_y: u32, + width: u32, + height: u32, + duration: u32, + use_alpha_blending: bool, + dispose: bool, + image: WebPStatic, +} + +pub(crate) fn read_extended_header(reader: &mut R) -> ImageResult { + let chunk_flags = reader.read_u8()?; + + let reserved_first = chunk_flags & 0b11000000; + let icc_profile = chunk_flags & 0b00100000 != 0; + let alpha = chunk_flags & 0b00010000 != 0; + let exif_metadata = chunk_flags & 0b00001000 != 0; + let xmp_metadata = chunk_flags & 0b00000100 != 0; + let animation = chunk_flags & 0b00000010 != 0; + let reserved_second = chunk_flags & 0b00000001; + + let reserved_third = read_3_bytes(reader)?; + + if reserved_first != 0 || reserved_second != 0 || reserved_third != 0 { + let value: u32 = if reserved_first != 0 { + reserved_first.into() + } else if reserved_second != 0 { + reserved_second.into() + } else { + reserved_third + }; + return Err(DecoderError::InfoBitsInvalid { + name: "reserved", + value, + } + .into()); + } + + let canvas_width = read_3_bytes(reader)? + 1; + let canvas_height = read_3_bytes(reader)? + 1; + + //product of canvas dimensions cannot be larger than u32 max + if u32::checked_mul(canvas_width, canvas_height).is_none() { + return Err(DecoderError::ImageTooLarge.into()); + } + + let info = WebPExtendedInfo { + _icc_profile: icc_profile, + alpha, + _exif_metadata: exif_metadata, + _xmp_metadata: xmp_metadata, + _animation: animation, + canvas_width, + canvas_height, + }; + + Ok(info) +} + +fn read_anim_frame( + mut reader: R, + canvas_width: u32, + canvas_height: u32, +) -> ImageResult { + //offsets for the frames are twice the values + let frame_x = read_3_bytes(&mut reader)? * 2; + let frame_y = read_3_bytes(&mut reader)? * 2; + + let frame_width = read_3_bytes(&mut reader)? + 1; + let frame_height = read_3_bytes(&mut reader)? + 1; + + if frame_x + frame_width > canvas_width || frame_y + frame_height > canvas_height { + return Err(DecoderError::FrameOutsideImage.into()); + } + + let duration = read_3_bytes(&mut reader)?; + + let frame_info = reader.read_u8()?; + let reserved = frame_info & 0b11111100; + if reserved != 0 { + return Err(DecoderError::InfoBitsInvalid { + name: "reserved", + value: reserved.into(), + } + .into()); + } + let use_alpha_blending = frame_info & 0b00000010 == 0; + let dispose = frame_info & 0b00000001 != 0; + + //read normal bitstream now + let static_image = read_image(&mut reader, frame_width, frame_height)?; + + let frame = AnimatedFrame { + offset_x: frame_x, + offset_y: frame_y, + width: frame_width, + height: frame_height, + duration, + use_alpha_blending, + dispose, + image: static_image, + }; + + Ok(frame) +} + +fn read_3_bytes(reader: &mut R) -> ImageResult { + let mut buffer: [u8; 3] = [0; 3]; + reader.read_exact(&mut buffer)?; + let value: u32 = + (u32::from(buffer[2]) << 16) | (u32::from(buffer[1]) << 8) | u32::from(buffer[0]); + Ok(value) +} + +fn read_lossy(reader: &mut R) -> ImageResult { + let (cursor, chunk) = + read_chunk(reader)?.ok_or_else(|| Error::from(io::ErrorKind::UnexpectedEof))?; + + if chunk != WebPRiffChunk::VP8 { + return Err(ChunkHeaderInvalid(chunk.to_fourcc()).into()); + } + + let mut vp8_decoder = Vp8Decoder::new(cursor); + let frame = vp8_decoder.decode_frame()?; + + Ok(frame.clone()) +} + +fn read_image(reader: &mut R, width: u32, height: u32) -> ImageResult { + let chunk = read_chunk(reader)?; + + match chunk { + Some((cursor, WebPRiffChunk::VP8)) => { + let mut vp8_decoder = Vp8Decoder::new(cursor); + let frame = vp8_decoder.decode_frame()?; + + let img = WebPStatic::from_lossy(frame.clone())?; + + Ok(img) + } + Some((cursor, WebPRiffChunk::VP8L)) => { + let mut lossless_decoder = LosslessDecoder::new(cursor); + let frame = lossless_decoder.decode_frame()?; + + let img = WebPStatic::Lossless(frame.clone()); + + Ok(img) + } + Some((mut cursor, WebPRiffChunk::ALPH)) => { + let alpha_chunk = read_alpha_chunk(&mut cursor, width, height)?; + + let vp8_frame = read_lossy(reader)?; + + let img = WebPStatic::from_alpha_lossy(alpha_chunk, vp8_frame)?; + + Ok(img) + } + None => Err(ImageError::IoError(Error::from( + io::ErrorKind::UnexpectedEof, + ))), + Some((_, chunk)) => Err(ChunkHeaderInvalid(chunk.to_fourcc()).into()), + } +} + +#[derive(Debug)] +struct AlphaChunk { + _preprocessing: bool, + filtering_method: FilteringMethod, + data: Vec, +} + +#[derive(Debug, Copy, Clone)] +enum FilteringMethod { + None, + Horizontal, + Vertical, + Gradient, +} + +fn read_alpha_chunk(reader: &mut R, width: u32, height: u32) -> ImageResult { + let info_byte = reader.read_u8()?; + + let reserved = info_byte & 0b11000000; + let preprocessing = (info_byte & 0b00110000) >> 4; + let filtering = (info_byte & 0b00001100) >> 2; + let compression = info_byte & 0b00000011; + + if reserved != 0 { + return Err(DecoderError::InfoBitsInvalid { + name: "reserved", + value: reserved.into(), + } + .into()); + } + + let preprocessing = match preprocessing { + 0 => false, + 1 => true, + _ => { + return Err(DecoderError::InfoBitsInvalid { + name: "reserved", + value: preprocessing.into(), + } + .into()) + } + }; + + let filtering_method = match filtering { + 0 => FilteringMethod::None, + 1 => FilteringMethod::Horizontal, + 2 => FilteringMethod::Vertical, + 3 => FilteringMethod::Gradient, + _ => unreachable!(), + }; + + let lossless_compression = match compression { + 0 => false, + 1 => true, + _ => { + return Err(DecoderError::InfoBitsInvalid { + name: "lossless compression", + value: compression.into(), + } + .into()) + } + }; + + let mut framedata = Vec::new(); + reader.read_to_end(&mut framedata)?; + + let data = if lossless_compression { + let cursor = io::Cursor::new(framedata); + + let mut decoder = LosslessDecoder::new(cursor); + //this is a potential problem for large images; would require rewriting lossless decoder to use u32 for width and height + let width: u16 = width + .try_into() + .map_err(|_| ImageError::from(DecoderError::ImageTooLarge))?; + let height: u16 = height + .try_into() + .map_err(|_| ImageError::from(DecoderError::ImageTooLarge))?; + let frame = decoder.decode_frame_implicit_dims(width, height)?; + + let mut data = vec![0u8; usize::from(width) * usize::from(height)]; + + frame.fill_green(&mut data); + + data + } else { + framedata + }; + + let chunk = AlphaChunk { + _preprocessing: preprocessing, + filtering_method, + data, + }; + + Ok(chunk) +} diff --git a/src/codecs/webp/lossless.rs b/src/codecs/webp/lossless.rs index 5b74926fcf..96362d4101 100644 --- a/src/codecs/webp/lossless.rs +++ b/src/codecs/webp/lossless.rs @@ -168,6 +168,31 @@ impl LosslessDecoder { Ok(&self.frame) } + //used for alpha data in extended decoding + pub(crate) fn decode_frame_implicit_dims( + &mut self, + width: u16, + height: u16, + ) -> ImageResult<&LosslessFrame> { + let mut buf = Vec::new(); + self.r.read_to_end(&mut buf)?; + self.bit_reader.init(buf); + + self.frame.width = width; + self.frame.height = height; + + let mut data = self.decode_image_stream(self.frame.width, self.frame.height, true)?; + + //transform_order is vector of indices(0-3) into transforms in order decoded + for &trans_index in self.transform_order.iter().rev() { + let trans = self.transforms[usize::from(trans_index)].as_ref().unwrap(); + trans.apply_transform(&mut data, self.frame.width, self.frame.height); + } + + self.frame.buf = data; + Ok(&self.frame) + } + /// Reads Image data from the bitstream /// Can be in any of the 5 roles described in the Specification /// ARGB Image role has different behaviour to the other 4 @@ -693,7 +718,7 @@ pub(crate) struct LosslessFrame { pub(crate) width: u16, pub(crate) height: u16, - buf: Vec, + pub(crate) buf: Vec, } impl LosslessFrame { @@ -711,6 +736,14 @@ impl LosslessFrame { pub(crate) fn get_buf_size(&self) -> usize { usize::from(self.width) * usize::from(self.height) * 4 } + + /// Fills a buffer with just the green values from the lossless decoding + /// Used in extended alpha decoding + pub(crate) fn fill_green(&self, buf: &mut [u8]) { + for (&argb_val, buf_value) in self.buf.iter().zip(buf.iter_mut()) { + *buf_value = ((argb_val >> 8) & 0xff).try_into().unwrap(); + } + } } #[cfg(test)] diff --git a/src/codecs/webp/mod.rs b/src/codecs/webp/mod.rs index ecc2c810df..99b94a4f37 100644 --- a/src/codecs/webp/mod.rs +++ b/src/codecs/webp/mod.rs @@ -10,4 +10,6 @@ mod huffman; mod lossless; mod lossless_transform; +mod extended; + pub mod vp8; diff --git a/src/codecs/webp/vp8.rs b/src/codecs/webp/vp8.rs index f828765c19..334293b99f 100644 --- a/src/codecs/webp/vp8.rs +++ b/src/codecs/webp/vp8.rs @@ -891,28 +891,59 @@ impl Frame { (self.height + 1) / 2 } - /// Conversion values from https://docs.microsoft.com/en-us/windows/win32/medfound/recommended-8-bit-yuv-formats-for-video-rendering#converting-8-bit-yuv-to-rgb888 - pub fn fill_rgb(&self, buf: &mut [u8]) { - for index in 0..self.ybuf.len() { + /// Fills an rgb buffer with the image + pub(crate) fn fill_rgb(&self, buf: &mut [u8]) { + for (index, rgb_chunk) in (0..self.ybuf.len()).zip(buf.chunks_exact_mut(3)) { let y = index / self.width as usize; let x = index % self.width as usize; let chroma_index = self.chroma_width() as usize * (y / 2) + x / 2; - let rgb_index = index * 3; - let c = self.ybuf[index] as i32 - 16; - let d = self.ubuf[chroma_index] as i32 - 128; - let e = self.vbuf[chroma_index] as i32 - 128; + Frame::fill_single( + self.ybuf[index], + self.ubuf[chroma_index], + self.vbuf[chroma_index], + rgb_chunk, + ); + } + } - let r = clamp((298 * c + 409 * e + 128) >> 8, 0, 255) as u8; - let g = clamp((298 * c - 100 * d - 208 * e + 128) >> 8, 0, 255) as u8; - let b = clamp((298 * c + 516 * d + 128) >> 8, 0, 255) as u8; + /// Fills an rgba buffer by skipping the alpha values + pub(crate) fn fill_rgba(&self, buf: &mut [u8]) { + for (index, rgba_chunk) in (0..self.ybuf.len()).zip(buf.chunks_exact_mut(4)) { + let y = index / self.width as usize; + let x = index % self.width as usize; + let chroma_index = self.chroma_width() as usize * (y / 2) + x / 2; - buf[rgb_index] = r; - buf[rgb_index + 1] = g; - buf[rgb_index + 2] = b; + Frame::fill_single( + self.ybuf[index], + self.ubuf[chroma_index], + self.vbuf[chroma_index], + rgba_chunk, + ); } } + /// Conversion values from https://docs.microsoft.com/en-us/windows/win32/medfound/recommended-8-bit-yuv-formats-for-video-rendering#converting-8-bit-yuv-to-rgb888 + fn fill_single(y: u8, u: u8, v: u8, rgb: &mut [u8]) { + let c: i32 = i32::from(y) - 16; + let d: i32 = i32::from(u) - 128; + let e: i32 = i32::from(v) - 128; + + let r: u8 = clamp((298 * c + 409 * e + 128) >> 8, 0, 255) + .try_into() + .unwrap(); + let g: u8 = clamp((298 * c - 100 * d - 208 * e + 128) >> 8, 0, 255) + .try_into() + .unwrap(); + let b: u8 = clamp((298 * c + 516 * d + 128) >> 8, 0, 255) + .try_into() + .unwrap(); + + rgb[0] = r; + rgb[1] = g; + rgb[2] = b; + } + /// Gets the buffer size pub fn get_buf_size(&self) -> usize { self.ybuf.len() * 3 diff --git a/src/lib.rs b/src/lib.rs index eac358294e..f2d99c286a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -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 | Lossy(Luma channel only) | No | +/// | WebP | Yes | No | /// | AVIF | Only 8-bit | Lossy | /// | PNM | PBM, PGM, PPM, standard PAM | Yes | /// | DDS | DXT1, DXT3, DXT5 | No | diff --git a/tests/images/webp/extended_images/anim.webp b/tests/images/webp/extended_images/anim.webp new file mode 100644 index 0000000000..b65e6f7558 Binary files /dev/null and b/tests/images/webp/extended_images/anim.webp differ diff --git a/tests/images/webp/extended_images/lossy_alpha.webp b/tests/images/webp/extended_images/lossy_alpha.webp new file mode 100644 index 0000000000..32126794a2 Binary files /dev/null and b/tests/images/webp/extended_images/lossy_alpha.webp differ diff --git a/tests/images/webp/lossy_images/vp8x-gray.webp b/tests/images/webp/lossy_images/vp8x-gray.webp deleted file mode 100644 index d38163fa3d..0000000000 Binary files a/tests/images/webp/lossy_images/vp8x-gray.webp and /dev/null differ diff --git a/tests/images/webp/lossy_images/vp8x-rgb.webp b/tests/images/webp/lossy_images/vp8x-rgb.webp deleted file mode 100644 index 9d866d1a8d..0000000000 Binary files a/tests/images/webp/lossy_images/vp8x-rgb.webp and /dev/null differ diff --git a/tests/reference/webp/extended_images/anim.webp.f6449d24.png b/tests/reference/webp/extended_images/anim.webp.f6449d24.png new file mode 100644 index 0000000000..daf1dc0fd7 Binary files /dev/null and b/tests/reference/webp/extended_images/anim.webp.f6449d24.png differ diff --git a/tests/reference/webp/extended_images/lossy_alpha.webp.37efcff9.png b/tests/reference/webp/extended_images/lossy_alpha.webp.37efcff9.png new file mode 100644 index 0000000000..be5d085a56 Binary files /dev/null and b/tests/reference/webp/extended_images/lossy_alpha.webp.37efcff9.png differ diff --git a/tests/reference/webp/lossy_images/vp8x-gray.webp.6b294f50.png b/tests/reference/webp/lossy_images/vp8x-gray.webp.6b294f50.png deleted file mode 100644 index d87582eed0..0000000000 Binary files a/tests/reference/webp/lossy_images/vp8x-gray.webp.6b294f50.png and /dev/null differ diff --git a/tests/reference/webp/lossy_images/vp8x-rgb.webp.4bb22be5.png b/tests/reference/webp/lossy_images/vp8x-rgb.webp.4bb22be5.png deleted file mode 100644 index 894383144a..0000000000 Binary files a/tests/reference/webp/lossy_images/vp8x-rgb.webp.4bb22be5.png and /dev/null differ