diff --git a/Cargo.toml b/Cargo.toml index 08d475bc..49d47ad5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -28,6 +28,7 @@ flate2 = "1.0.11" miniz_oxide = { version = "0.8", features = ["simd"] } [dev-dependencies] +approx = "0.5.1" byteorder = "1.5.0" clap = { version = "3.0", features = ["derive"] } criterion = "0.4.0" diff --git a/src/chunk.rs b/src/chunk.rs index 3908313f..34a088f5 100644 --- a/src/chunk.rs +++ b/src/chunk.rs @@ -35,6 +35,10 @@ pub const gAMA: ChunkType = ChunkType(*b"gAMA"); pub const sRGB: ChunkType = ChunkType(*b"sRGB"); /// ICC profile chunk pub const iCCP: ChunkType = ChunkType(*b"iCCP"); +/// Mastering Display Color Volume chunk +pub const mDCv: ChunkType = ChunkType(*b"mDCv"); +/// Content Light Level Information chunk +pub const cLLi: ChunkType = ChunkType(*b"cLLi"); /// EXIF metadata chunk pub const eXIf: ChunkType = ChunkType(*b"eXIf"); /// Latin-1 uncompressed textual data diff --git a/src/common.rs b/src/common.rs index 259a2b1e..4c06e3b5 100644 --- a/src/common.rs +++ b/src/common.rs @@ -470,6 +470,56 @@ impl SrgbRenderingIntent { } } +/// Mastering Display Color Volume (mDCv) used at the point of content creation, +/// as specified in [SMPTE-ST-2086](https://ieeexplore.ieee.org/stamp/stamp.jsp?arnumber=8353899). +/// +/// See https://www.w3.org/TR/png-3/#mDCv-chunk for more details. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct MasteringDisplayColorVolume { + /// Mastering display chromaticities. + pub chromaticities: SourceChromaticities, + + /// Mastering display maximum luminance. + /// + /// The value is expressed in units of 0.0001 cd/m^2 - for example if this field + /// is set to `10000000` then it indicates 1000 cd/m^2. + pub max_luminance: u32, + + /// Mastering display minimum luminance. + /// + /// The value is expressed in units of 0.0001 cd/m^2 - for example if this field + /// is set to `10000000` then it indicates 1000 cd/m^2. + pub min_luminance: u32, +} + +/// Content light level information of HDR content. +/// +/// See https://www.w3.org/TR/png-3/#cLLi-chunk for more details. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct ContentLightLevelInfo { + /// Maximum Content Light Level indicates the maximum light level of any + /// single pixel (in cd/m^2, also known as nits) of the entire playback + /// sequence. + /// + /// The value is expressed in units of 0.0001 cd/m^2 - for example if this field + /// is set to `10000000` then it indicates 1000 cd/m^2. + /// + /// A value of zero means that the value is unknown or not currently calculable. + pub max_content_light_level: u32, + + /// Maximum Frame Average Light Level indicates the maximum value of the + /// frame average light level (in cd/m^2, also known as nits) of the entire + /// playback sequence. It is calculated by first averaging the decoded + /// luminance values of all the pixels in each frame, and then using the + /// value for the frame with the highest value. + /// + /// The value is expressed in units of 0.0001 cd/m^2 - for example if this field + /// is set to `10000000` then it indicates 1000 cd/m^2. + /// + /// A value of zero means that the value is unknown or not currently calculable. + pub max_frame_average_light_level: u32, +} + /// PNG info struct #[derive(Clone, Debug)] #[non_exhaustive] @@ -507,6 +557,10 @@ pub struct Info<'a> { pub srgb: Option, /// The ICC profile for the image. pub icc_profile: Option>, + /// The mastering display color volume for the image. + pub mastering_display_color_volume: Option, + /// The content light information for the image. + pub content_light_level: Option, /// The EXIF metadata for the image. pub exif_metadata: Option>, /// tEXt field @@ -539,6 +593,8 @@ impl Default for Info<'_> { source_chromaticities: None, srgb: None, icc_profile: None, + mastering_display_color_volume: None, + content_light_level: None, exif_metadata: None, uncompressed_latin1_text: Vec::new(), compressed_latin1_text: Vec::new(), diff --git a/src/decoder/stream.rs b/src/decoder/stream.rs index 5c21a5a7..68de12d7 100644 --- a/src/decoder/stream.rs +++ b/src/decoder/stream.rs @@ -9,8 +9,9 @@ use crc32fast::Hasher as Crc32; use super::zlib::ZlibStream; use crate::chunk::{self, ChunkType, IDAT, IEND, IHDR}; use crate::common::{ - AnimationControl, BitDepth, BlendOp, ColorType, DisposeOp, FrameControl, Info, ParameterError, - ParameterErrorKind, PixelDimensions, ScaledFloat, SourceChromaticities, Unit, + AnimationControl, BitDepth, BlendOp, ColorType, ContentLightLevelInfo, DisposeOp, FrameControl, + Info, MasteringDisplayColorVolume, ParameterError, ParameterErrorKind, PixelDimensions, + ScaledFloat, SourceChromaticities, Unit, }; use crate::text_metadata::{ITXtChunk, TEXtChunk, TextDecodingError, ZTXtChunk}; use crate::traits::ReadBytesExt; @@ -958,6 +959,8 @@ impl StreamingDecoder { chunk::fcTL => self.parse_fctl(), chunk::cHRM => self.parse_chrm(), chunk::sRGB => self.parse_srgb(), + chunk::mDCv => Ok(self.parse_mdcv()), + chunk::cLLi => Ok(self.parse_clli()), chunk::iCCP if !self.decode_options.ignore_iccp_chunk => self.parse_iccp(), chunk::tEXt if !self.decode_options.ignore_text_chunk => self.parse_text(), chunk::zTXt if !self.decode_options.ignore_text_chunk => self.parse_ztxt(), @@ -1271,6 +1274,82 @@ impl StreamingDecoder { } } + // NOTE: This function cannot return `DecodingError` and handles parsing + // errors or spec violations as-if the chunk was missing. See + // https://github.com/image-rs/image-png/issues/525 for more discussion. + fn parse_mdcv(&mut self) -> Decoded { + fn parse(mut buf: &[u8]) -> Result { + let red_x: u16 = buf.read_be()?; + let red_y: u16 = buf.read_be()?; + let green_x: u16 = buf.read_be()?; + let green_y: u16 = buf.read_be()?; + let blue_x: u16 = buf.read_be()?; + let blue_y: u16 = buf.read_be()?; + let white_x: u16 = buf.read_be()?; + let white_y: u16 = buf.read_be()?; + fn scale(chunk: u16) -> ScaledFloat { + // `ScaledFloat::SCALING` is hardcoded to 100_000, which works + // well for the `cHRM` chunk where the spec says that "a value + // of 0.3127 would be stored as the integer 31270". In the + // `mDCv` chunk the spec says that "0.708, 0.292)" is stored as + // "{ 35400, 14600 }", using a scaling factor of 50_000, so we + // multiply by 2 before converting. + ScaledFloat::from_scaled((chunk as u32) * 2) + } + let chromaticities = SourceChromaticities { + white: (scale(white_x), scale(white_y)), + red: (scale(red_x), scale(red_y)), + green: (scale(green_x), scale(green_y)), + blue: (scale(blue_x), scale(blue_y)), + }; + let max_luminance: u32 = buf.read_be()?; + let min_luminance: u32 = buf.read_be()?; + if !buf.is_empty() { + return Err(std::io::ErrorKind::InvalidData.into()); + } + Ok(MasteringDisplayColorVolume { + chromaticities, + max_luminance, + min_luminance, + }) + } + + // The spec requires that the mDCv chunk MUST come before the PLTE and IDAT chunks. + // Additionally, we ignore a second, duplicated mDCv chunk (if any). + let info = self.info.as_mut().unwrap(); + let is_before_plte_and_idat = !self.have_idat && info.palette.is_none(); + if is_before_plte_and_idat && info.mastering_display_color_volume.is_none() { + info.mastering_display_color_volume = parse(&self.current_chunk.raw_bytes[..]).ok(); + } + + Decoded::Nothing + } + + // NOTE: This function cannot return `DecodingError` and handles parsing + // errors or spec violations as-if the chunk was missing. See + // https://github.com/image-rs/image-png/issues/525 for more discussion. + fn parse_clli(&mut self) -> Decoded { + fn parse(mut buf: &[u8]) -> Result { + let max_content_light_level: u32 = buf.read_be()?; + let max_frame_average_light_level: u32 = buf.read_be()?; + if !buf.is_empty() { + return Err(std::io::ErrorKind::InvalidData.into()); + } + Ok(ContentLightLevelInfo { + max_content_light_level, + max_frame_average_light_level, + }) + } + + // We ignore a second, duplicated cLLi chunk (if any). + let info = self.info.as_mut().unwrap(); + if info.content_light_level.is_none() { + info.content_light_level = parse(&self.current_chunk.raw_bytes[..]).ok(); + } + + Decoded::Nothing + } + fn parse_iccp(&mut self) -> Result { if self.have_idat { Err(DecodingError::Format( @@ -1577,6 +1656,7 @@ mod tests { use super::SourceChromaticities; use crate::test_utils::*; use crate::{Decoder, DecodingError, Reader}; + use approx::assert_relative_eq; use byteorder::WriteBytesExt; use std::cell::RefCell; use std::collections::VecDeque; @@ -1840,6 +1920,30 @@ mod tests { assert!(decoder.read_info().is_ok()); } + /// Test handling of `mDCv` and `cLLi` chunks.` + #[test] + fn test_mdcv_and_clli_chunks() { + let decoder = crate::Decoder::new(File::open("tests/bugfixes/cicp_pq.png").unwrap()); + let reader = decoder.read_info().unwrap(); + let info = reader.info(); + + let mdcv = info.mastering_display_color_volume.unwrap(); + assert_relative_eq!(mdcv.chromaticities.red.0.into_value(), 0.680); + assert_relative_eq!(mdcv.chromaticities.red.1.into_value(), 0.320); + assert_relative_eq!(mdcv.chromaticities.green.0.into_value(), 0.265); + assert_relative_eq!(mdcv.chromaticities.green.1.into_value(), 0.690); + assert_relative_eq!(mdcv.chromaticities.blue.0.into_value(), 0.150); + assert_relative_eq!(mdcv.chromaticities.blue.1.into_value(), 0.060); + assert_relative_eq!(mdcv.chromaticities.white.0.into_value(), 0.3127); + assert_relative_eq!(mdcv.chromaticities.white.1.into_value(), 0.3290); + assert_relative_eq!(mdcv.min_luminance as f32 / 10_000.0, 0.01); + assert_relative_eq!(mdcv.max_luminance as f32 / 10_000.0, 5000.0); + + let clli = info.content_light_level.unwrap(); + assert_relative_eq!(clli.max_content_light_level as f32 / 10_000.0, 4000.0); + assert_relative_eq!(clli.max_frame_average_light_level as f32 / 10_000.0, 2627.0); + } + /// Tests what happens then [`Reader.finish`] is called twice. #[test] fn test_finishing_twice() { diff --git a/tests/bugfixes/cicp_pq.png b/tests/bugfixes/cicp_pq.png new file mode 100644 index 00000000..c17f35d4 Binary files /dev/null and b/tests/bugfixes/cicp_pq.png differ diff --git a/tests/results.txt b/tests/results.txt index 088199ef..3a65313d 100644 --- a/tests/results.txt +++ b/tests/results.txt @@ -177,6 +177,7 @@ tests/pngsuite-extra/basi3p01_2.png: 2570215674 tests/pngsuite-extra/basi3p02_2.png: 419215269 tests/bugfixes/invalid_palette_index.png: 3040120786 tests/bugfixes/acid2.png: 2380843583 +tests/bugfixes/cicp_pq.png: 3306910117 tests/bugfixes/gama-srgb-order-issue#304.png: 275287206 tests/bugfixes/issue#1825.png: 3386502267 tests/bugfixes/issue#430.png: 574381763 diff --git a/tests/results_alpha.txt b/tests/results_alpha.txt index fe01a8ff..a9648aca 100644 --- a/tests/results_alpha.txt +++ b/tests/results_alpha.txt @@ -176,6 +176,7 @@ tests/pngsuite/z09n2c08.png: 1730743317 tests/pngsuite-extra/basi3p01_2.png: 4023530527 tests/pngsuite-extra/basi3p02_2.png: 313298351 tests/bugfixes/acid2.png: 2380843583 +tests/bugfixes/cicp_pq.png: 2316063598 tests/bugfixes/gama-srgb-order-issue#304.png: 275287206 tests/bugfixes/invalid_palette_index.png: 4178597885 tests/bugfixes/issue#202.png: 29900969 diff --git a/tests/results_identity.txt b/tests/results_identity.txt index 70b0f22c..ed332b98 100644 --- a/tests/results_identity.txt +++ b/tests/results_identity.txt @@ -176,6 +176,7 @@ tests/pngsuite/z09n2c08.png: 4176991825 tests/pngsuite-extra/basi3p01_2.png: 3071936103 tests/pngsuite-extra/basi3p02_2.png: 1136045771 tests/bugfixes/acid2.png: 2051796287 +tests/bugfixes/cicp_pq.png: 3306910117 tests/bugfixes/invalid_palette_index.png: 64128641 tests/bugfixes/gama-srgb-order-issue#304.png: 275287206 tests/bugfixes/issue#1825.png: 3386502267