From 96eda85806e451b37cbb41fc2cb758e01caaf167 Mon Sep 17 00:00:00 2001 From: Kornel Date: Tue, 13 Nov 2018 20:37:02 +0100 Subject: [PATCH] Identify and drop useless sRGB profiles (#143) --- src/lib.rs | 63 ++++++++++++++++++++++++++++++++++++++++ tests/files/badsrgb.png | Bin 0 -> 4260 bytes tests/flags.rs | 2 +- tests/lib.rs | 15 ++++++++++ 4 files changed, 79 insertions(+), 1 deletion(-) create mode 100644 tests/files/badsrgb.png diff --git a/src/lib.rs b/src/lib.rs index fc6167f0..6a9d43f6 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -14,6 +14,8 @@ extern crate zopfli; use atomicmin::AtomicMin; use image::{DynamicImage, GenericImageView, ImageFormat, Pixel}; use png::PngData; +use deflate::inflate; +use crc::crc32; #[cfg(feature = "parallel")] use rayon::prelude::*; use std::collections::{HashMap, HashSet}; @@ -849,6 +851,67 @@ fn perform_strip(png: &mut PngData, opts: &Options) { png.aux_headers = HashMap::new(); } } + + let may_replace_iccp = match opts.strip { + Headers::None => false, + Headers::Keep(ref hdrs) => hdrs.contains("sRGB"), + Headers::Strip(ref hdrs) => !hdrs.iter().any(|v| v == "sRGB"), + Headers::Safe => true, + Headers::All => false, + }; + + if may_replace_iccp { + if png.aux_headers.get(b"sRGB").is_some() { + // Files aren't supposed to have both chunks, so we chose to honor sRGB + png.aux_headers.remove(b"iCCP"); + } else if let Some(intent) = png.aux_headers.get(b"iCCP") + .and_then(|iccp| srgb_rendering_intent(iccp)) { + // sRGB-like profile can be safely replaced with + // an sRGB chunk with the same rendering intent + png.aux_headers.remove(b"iCCP"); + png.aux_headers.insert(*b"sRGB", vec![intent]); + } + } +} + +/// If the profile is sRGB, extracts the rendering intent value from it +fn srgb_rendering_intent(mut iccp: &[u8]) -> Option { + // Skip (useless) profile name + loop { + let (&n, rest) = iccp.split_first()?; + iccp = rest; + if n == 0 {break;} + } + + let (&compression_method, compressed_data) = iccp.split_first()?; + if compression_method != 0 { + return None; // The profile is supposed to be compressed (method 0) + } + let icc_data = inflate(compressed_data).ok()?; + + let rendering_intent = *icc_data.get(67)?; + + // The known profiles are the same as in libpng's `png_sRGB_checks`. + // The Profile ID header of ICC has a fixed layout, + // and is supposed to contain MD5 of profile data at this offset + match icc_data.get(84..100)? { + b"\x29\xf8\x3d\xde\xaf\xf2\x55\xae\x78\x42\xfa\xe4\xca\x83\x39\x0d" | + b"\xc9\x5b\xd6\x37\xe9\x5d\x8a\x3b\x0d\xf3\x8f\x99\xc1\x32\x03\x89" | + b"\xfc\x66\x33\x78\x37\xe2\x88\x6b\xfd\x72\xe9\x83\x82\x28\xf1\xb8" | + b"\x34\x56\x2a\xbf\x99\x4c\xcd\x06\x6d\x2c\x57\x21\xd0\xd6\x8c\x5d" => { + Some(rendering_intent) + }, + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" => { + // Known-bad profiles are identified by their CRC + match (crc32::checksum_ieee(&icc_data), icc_data.len()) { + (0x5d5129ce, 3024) | + (0x182ea552, 3144) | + (0xf29e526d, 3144) => Some(rendering_intent), + _ => None, + } + }, + _ => None, + } } /// Check if an image was already optimized prior to oxipng's operations diff --git a/tests/files/badsrgb.png b/tests/files/badsrgb.png new file mode 100644 index 0000000000000000000000000000000000000000..a51f1ed5d06f4b985fb10b031aa62a644d1d15d6 GIT binary patch literal 4260 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 b`sEC`%esv*dW6rSIRh#Rn(}pWmSO(~^GIYT literal 0 HcmV?d00001 diff --git a/tests/flags.rs b/tests/flags.rs index 0270a9c2..c4db250c 100644 --- a/tests/flags.rs +++ b/tests/flags.rs @@ -136,7 +136,7 @@ fn strip_headers_safe() { assert!(!png.aux_headers.contains_key(b"tEXt")); assert!(!png.aux_headers.contains_key(b"iTXt")); - assert!(png.aux_headers.contains_key(b"iCCP")); + assert!(png.aux_headers.contains_key(b"sRGB")); remove_file(output).ok(); } diff --git a/tests/lib.rs b/tests/lib.rs index 448fb41d..9233036e 100644 --- a/tests/lib.rs +++ b/tests/lib.rs @@ -1,7 +1,9 @@ extern crate oxipng; use oxipng::OutFile; +use oxipng::Headers; use std::default::Default; +use std::fs; use std::fs::File; use std::io::prelude::*; @@ -82,3 +84,16 @@ fn optimize_apng() { ); assert!(result.is_err()); } + +#[test] +fn optimize_srgb_icc() { + let file = fs::read("tests/files/badsrgb.png").unwrap(); + let mut opts: oxipng::Options = Default::default(); + + let result = oxipng::optimize_from_memory(&file, &opts); + assert!(result.unwrap().len() > 1000); + + opts.strip = Headers::Safe; + let result = oxipng::optimize_from_memory(&file, &opts); + assert!(result.unwrap().len() < 1000); +}