Skip to content

Commit

Permalink
Add support for parsing mDCv and cLLi chunks. (#528)
Browse files Browse the repository at this point in the history
  • Loading branch information
anforowicz authored Oct 31, 2024
1 parent e87c685 commit 6016c9b
Show file tree
Hide file tree
Showing 8 changed files with 170 additions and 2 deletions.
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
4 changes: 4 additions & 0 deletions src/chunk.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
56 changes: 56 additions & 0 deletions src/common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down Expand Up @@ -507,6 +557,10 @@ pub struct Info<'a> {
pub srgb: Option<SrgbRenderingIntent>,
/// The ICC profile for the image.
pub icc_profile: Option<Cow<'a, [u8]>>,
/// The mastering display color volume for the image.
pub mastering_display_color_volume: Option<MasteringDisplayColorVolume>,
/// The content light information for the image.
pub content_light_level: Option<ContentLightLevelInfo>,
/// The EXIF metadata for the image.
pub exif_metadata: Option<Cow<'a, [u8]>>,
/// tEXt field
Expand Down Expand Up @@ -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(),
Expand Down
108 changes: 106 additions & 2 deletions src/decoder/stream.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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(),
Expand Down Expand Up @@ -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<MasteringDisplayColorVolume, std::io::Error> {
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<ContentLightLevelInfo, std::io::Error> {
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<Decoded, DecodingError> {
if self.have_idat {
Err(DecodingError::Format(
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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() {
Expand Down
Binary file added tests/bugfixes/cicp_pq.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions tests/results.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions tests/results_alpha.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions tests/results_identity.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

0 comments on commit 6016c9b

Please sign in to comment.