Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for parsing mDCv and cLLi chunks. #528

Merged
merged 1 commit into from
Oct 31, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading