diff --git a/CHANGES.md b/CHANGES.md index 5d72ad0b06..b66b25f6b2 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -6,6 +6,8 @@ - The color space information of pixels is not clearly communicated. ## Changes +- Added Float32 RGB and RGBA image encoding/decoding support for TIFF. +- Implemented serialization/deserialization for DynamicImage using PNG for uint images, and TIFF for float32 images. The TIFF images are further compressed using Zlib::Deflate to reduce size. The PNG or TIFF-encoded images are stored internally as base64 encoded strings. ### Version 0.25.1 diff --git a/Cargo.toml b/Cargo.toml index db327d74fe..78d92c5628 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,6 +4,7 @@ version = "0.25.1" edition = "2021" resolver = "2" + # note: when changed, also update test runner in `.github/workflows/rust.yml` rust-version = "1.67.1" @@ -55,6 +56,9 @@ rgb = { version = "0.8.25", optional = true } tiff = { version = "0.9.0", optional = true } zune-core = { version = "0.4.11", default-features = false, optional = true } zune-jpeg = { version = "0.4.11", optional = true } +base64 = "0.22" +serde = "1.0" +flate2 = "1.0.30" [dev-dependencies] crc32fast = "1.2.0" @@ -62,6 +66,7 @@ num-complex = "0.4" glob = "0.3" quickcheck = "1" criterion = "0.5.0" +serde_json = "1.0" [features] default = ["rayon", "default-formats"] diff --git a/README.md b/README.md index 924a165bcf..57d99071b5 100644 --- a/README.md +++ b/README.md @@ -221,3 +221,20 @@ fn main() { image::save_buffer("image.png", buffer, 800, 600, image::ExtendedColorType::Rgb8).unwrap() } ``` + +### Serialization and Deserialization +A `DynamicImage` object can be serialized and deserialized: + +```rust,no_run +fn main() { + use image::DynamicImage; + let path = "/path/to/image"; + let img = image::open(&path).expect("Could not find image in path."); + let img_string = serde_json::to_string(&img).expect("Could not serialize."); + // Send the image + + // Receive an image + let recv_json: String = todo!(); + let recv_img = serde_json::from_str::(&recv_json).expect("Could not deserialize"); +} +``` diff --git a/src/codecs/tiff.rs b/src/codecs/tiff.rs index 9f4dd7358e..ed197ad870 100644 --- a/src/codecs/tiff.rs +++ b/src/codecs/tiff.rs @@ -12,6 +12,10 @@ use std::io::{self, BufRead, Cursor, Read, Seek, Write}; use std::marker::PhantomData; use std::mem; +use tiff::decoder::{Decoder, DecodingResult}; +use tiff::encoder::compression::{Compressor, Deflate, Lzw, Packbits, Uncompressed}; +use tiff::tags::Tag; + use crate::color::{ColorType, ExtendedColorType}; use crate::error::{ DecodingError, EncodingError, ImageError, ImageResult, LimitError, LimitErrorKind, @@ -29,7 +33,7 @@ where original_color_type: ExtendedColorType, // We only use an Option here so we can call with_limits on the decoder without moving. - inner: Option>, + inner: Option>, } impl TiffDecoder @@ -38,14 +42,14 @@ where { /// Create a new TiffDecoder. pub fn new(r: R) -> Result, ImageError> { - let mut inner = tiff::decoder::Decoder::new(r).map_err(ImageError::from_tiff_decode)?; + let mut inner = Decoder::new(r).map_err(ImageError::from_tiff_decode)?; let dimensions = inner.dimensions().map_err(ImageError::from_tiff_decode)?; let tiff_color_type = inner.colortype().map_err(ImageError::from_tiff_decode)?; - match inner.find_tag_unsigned_vec::(tiff::tags::Tag::SampleFormat) { + match inner.find_tag_unsigned_vec::(Tag::SampleFormat) { Ok(Some(sample_formats)) => { for format in sample_formats { - check_sample_format(format)?; + check_sample_format(format, tiff_color_type)?; } } Ok(None) => { /* assume UInt format */ } @@ -62,6 +66,8 @@ where tiff::ColorType::RGBA(8) => ColorType::Rgba8, tiff::ColorType::RGBA(16) => ColorType::Rgba16, tiff::ColorType::CMYK(8) => ColorType::Rgb8, + tiff::ColorType::RGB(32) => ColorType::Rgb32F, + tiff::ColorType::RGBA(32) => ColorType::Rgba32F, tiff::ColorType::Palette(n) | tiff::ColorType::Gray(n) => { return Err(err_unknown_color_type(n)) @@ -100,18 +106,44 @@ where } } -fn check_sample_format(sample_format: u16) -> Result<(), ImageError> { - match tiff::tags::SampleFormat::from_u16(sample_format) { - Some(tiff::tags::SampleFormat::Uint) => Ok(()), - Some(other) => Err(ImageError::Unsupported( - UnsupportedError::from_format_and_kind( - ImageFormat::Tiff.into(), - UnsupportedErrorKind::GenericFeature(format!( - "Unhandled TIFF sample format {:?}", - other - )), - ), - )), +fn check_sample_format(sample_format: u16, color_type: tiff::ColorType) -> Result<(), ImageError> { + use tiff::{tags::SampleFormat, ColorType}; + let num_bits = match color_type { + ColorType::CMYK(k) => k, + ColorType::Gray(k) => k, + ColorType::RGB(k) => k, + ColorType::RGBA(k) => k, + ColorType::GrayA(k) => k, + ColorType::Palette(k) | ColorType::YCbCr(k) => { + return Err(ImageError::Unsupported( + UnsupportedError::from_format_and_kind( + ImageFormat::Tiff.into(), + UnsupportedErrorKind::GenericFeature(format!( + "Unhandled TIFF color type {:?} for {} bits", + color_type, k + )), + ), + )) + } + }; + match SampleFormat::from_u16(sample_format) { + Some(format) => { + if (format == SampleFormat::Uint && num_bits <= 16) + || (format == SampleFormat::IEEEFP && num_bits == 32) + { + Ok(()) + } else { + Err(ImageError::Unsupported( + UnsupportedError::from_format_and_kind( + ImageFormat::Tiff.into(), + UnsupportedErrorKind::GenericFeature(format!( + "Unhandled TIFF sample format {:?} for {} bits", + format, num_bits + )), + ), + )) + } + } None => Err(ImageError::Decoding(DecodingError::from_format_hint( ImageFormat::Tiff.into(), ))), @@ -198,7 +230,7 @@ impl ImageDecoder for TiffDecoder { fn icc_profile(&mut self) -> ImageResult>> { if let Some(decoder) = &mut self.inner { - Ok(decoder.get_tag_u8_vec(tiff::tags::Tag::Unknown(34675)).ok()) + Ok(decoder.get_tag_u8_vec(Tag::Unknown(34675)).ok()) } else { Ok(None) } @@ -232,42 +264,40 @@ impl ImageDecoder for TiffDecoder { .read_image() .map_err(ImageError::from_tiff_decode)? { - tiff::decoder::DecodingResult::U8(v) - if self.original_color_type == ExtendedColorType::Cmyk8 => - { + DecodingResult::U8(v) if self.original_color_type == ExtendedColorType::Cmyk8 => { let mut out_cur = Cursor::new(buf); for cmyk in v.chunks_exact(4) { out_cur.write_all(&cmyk_to_rgb(cmyk))?; } } - tiff::decoder::DecodingResult::U8(v) => { + DecodingResult::U8(v) => { buf.copy_from_slice(&v); } - tiff::decoder::DecodingResult::U16(v) => { + DecodingResult::U16(v) => { buf.copy_from_slice(bytemuck::cast_slice(&v)); } - tiff::decoder::DecodingResult::U32(v) => { + DecodingResult::U32(v) => { buf.copy_from_slice(bytemuck::cast_slice(&v)); } - tiff::decoder::DecodingResult::U64(v) => { + DecodingResult::U64(v) => { buf.copy_from_slice(bytemuck::cast_slice(&v)); } - tiff::decoder::DecodingResult::I8(v) => { + DecodingResult::I8(v) => { buf.copy_from_slice(bytemuck::cast_slice(&v)); } - tiff::decoder::DecodingResult::I16(v) => { + DecodingResult::I16(v) => { buf.copy_from_slice(bytemuck::cast_slice(&v)); } - tiff::decoder::DecodingResult::I32(v) => { + DecodingResult::I32(v) => { buf.copy_from_slice(bytemuck::cast_slice(&v)); } - tiff::decoder::DecodingResult::I64(v) => { + DecodingResult::I64(v) => { buf.copy_from_slice(bytemuck::cast_slice(&v)); } - tiff::decoder::DecodingResult::F32(v) => { + DecodingResult::F32(v) => { buf.copy_from_slice(bytemuck::cast_slice(&v)); } - tiff::decoder::DecodingResult::F64(v) => { + DecodingResult::F64(v) => { buf.copy_from_slice(bytemuck::cast_slice(&v)); } } @@ -282,6 +312,7 @@ impl ImageDecoder for TiffDecoder { /// Encoder for tiff images pub struct TiffEncoder { w: W, + comp: Compressor, } fn cmyk_to_rgb(cmyk: &[u8]) -> [u8; 3] { @@ -296,24 +327,85 @@ fn cmyk_to_rgb(cmyk: &[u8]) -> [u8; 3] { ] } -// Utility to simplify and deduplicate error handling during 16-bit encoding. -fn u8_slice_as_u16(buf: &[u8]) -> ImageResult<&[u16]> { - bytemuck::try_cast_slice(buf).map_err(|err| { - // If the buffer is not aligned or the correct length for a u16 slice, err. - // - // `bytemuck::PodCastError` of bytemuck-1.2.0 does not implement - // `Error` and `Display` trait. - // See . - ImageError::Parameter(ParameterError::from_kind(ParameterErrorKind::Generic( - format!("{:?}", err), - ))) - }) +enum DtypeContainer<'a, T> { + Slice(&'a [T]), + Vec(Vec), +} + +impl DtypeContainer<'_, T> { + fn as_slice(&self) -> &[T] { + match self { + DtypeContainer::Slice(slice) => slice, + DtypeContainer::Vec(vec) => vec, + } + } +} + +fn u8_slice_as_f32(buf: &[u8]) -> ImageResult> { + let res = bytemuck::try_cast_slice(buf); + match res { + Ok(slc) => Ok(DtypeContainer::::Slice(slc)), + Err(err) => { + match err { + bytemuck::PodCastError::TargetAlignmentGreaterAndInputNotAligned => { + // If the buffer is not aligned for a f32 slice, copy the buffer into a new Vec + let mut vec = vec![0.0; buf.len() / 4]; + for (i, chunk) in buf.chunks_exact(4).enumerate() { + let f32_val = f32::from_ne_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]); + vec[i] = f32_val; + } + Ok(DtypeContainer::Vec(vec)) + } + _ => { + // If the buffer is not the correct length for a f32 slice, err. + Err(ImageError::Parameter(ParameterError::from_kind( + ParameterErrorKind::Generic(format!("{:?}", err)), + ))) + } + } + } + } +} + +fn u8_slice_as_u16(buf: &[u8]) -> ImageResult> { + let res = bytemuck::try_cast_slice(buf); + match res { + Ok(slc) => Ok(DtypeContainer::::Slice(slc)), + Err(err) => { + match err { + bytemuck::PodCastError::TargetAlignmentGreaterAndInputNotAligned => { + // If the buffer is not aligned for a f32 slice, copy the buffer into a new Vec + let mut vec = vec![0; buf.len() / 2]; + for (i, chunk) in buf.chunks_exact(2).enumerate() { + let u16_val = u16::from_ne_bytes([chunk[0], chunk[1]]); + vec[i] = u16_val; + } + Ok(DtypeContainer::Vec(vec)) + } + _ => { + // If the buffer is not the correct length for a f32 slice, err. + Err(ImageError::Parameter(ParameterError::from_kind( + ParameterErrorKind::Generic(format!("{:?}", err)), + ))) + } + } + } + } } impl TiffEncoder { /// Create a new encoder that writes its output to `w` pub fn new(w: W) -> TiffEncoder { - TiffEncoder { w } + TiffEncoder { + w, + comp: Compressor::Deflate(Deflate::default()), + } + } + + /// Set the image compression setting + pub fn with_compression(mut self, comp: Compressor) -> Self { + self.comp = comp; + self } /// Encodes the image `image` that has dimensions `width` and `height` and `ColorType` `c`. @@ -331,6 +423,9 @@ impl TiffEncoder { height: u32, color_type: ExtendedColorType, ) -> ImageResult<()> { + use tiff::encoder::colortype::{ + Gray16, Gray8, RGB32Float, RGBA32Float, RGB16, RGB8, RGBA16, RGBA8, + }; let expected_buffer_len = color_type.buffer_size(width, height); assert_eq!( expected_buffer_len, @@ -338,44 +433,233 @@ impl TiffEncoder { "Invalid buffer length: expected {expected_buffer_len} got {} for {width}x{height} image", buf.len(), ); - let mut encoder = tiff::encoder::TiffEncoder::new(self.w).map_err(ImageError::from_tiff_encode)?; - match color_type { - ExtendedColorType::L8 => { - encoder.write_image::(width, height, buf) + match self.comp { + Compressor::Uncompressed(comp) => { + match color_type { + ExtendedColorType::L8 => encoder + .write_image_with_compression::( + width, height, comp, buf, + ), + ExtendedColorType::Rgb8 => encoder + .write_image_with_compression::( + width, height, comp, buf, + ), + ExtendedColorType::Rgba8 => encoder + .write_image_with_compression::( + width, height, comp, buf, + ), + ExtendedColorType::L16 => encoder + .write_image_with_compression::( + width, + height, + comp, + u8_slice_as_u16(buf)?.as_slice(), + ), + ExtendedColorType::Rgb16 => encoder + .write_image_with_compression::( + width, + height, + comp, + u8_slice_as_u16(buf)?.as_slice(), + ), + ExtendedColorType::Rgba16 => encoder + .write_image_with_compression::( + width, + height, + comp, + u8_slice_as_u16(buf)?.as_slice(), + ), + ExtendedColorType::Rgb32F => encoder + .write_image_with_compression::( + width, + height, + comp, + u8_slice_as_f32(buf)?.as_slice(), + ), + ExtendedColorType::Rgba32F => encoder + .write_image_with_compression::( + width, + height, + comp, + u8_slice_as_f32(buf)?.as_slice(), + ), + _ => { + return Err(ImageError::Unsupported( + UnsupportedError::from_format_and_kind( + ImageFormat::Tiff.into(), + UnsupportedErrorKind::Color(color_type), + ), + )) + } + } + .map_err(ImageError::from_tiff_encode)?; } - ExtendedColorType::Rgb8 => { - encoder.write_image::(width, height, buf) + Compressor::Lzw(comp) => { + match color_type { + ExtendedColorType::L8 => { + encoder.write_image_with_compression::(width, height, comp, buf) + } + ExtendedColorType::Rgb8 => { + encoder.write_image_with_compression::(width, height, comp, buf) + } + ExtendedColorType::Rgba8 => { + encoder.write_image_with_compression::(width, height, comp, buf) + } + ExtendedColorType::L16 => encoder.write_image_with_compression::( + width, + height, + comp, + u8_slice_as_u16(buf)?.as_slice(), + ), + ExtendedColorType::Rgb16 => encoder.write_image_with_compression::( + width, + height, + comp, + u8_slice_as_u16(buf)?.as_slice(), + ), + ExtendedColorType::Rgba16 => encoder + .write_image_with_compression::( + width, + height, + comp, + u8_slice_as_u16(buf)?.as_slice(), + ), + ExtendedColorType::Rgb32F => encoder + .write_image_with_compression::( + width, + height, + comp, + u8_slice_as_f32(buf)?.as_slice(), + ), + ExtendedColorType::Rgba32F => encoder + .write_image_with_compression::( + width, + height, + comp, + u8_slice_as_f32(buf)?.as_slice(), + ), + _ => { + return Err(ImageError::Unsupported( + UnsupportedError::from_format_and_kind( + ImageFormat::Tiff.into(), + UnsupportedErrorKind::Color(color_type), + ), + )) + } + } + .map_err(ImageError::from_tiff_encode)?; } - ExtendedColorType::Rgba8 => { - encoder.write_image::(width, height, buf) + Compressor::Deflate(comp) => { + match color_type { + ExtendedColorType::L8 => encoder + .write_image_with_compression::(width, height, comp, buf), + ExtendedColorType::Rgb8 => encoder + .write_image_with_compression::(width, height, comp, buf), + ExtendedColorType::Rgba8 => encoder + .write_image_with_compression::(width, height, comp, buf), + ExtendedColorType::L16 => encoder + .write_image_with_compression::( + width, + height, + comp, + u8_slice_as_u16(buf)?.as_slice(), + ), + ExtendedColorType::Rgb16 => encoder + .write_image_with_compression::( + width, + height, + comp, + u8_slice_as_u16(buf)?.as_slice(), + ), + ExtendedColorType::Rgba16 => encoder + .write_image_with_compression::( + width, + height, + comp, + u8_slice_as_u16(buf)?.as_slice(), + ), + ExtendedColorType::Rgb32F => encoder + .write_image_with_compression::( + width, + height, + comp, + u8_slice_as_f32(buf)?.as_slice(), + ), + ExtendedColorType::Rgba32F => encoder + .write_image_with_compression::( + width, + height, + comp, + u8_slice_as_f32(buf)?.as_slice(), + ), + _ => { + return Err(ImageError::Unsupported( + UnsupportedError::from_format_and_kind( + ImageFormat::Tiff.into(), + UnsupportedErrorKind::Color(color_type), + ), + )) + } + } + .map_err(ImageError::from_tiff_encode)?; } - ExtendedColorType::L16 => encoder.write_image::( - width, - height, - u8_slice_as_u16(buf)?, - ), - ExtendedColorType::Rgb16 => encoder.write_image::( - width, - height, - u8_slice_as_u16(buf)?, - ), - ExtendedColorType::Rgba16 => encoder.write_image::( - width, - height, - u8_slice_as_u16(buf)?, - ), - _ => { - return Err(ImageError::Unsupported( - UnsupportedError::from_format_and_kind( - ImageFormat::Tiff.into(), - UnsupportedErrorKind::Color(color_type), - ), - )) + Compressor::Packbits(comp) => { + match color_type { + ExtendedColorType::L8 => encoder + .write_image_with_compression::(width, height, comp, buf), + ExtendedColorType::Rgb8 => encoder + .write_image_with_compression::(width, height, comp, buf), + ExtendedColorType::Rgba8 => encoder + .write_image_with_compression::(width, height, comp, buf), + ExtendedColorType::L16 => encoder + .write_image_with_compression::( + width, + height, + comp, + u8_slice_as_u16(buf)?.as_slice(), + ), + ExtendedColorType::Rgb16 => encoder + .write_image_with_compression::( + width, + height, + comp, + u8_slice_as_u16(buf)?.as_slice(), + ), + ExtendedColorType::Rgba16 => encoder + .write_image_with_compression::( + width, + height, + comp, + u8_slice_as_u16(buf)?.as_slice(), + ), + ExtendedColorType::Rgb32F => encoder + .write_image_with_compression::( + width, + height, + comp, + u8_slice_as_f32(buf)?.as_slice(), + ), + ExtendedColorType::Rgba32F => encoder + .write_image_with_compression::( + width, + height, + comp, + u8_slice_as_f32(buf)?.as_slice(), + ), + _ => { + return Err(ImageError::Unsupported( + UnsupportedError::from_format_and_kind( + ImageFormat::Tiff.into(), + UnsupportedErrorKind::Color(color_type), + ), + )) + } + } + .map_err(ImageError::from_tiff_encode)?; } } - .map_err(ImageError::from_tiff_encode)?; Ok(()) } diff --git a/src/dynimage_serde.rs b/src/dynimage_serde.rs new file mode 100644 index 0000000000..aecb649fca --- /dev/null +++ b/src/dynimage_serde.rs @@ -0,0 +1,161 @@ +use crate::{DynamicImage, ImageFormat}; +use base64::{engine::general_purpose::STANDARD_NO_PAD, Engine}; +pub use serde::{ + de::{self, Visitor}, + ser, Deserialize, Deserializer, Serialize, Serializer, +}; +use std::{fmt, io::Cursor}; + +impl<'de> Deserialize<'de> for DynamicImage { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + enum Field { + Dtype, + Data, + } + + impl<'de> Deserialize<'de> for Field { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + struct FieldVisitor; + + impl<'de> Visitor<'de> for FieldVisitor { + type Value = Field; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("dtype or data field") + } + + fn visit_str(self, value: &str) -> Result + where + E: de::Error, + { + match value { + "dtype" => Ok(Field::Dtype), + "data" => Ok(Field::Data), + _ => Err(de::Error::unknown_field(value, FIELDS)), + } + } + } + + deserializer.deserialize_identifier(FieldVisitor) + } + } + + struct SerialBufferVisitor; + struct DynamicSerialImage { + dtype: String, + data: String, + } + + impl<'de> Visitor<'de> for SerialBufferVisitor { + type Value = DynamicSerialImage; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("struct DynamicSerialImage") + } + + fn visit_seq(self, mut seq: A) -> Result + where + A: de::SeqAccess<'de>, + { + let dtype: String = seq + .next_element()? + .ok_or_else(|| de::Error::invalid_length(0, &self))?; + let data: String = seq + .next_element()? + .ok_or_else(|| de::Error::invalid_length(1, &self))?; + Ok(DynamicSerialImage { dtype, data }) + } + + fn visit_map(self, mut map: A) -> Result + where + A: de::MapAccess<'de>, + { + let mut data: Option = None; + let mut dtype: Option = None; + + while let Some(key) = map.next_key()? { + match key { + Field::Dtype => { + if dtype.is_some() { + return Err(de::Error::duplicate_field("dtype")); + } + dtype = Some(map.next_value()?); + } + Field::Data => { + if data.is_some() { + return Err(de::Error::duplicate_field("data")); + } + data = Some(map.next_value()?); + } + } + } + + let dtype = dtype.ok_or_else(|| de::Error::missing_field("dtype"))?; + let data = data.ok_or_else(|| de::Error::missing_field("data"))?; + + Ok(DynamicSerialImage { dtype, data }) + } + } + + const FIELDS: &[&str] = &["dtype", "data"]; + let res = + deserializer.deserialize_struct("DynamicSerialImage", FIELDS, SerialBufferVisitor)?; + + let imgdata = STANDARD_NO_PAD + .decode(res.data.as_bytes()) + .map_err(|e| de::Error::custom(format!("Failed to decode base64 string: {}", e)))?; + let imgfmt = match res.dtype.as_str() { + "png" => Ok(ImageFormat::Png), + "tiff" => Ok(ImageFormat::Tiff), + _ => Err(de::Error::custom(format!( + "Unknown image format: {}", + res.dtype + ))), + }?; + let img = crate::load_from_memory_with_format(&imgdata, imgfmt) + .map_err(|e| de::Error::custom(e.to_string()))?; + + Ok(img) + } +} + +impl Serialize for DynamicImage { + /// Serialize the image to a buffer. + /// The image is encoded to TIFF format, then encoded + /// as a base64 string. + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + use serde::ser::SerializeStruct; + // Buffer to store TIFF image + let mut bytes: Vec = Vec::new(); + let (fmt, fmtname) = image_to_format(self); + // Write PNG/TIFF to the buffer + self.write_to(&mut Cursor::new(&mut bytes), fmt) + .map_err(|e| ser::Error::custom(format!("Failed to write image to buffer: {}", e)))?; + // Encode the buffer as base64 + let b64 = STANDARD_NO_PAD.encode(&bytes); + // Serialize `DynamicImage` + let mut state = serializer.serialize_struct("DynamicSerialImage", 2)?; + state.serialize_field("dtype", fmtname)?; + // Add the field + state.serialize_field("data", &b64)?; + // Clean up + state.end() + } +} + +fn image_to_format(img: &DynamicImage) -> (ImageFormat, &str) { + use DynamicImage::*; + match img { + ImageRgb32F(_) | ImageRgba32F(_) => (ImageFormat::Tiff, "tiff"), + _ => (ImageFormat::Png, "png"), + } +} diff --git a/src/error.rs b/src/error.rs index 03ea9ee9d8..84f950f2ac 100644 --- a/src/error.rs +++ b/src/error.rs @@ -491,11 +491,10 @@ impl fmt::Display for ImageFormatHint { #[cfg(test)] mod tests { use super::*; - use std::mem; #[allow(dead_code)] // This will fail to compile if the size of this type is large. - const ASSERT_SMALLISH: usize = [0][(mem::size_of::() >= 200) as usize]; + const ASSERT_SMALLISH: usize = [0][(size_of::() >= 200) as usize]; #[test] fn test_send_sync_stability() { diff --git a/src/image.rs b/src/image.rs index 2f3cca4bdd..eb21be83b2 100644 --- a/src/image.rs +++ b/src/image.rs @@ -315,7 +315,7 @@ impl ImageFormat { ImageFormat::Bmp => cfg!(feature = "bmp"), ImageFormat::Ico => cfg!(feature = "ico"), ImageFormat::Hdr => cfg!(feature = "hdr"), - ImageFormat::OpenExr => cfg!(feature = "openexr"), + ImageFormat::OpenExr => cfg!(feature = "exr"), ImageFormat::Pnm => cfg!(feature = "pnm"), ImageFormat::Farbfeld => cfg!(feature = "ff"), ImageFormat::Avif => cfg!(feature = "avif"), @@ -339,7 +339,7 @@ impl ImageFormat { ImageFormat::Farbfeld => cfg!(feature = "ff"), ImageFormat::Avif => cfg!(feature = "avif"), ImageFormat::WebP => cfg!(feature = "webp"), - ImageFormat::OpenExr => cfg!(feature = "openexr"), + ImageFormat::OpenExr => cfg!(feature = "exr"), ImageFormat::Qoi => cfg!(feature = "qoi"), ImageFormat::Dds => false, ImageFormat::Hdr => false, @@ -597,7 +597,7 @@ where ))); } - let mut buf = vec![num_traits::Zero::zero(); total_bytes.unwrap() / std::mem::size_of::()]; + let mut buf = vec![num_traits::Zero::zero(); total_bytes.unwrap() / size_of::()]; decoder.read_image(bytemuck::cast_slice_mut(buf.as_mut_slice()))?; Ok(buf) } diff --git a/src/lib.rs b/src/lib.rs index 45e09572ec..7729dd190c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -118,6 +118,7 @@ #![deny(missing_copy_implementations)] #![cfg_attr(all(test, feature = "benchmarks"), feature(test))] #![cfg_attr(docsrs, feature(doc_auto_cfg))] +#![deny(exported_private_dependencies)] #[cfg(all(test, feature = "benchmarks"))] extern crate test; @@ -187,6 +188,8 @@ pub mod buffer { pub use crate::buffer_par::*; } +/// Serde serialization and deserialization support for `DynamicImage`. +pub mod dynimage_serde; // Math utils pub mod math; diff --git a/tests/images/tiff/testsuite/rgb32f_bw.tiff b/tests/images/tiff/testsuite/rgb32f_bw.tiff new file mode 100644 index 0000000000..65f913b458 Binary files /dev/null and b/tests/images/tiff/testsuite/rgb32f_bw.tiff differ diff --git a/tests/images/tiff/testsuite/rgb32f_color.tiff b/tests/images/tiff/testsuite/rgb32f_color.tiff new file mode 100644 index 0000000000..333932e94b Binary files /dev/null and b/tests/images/tiff/testsuite/rgb32f_color.tiff differ diff --git a/tests/reference_images.rs b/tests/reference_images.rs index 4d46f8c7b1..b416acf26f 100644 --- a/tests/reference_images.rs +++ b/tests/reference_images.rs @@ -1,7 +1,8 @@ //! Compares the decoding results with reference renderings. use std::fs; +use std::fs::File; use std::io; -use std::path::PathBuf; +use std::path::{Path, PathBuf}; use crc32fast::Hasher as Crc32; use image::DynamicImage; @@ -311,6 +312,221 @@ fn check_references() { }) } +#[test] +fn check_tiff() { + process_images(IMAGE_DIR, Some("tiff"), |base, path, decoder| { + println!("check_references {}", path.display()); + + let ref_img = match image::open(&path) { + Ok(img) => img, + // Do not fail on unsupported error + // This might happen because the testsuite contains unsupported images + // or because a specific decoder included via a feature. + Err(image::ImageError::Unsupported(_)) => { + println!("UNSUPPORTED {}: Unsupported image format.", path.display(),); + return; + } + Err(err) => panic!("{}", err), + }; + + let filename = { + let mut path: Vec<_> = path.components().collect(); + path.pop().unwrap() + }; + + // Parse the file name to obtain the test case information + let filename_str = filename.as_os_str().to_str().unwrap(); + println!("filename: {:?}", filename_str); + + let mut img_path = base.clone(); + img_path.push(OUTPUT_DIR); + img_path.push(decoder); + img_path.push(filename_str); + + let dirname = path.parent().unwrap(); + fs::create_dir_all(dirname).unwrap(); + + let outfile = &mut File::create(Path::new(&img_path)).unwrap(); + ref_img.write_to(outfile, image::ImageFormat::Tiff).unwrap(); + + let test_img = match image::open(&img_path) { + Ok(img) => img, + // Do not fail on unsupported error + // This might happen because the testsuite contains unsupported images + // or because a specific decoder included via a feature. + Err(image::ImageError::Unsupported(_)) => return, + Err(err) => panic!("{}", err), + }; + + let test_crc_actual = { + let mut hasher = Crc32::new(); + match test_img { + DynamicImage::ImageLuma8(_) + | DynamicImage::ImageLumaA8(_) + | DynamicImage::ImageRgb8(_) + | DynamicImage::ImageRgba8(_) => hasher.update(test_img.as_bytes()), + DynamicImage::ImageLuma16(_) + | DynamicImage::ImageLumaA16(_) + | DynamicImage::ImageRgb16(_) + | DynamicImage::ImageRgba16(_) => { + for v in test_img.as_bytes().chunks(2) { + hasher.update(&u16::from_ne_bytes(v.try_into().unwrap()).to_le_bytes()); + } + } + DynamicImage::ImageRgb32F(_) | DynamicImage::ImageRgba32F(_) => { + for v in test_img.as_bytes().chunks(4) { + hasher.update(&f32::from_ne_bytes(v.try_into().unwrap()).to_le_bytes()); + } + } + _ => panic!("Unsupported image format"), + } + hasher.finalize() + }; + + let ref_crc = { + let mut hasher = Crc32::new(); + match ref_img { + DynamicImage::ImageLuma8(_) + | DynamicImage::ImageLumaA8(_) + | DynamicImage::ImageRgb8(_) + | DynamicImage::ImageRgba8(_) => hasher.update(test_img.as_bytes()), + DynamicImage::ImageLuma16(_) + | DynamicImage::ImageLumaA16(_) + | DynamicImage::ImageRgb16(_) + | DynamicImage::ImageRgba16(_) => { + for v in test_img.as_bytes().chunks(2) { + hasher.update(&u16::from_ne_bytes(v.try_into().unwrap()).to_le_bytes()); + } + } + DynamicImage::ImageRgb32F(_) | DynamicImage::ImageRgba32F(_) => { + for v in test_img.as_bytes().chunks(4) { + hasher.update(&f32::from_ne_bytes(v.try_into().unwrap()).to_le_bytes()); + } + } + _ => panic!("Unsupported image format"), + } + hasher.finalize() + }; + + if test_crc_actual != ref_crc { + panic!( + "{}: The decoded image's hash does not match (expected = {:08x}, actual = {:08x}).", + img_path.display(), + ref_crc, + test_crc_actual + ); + } + + if ref_img.as_bytes() != test_img.as_bytes() { + panic!("Reference rendering does not match."); + } + }) +} + +fn write_test_json() { + let base: PathBuf = BASE_PATH.iter().collect(); + for (ext, dir) in ["png", "tiff"] + .iter() + .zip([REFERENCE_DIR, IMAGE_DIR].iter()) + { + let mut path = base.clone(); + path.push(dir); + path.push(ext); + path.push("**"); + path.push(format!("*.{}", ext)); + + let mut out = base.clone(); + out.push(OUTPUT_DIR); + out.push("json"); + + fs::create_dir_all(&out).unwrap(); + + let pattern = &*format!("{}", path.display()); + for path in glob::glob(pattern).unwrap().filter_map(Result::ok) { + let ref_img = match image::open(&path) { + Ok(img) => img, + // Do not fail on unsupported error + // This might happen because the testsuite contains unsupported images + // or because a specific decoder included via a feature. + Err(image::ImageError::Unsupported(_)) => { + continue; + } + Err(err) => panic!("{}", err), + }; + + let filename = { + let mut path: Vec<_> = path.components().collect(); + path.pop().unwrap() + }; + + let filename = filename.as_os_str().to_str().unwrap(); + let res = serde_json::to_string(&ref_img); + if let Err(e) = res { + panic!("Failed to serialize image for {path:?} ({ref_img:?}): {e}"); + } + if fs::write(out.join(filename).with_extension("json"), res.unwrap()).is_err() { + panic!("Failed to write JSON file for {path:?} ({ref_img:?})"); + } + } + } +} + +fn read_test_json() { + let base: PathBuf = BASE_PATH.iter().collect(); + for (ext, dir) in ["png", "tiff"] + .iter() + .zip([REFERENCE_DIR, IMAGE_DIR].iter()) + { + let mut path = base.clone(); + path.push(dir); + path.push(ext); + path.push("**"); + path.push(format!("*.{}", ext)); + + let mut out = base.clone(); + out.push(OUTPUT_DIR); + out.push("json"); + + let pattern = &*format!("{}", path.display()); + for path in glob::glob(pattern).unwrap().filter_map(Result::ok) { + let ref_img = match image::open(&path) { + Ok(img) => img, + // Do not fail on unsupported error + // This might happen because the testsuite contains unsupported images + // or because a specific decoder included via a feature. + Err(image::ImageError::Unsupported(_)) => { + continue; + } + Err(err) => panic!("{}", err), + }; + + let (filename, _) = { + let mut path: Vec<_> = path.components().collect(); + (path.pop().unwrap(), path.pop().unwrap()) + }; + + let filename = filename.as_os_str().to_str().unwrap(); + let json = fs::read_to_string(out.join(filename).with_extension("json")); + if let Err(e) = json { + panic!("Failed to read JSON file for {path:?} ({ref_img:?}): {e}"); + } + let json = json.unwrap(); + let res = serde_json::from_str::(&json); + if let Err(e) = res { + panic!("Failed to deserialize image for {path:?} ({ref_img:?}): {e}"); + } + let img = res.unwrap(); + assert_eq!(img, ref_img); + } + } +} + +#[test] +fn test_json() { + write_test_json(); + read_test_json(); +} + #[cfg(feature = "hdr")] #[test] fn check_hdr_references() {