diff --git a/src/image.rs b/src/image.rs index 9371fb99..03668f86 100644 --- a/src/image.rs +++ b/src/image.rs @@ -356,19 +356,25 @@ mod tests { } assert_eq!( - case("foo.png", &png_with_exif(2, 1, &exif(5))) - .unwrap() - .dimensions, + case( + "foo.png", + &PngBuilder::new().width(2).height(3).exif(&exif(5)).build(), + ) + .unwrap() + .dimensions, Dimensions { - height: 1, + height: 3, width: 2, }, ); assert_eq!( - case("foo.jpg", &jpeg_with_exif(1, 2, &exif(5))) - .unwrap() - .dimensions, + case( + "foo.jpg", + &JpegBuilder::new().height(2).exif(&exif(5)).build(), + ) + .unwrap() + .dimensions, Dimensions { height: 2, width: 1, @@ -376,7 +382,11 @@ mod tests { ); assert_eq!( - case("foo.jpg", &jpeg_with_exif(2, 1, &exif(6))).unwrap(), + case( + "foo.jpg", + &JpegBuilder::new().width(2).exif(&exif(6)).build(), + ) + .unwrap(), Image { alpha: false, bit_depth: 8, @@ -396,7 +406,11 @@ mod tests { ); assert_eq!( - case("foo.png", &png_with_exif(2, 1, &exif(5))).unwrap(), + case( + "foo.png", + &PngBuilder::new().width(2).exif(&exif(5)).build(), + ) + .unwrap(), Image { alpha: false, bit_depth: 8, @@ -415,21 +429,21 @@ mod tests { }, ); - let image = case("foo.jpg", &jpeg_grayscale(1, 1)).unwrap(); + let image = case("foo.jpg", &JpegBuilder::new().grayscale().build()).unwrap(); assert!(!image.alpha); assert_eq!(image.bit_depth, 8); assert_eq!(image.chroma_subsampling, Some(ChromaSubsampling::Yuv400)); assert_eq!(image.color_type, ColorType::Grayscale); assert_eq!( - case("foo.jpg", &jpeg_with_sampling(1, 1, 0x22)) + case("foo.jpg", &JpegBuilder::new().sampling(0x22).build()) .unwrap() .chroma_subsampling, Some(ChromaSubsampling::Yuv420), ); assert_matches_regex!( - case("foo.jpg", &jpeg_with_sampling(1, 1, 0x41)) + case("foo.jpg", &JpegBuilder::new().sampling(0x41).build()) .unwrap_err() .to_string(), r"^unsupported chroma subsampling 4×1 in image `.*foo\.jpg`$", @@ -437,14 +451,10 @@ mod tests { let image = case( "foo.png", - &png( - 1, - 1, - png::ColorType::Rgba, - png::BitDepth::Sixteen, - None, - None, - ), + &PngBuilder::new() + .color(png::ColorType::Rgba) + .depth(png::BitDepth::Sixteen) + .build(), ) .unwrap(); assert!(image.alpha); @@ -454,14 +464,10 @@ mod tests { let image = case( "foo.png", - &png( - 1, - 1, - png::ColorType::Indexed, - png::BitDepth::One, - None, - None, - ), + &PngBuilder::new() + .color(png::ColorType::Indexed) + .depth(png::BitDepth::One) + .build(), ) .unwrap(); assert!(!image.alpha); @@ -471,14 +477,11 @@ mod tests { assert!( case( "foo.png", - &png( - 1, - 1, - png::ColorType::Indexed, - png::BitDepth::One, - Some(&[0]), - None, - ), + &PngBuilder::new() + .color(png::ColorType::Indexed) + .depth(png::BitDepth::One) + .trns(&[0]) + .build(), ) .unwrap() .alpha @@ -486,14 +489,9 @@ mod tests { let image = case( "foo.png", - &png( - 1, - 1, - png::ColorType::GrayscaleAlpha, - png::BitDepth::Eight, - None, - None, - ), + &PngBuilder::new() + .color(png::ColorType::GrayscaleAlpha) + .build(), ) .unwrap(); assert!(image.alpha); @@ -501,14 +499,10 @@ mod tests { let image = case( "foo.png", - &png( - 1, - 1, - png::ColorType::Grayscale, - png::BitDepth::Two, - None, - None, - ), + &PngBuilder::new() + .color(png::ColorType::Grayscale) + .depth(png::BitDepth::Two) + .build(), ) .unwrap(); assert!(!image.alpha); @@ -526,14 +520,14 @@ mod tests { ); assert_matches_regex!( - case("foo.jpg", &jpeg_with_exif(2, 1, b"foo")) + case("foo.jpg", &JpegBuilder::new().width(2).exif(b"foo").build()) .unwrap_err() .to_string(), r"^invalid EXIF in image `.*foo\.jpg`$", ); assert_matches_regex!( - case("foo.png", &png_with_exif(2, 1, b"foo")) + case("foo.png", &PngBuilder::new().width(2).exif(b"foo").build()) .unwrap_err() .to_string(), r"^invalid EXIF in image `.*foo\.png`$", diff --git a/src/jpeg_builder.rs b/src/jpeg_builder.rs new file mode 100644 index 00000000..79378e6e --- /dev/null +++ b/src/jpeg_builder.rs @@ -0,0 +1,87 @@ +use { + super::*, + ::image::{DynamicImage, ImageFormat}, +}; + +pub(crate) struct JpegBuilder { + exif: Option>, + grayscale: bool, + height: u32, + sampling: Option, + width: u32, +} + +impl JpegBuilder { + pub(crate) fn build(self) -> Vec { + let image = if self.grayscale { + DynamicImage::new_luma8(self.width, self.height) + } else { + DynamicImage::new_rgb8(self.width, self.height) + }; + + let mut buffer = io::Cursor::new(Vec::new()); + + image.write_to(&mut buffer, ImageFormat::Jpeg).unwrap(); + + let mut bytes = buffer.into_inner(); + + if let Some(sampling) = self.sampling { + let sof = bytes.windows(2).position(|w| w == [0xFF, 0xC0]).unwrap(); + bytes[sof + 11] = sampling; + } + + if let Some(exif) = self.exif { + let mut app1 = b"Exif\0\0".to_vec(); + app1.extend_from_slice(&exif); + + let mut spliced = bytes[..2].to_vec(); + spliced.extend_from_slice(&[0xFF, 0xE1]); + spliced.extend_from_slice(&u16::try_from(app1.len() + 2).unwrap().to_be_bytes()); + spliced.extend_from_slice(&app1); + spliced.extend_from_slice(&bytes[2..]); + spliced + } else { + bytes + } + } + + #[must_use] + pub(crate) fn exif(mut self, exif: &[u8]) -> Self { + self.exif = Some(exif.into()); + self + } + + #[must_use] + pub(crate) fn grayscale(mut self) -> Self { + self.grayscale = true; + self + } + + #[must_use] + pub(crate) fn height(mut self, height: u32) -> Self { + self.height = height; + self + } + + pub(crate) fn new() -> Self { + Self { + exif: None, + grayscale: false, + height: 1, + sampling: None, + width: 1, + } + } + + #[must_use] + pub(crate) fn sampling(mut self, sampling: u8) -> Self { + self.sampling = Some(sampling); + self + } + + #[must_use] + pub(crate) fn width(mut self, width: u32) -> Self { + self.width = width; + self + } +} diff --git a/src/lib.rs b/src/lib.rs index 3978efac..57d832f4 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -245,12 +245,11 @@ pub use self::{ #[cfg(test)] use { + jpeg_builder::JpegBuilder, + png_builder::PngBuilder, std::assert_matches, tempfile::TempDir, - test::{ - assert_cbor, assert_cbor_eq, assert_encoding, exif, flac, jpeg_grayscale, jpeg_with_exif, - jpeg_with_sampling, mp3, mp3_frame, png, png_with_exif, tempdir, - }, + test::{assert_cbor, assert_cbor_eq, assert_encoding, exif, flac, mp3, mp3_frame, tempdir}, unindent::unindent, webm_builder::WebmBuilder, }; @@ -420,6 +419,10 @@ mod webm_decoder; #[cfg(test)] mod derive; #[cfg(test)] +mod jpeg_builder; +#[cfg(test)] +mod png_builder; +#[cfg(test)] mod test; #[cfg(test)] mod webm_builder; diff --git a/src/png_builder.rs b/src/png_builder.rs new file mode 100644 index 00000000..15d4127a --- /dev/null +++ b/src/png_builder.rs @@ -0,0 +1,91 @@ +use png::{BitDepth, chunk::eXIf}; + +pub(crate) struct PngBuilder { + color: png::ColorType, + depth: BitDepth, + exif: Option>, + height: u32, + trns: Option>, + width: u32, +} + +impl PngBuilder { + pub(crate) fn build(self) -> Vec { + let mut buffer = Vec::new(); + + let mut encoder = png::Encoder::new(&mut buffer, self.width, self.height); + encoder.set_color(self.color); + encoder.set_depth(self.depth); + + if self.color == png::ColorType::Indexed { + encoder.set_palette(vec![0; 3]); + } + + if let Some(trns) = self.trns { + encoder.set_trns(trns); + } + + let mut writer = encoder.write_header().unwrap(); + + if let Some(exif) = self.exif { + writer.write_chunk(eXIf, &exif).unwrap(); + } + + let samples = u32::try_from(self.color.samples()).unwrap(); + let row = (self.width * samples * u32::from(self.depth as u8)).div_ceil(8); + + writer + .write_image_data(&vec![0; usize::try_from(row * self.height).unwrap()]) + .unwrap(); + writer.finish().unwrap(); + + buffer + } + + #[must_use] + pub(crate) fn color(mut self, color: png::ColorType) -> Self { + self.color = color; + self + } + + #[must_use] + pub(crate) fn depth(mut self, depth: BitDepth) -> Self { + self.depth = depth; + self + } + + #[must_use] + pub(crate) fn exif(mut self, exif: &[u8]) -> Self { + self.exif = Some(exif.into()); + self + } + + #[must_use] + pub(crate) fn height(mut self, height: u32) -> Self { + self.height = height; + self + } + + pub(crate) fn new() -> Self { + Self { + color: png::ColorType::Rgb, + depth: BitDepth::Eight, + exif: None, + height: 1, + trns: None, + width: 1, + } + } + + #[must_use] + pub(crate) fn trns(mut self, trns: &[u8]) -> Self { + self.trns = Some(trns.into()); + self + } + + #[must_use] + pub(crate) fn width(mut self, width: u32) -> Self { + self.width = width; + self + } +} diff --git a/src/test.rs b/src/test.rs index 7245f261..dc7c19c5 100644 --- a/src/test.rs +++ b/src/test.rs @@ -132,43 +132,6 @@ pub(crate) fn flac(comments: &[&str], samples: u32) -> Vec { bytes } -pub(crate) fn jpeg(width: u32, height: u32) -> Vec { - let mut buffer = io::Cursor::new(Vec::new()); - ::image::DynamicImage::new_rgb8(width, height) - .write_to(&mut buffer, ::image::ImageFormat::Jpeg) - .unwrap(); - buffer.into_inner() -} - -pub(crate) fn jpeg_grayscale(width: u32, height: u32) -> Vec { - let mut buffer = io::Cursor::new(Vec::new()); - ::image::DynamicImage::new_luma8(width, height) - .write_to(&mut buffer, ::image::ImageFormat::Jpeg) - .unwrap(); - buffer.into_inner() -} - -pub(crate) fn jpeg_with_exif(width: u32, height: u32, exif: &[u8]) -> Vec { - let buffer = jpeg(width, height); - - let mut app1 = b"Exif\0\0".to_vec(); - app1.extend_from_slice(exif); - - let mut spliced = buffer[..2].to_vec(); - spliced.extend_from_slice(&[0xFF, 0xE1]); - spliced.extend_from_slice(&u16::try_from(app1.len() + 2).unwrap().to_be_bytes()); - spliced.extend_from_slice(&app1); - spliced.extend_from_slice(&buffer[2..]); - spliced -} - -pub(crate) fn jpeg_with_sampling(width: u32, height: u32, sampling: u8) -> Vec { - let mut bytes = jpeg(width, height); - let sof = bytes.windows(2).position(|w| w == [0xFF, 0xC0]).unwrap(); - bytes[sof + 11] = sampling; - bytes -} - pub(crate) fn mp3(tags: &[&str], frames: u32) -> Vec { fn syncsafe(n: usize) -> [u8; 4] { let n = u32::try_from(n).unwrap(); @@ -209,56 +172,6 @@ pub(crate) fn mp3_frame() -> Vec { bytes } -pub(crate) fn png( - width: u32, - height: u32, - color_type: png::ColorType, - bit_depth: png::BitDepth, - trns: Option<&[u8]>, - exif: Option<&[u8]>, -) -> Vec { - let mut buffer = Vec::new(); - - let mut encoder = png::Encoder::new(&mut buffer, width, height); - encoder.set_color(color_type); - encoder.set_depth(bit_depth); - - if color_type == png::ColorType::Indexed { - encoder.set_palette(vec![0; 3]); - } - - if let Some(trns) = trns { - encoder.set_trns(trns.to_vec()); - } - - let mut writer = encoder.write_header().unwrap(); - - if let Some(exif) = exif { - writer.write_chunk(png::chunk::eXIf, exif).unwrap(); - } - - let samples = u32::try_from(color_type.samples()).unwrap(); - let row = (width * samples * u32::from(bit_depth as u8)).div_ceil(8); - - writer - .write_image_data(&vec![0; usize::try_from(row * height).unwrap()]) - .unwrap(); - writer.finish().unwrap(); - - buffer -} - -pub(crate) fn png_with_exif(width: u32, height: u32, exif: &[u8]) -> Vec { - png( - width, - height, - png::ColorType::Rgb, - png::BitDepth::Eight, - None, - Some(exif), - ) -} - pub(crate) fn tempdir() -> (TempDir, Utf8PathBuf) { let tempdir = tempfile::Builder::new() .prefix("filepack-test-tempdir")