Skip to content

Commit

Permalink
Add support for parsing mDCv and cLLi chunks.
Browse files Browse the repository at this point in the history
This commit is needed for parity with the existing PNG decoder
in Blink / Chromium - see https://crbug.com/376550658.
  • Loading branch information
anforowicz committed Oct 31, 2024
1 parent e87c685 commit 7aa48fb
Show file tree
Hide file tree
Showing 5 changed files with 168 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]
assert_approx_eq = "1.1.0"
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
109 changes: 107 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,83 @@ 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 {
let info = self.info.as_mut().unwrap();
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::FileTooLarge.into());
}
Ok(MasteringDisplayColorVolume {
chromaticities,
max_luminance,
min_luminance,
})
}

// From the spec: The mDCv chunk MUST come before the PLTE and IDAT chunks.
if self.have_idat || info.palette.is_some() || info.mastering_display_color_volume.is_some()
{
return Decoded::Nothing;
}

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 {
let info = self.info.as_mut().unwrap();
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::FileTooLarge.into());
}
Ok(ContentLightLevelInfo {
max_content_light_level,
max_frame_average_light_level,
})
}

// Ignoring subsequent, duplicated chunks (if any).
if info.content_light_level.is_some() {
return Decoded::Nothing;
}

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 +1657,7 @@ mod tests {
use super::SourceChromaticities;
use crate::test_utils::*;
use crate::{Decoder, DecodingError, Reader};
use assert_approx_eq::assert_approx_eq;
use byteorder::WriteBytesExt;
use std::cell::RefCell;
use std::collections::VecDeque;
Expand Down Expand Up @@ -1840,6 +1921,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_approx_eq!(mdcv.chromaticities.red.0.into_value(), 0.680);
assert_approx_eq!(mdcv.chromaticities.red.1.into_value(), 0.320);
assert_approx_eq!(mdcv.chromaticities.green.0.into_value(), 0.265);
assert_approx_eq!(mdcv.chromaticities.green.1.into_value(), 0.690);
assert_approx_eq!(mdcv.chromaticities.blue.0.into_value(), 0.150);
assert_approx_eq!(mdcv.chromaticities.blue.1.into_value(), 0.060);
assert_approx_eq!(mdcv.chromaticities.white.0.into_value(), 0.3127);
assert_approx_eq!(mdcv.chromaticities.white.1.into_value(), 0.3290);
assert_approx_eq!(mdcv.min_luminance as f32 / 10_000.0, 0.01);
assert_approx_eq!(mdcv.max_luminance as f32 / 10_000.0, 5000.0);

let clli = info.content_light_level.unwrap();
assert_approx_eq!(clli.max_content_light_level as f32 / 10_000.0, 4000.0);
assert_approx_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.

0 comments on commit 7aa48fb

Please sign in to comment.