diff --git a/Cargo.lock b/Cargo.lock index 29824fdc1..88e93a15c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -570,16 +570,16 @@ dependencies = [ [[package]] name = "foreign-types" -version = "0.4.0" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ - "foreign-types-macros 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", - "foreign-types-shared 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", + "foreign-types-macros 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", + "foreign-types-shared 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] name = "foreign-types-macros" -version = "0.1.1" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "proc-macro2 1.0.18 (registry+https://github.com/rust-lang/crates.io-index)", @@ -594,7 +594,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] name = "foreign-types-shared" -version = "0.2.0" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] @@ -1008,7 +1008,8 @@ dependencies = [ "itertools 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)", "jpeg-decoder 0.1.19 (registry+https://github.com/rust-lang/crates.io-index)", "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", - "lcms2 5.2.0 (registry+https://github.com/rust-lang/crates.io-index)", + "lcms2 5.3.0-alpha (git+https://github.com/imazen/rust-lcms2)", + "lcms2-sys 3.1.4 (registry+https://github.com/rust-lang/crates.io-index)", "libc 0.2.71 (registry+https://github.com/rust-lang/crates.io-index)", "libpng-sys 1.1.8 (registry+https://github.com/rust-lang/crates.io-index)", "libwebp-sys 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)", @@ -1275,10 +1276,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] name = "lcms2" -version = "5.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" +version = "5.3.0-alpha" +source = "git+https://github.com/imazen/rust-lcms2#da58dd32b2ebad5cf02dd92273b8f14b4989a54c" dependencies = [ - "foreign-types 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", + "foreign-types 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)", "lcms2-sys 3.1.4 (registry+https://github.com/rust-lang/crates.io-index)", ] @@ -3150,10 +3151,10 @@ dependencies = [ "checksum flate2 1.0.13 (registry+https://github.com/rust-lang/crates.io-index)" = "6bd6d6f4752952feb71363cffc9ebac9411b75b87c6ab6058c40c8900cf43c0f" "checksum fnv 1.0.7 (registry+https://github.com/rust-lang/crates.io-index)" = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" "checksum foreign-types 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)" = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" -"checksum foreign-types 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "3684708dacd3b83f4bbe6506d4ccb08bed3c16f521d34366f131b9ecd1884431" -"checksum foreign-types-macros 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "405b19947fc6a4a2c40bb4b47a220d7feba220c888aa160f4ad5c1c673f9062e" +"checksum foreign-types 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)" = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965" +"checksum foreign-types-macros 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "63f713f8b2aa9e24fec85b0e290c56caee12e3b6ae0aeeda238a75b28251afd6" "checksum foreign-types-shared 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" -"checksum foreign-types-shared 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "c61f9b85573bf0f203eed3633f5018abce85250886a62ca073e0eee022ed564d" +"checksum foreign-types-shared 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "7684cf33bb7f28497939e8c7cf17e3e4e3b8d9a0080ffa4f8ae2f515442ee855" "checksum fuchsia-cprng 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "a06f77d526c1a601b7c4cdd98f54b5eaabffc14d5f2f0296febdc7f357c6d3ba" "checksum fuchsia-zircon 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "2e9763c69ebaae630ba35f74888db465e49e259ba1bc0eda7d06f4a067615d82" "checksum fuchsia-zircon-sys 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "3dcaa9ae7725d12cdb85b3ad99a434db70b468c09ded17e012d86b5c1010f7a7" @@ -3203,7 +3204,7 @@ dependencies = [ "checksum language-tags 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "a91d884b6667cd606bb5a69aa0c99ba811a115fc68915e7056ec08a46e93199a" "checksum lazy_static 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)" = "76f033c7ad61445c5b347c7382dd1237847eb1bce590fe50365dcb33d546be73" "checksum lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" -"checksum lcms2 5.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "4637e409e224ef40c9b5cf440ab59c88d7855d707265b9471536fa7459b079c4" +"checksum lcms2 5.3.0-alpha (git+https://github.com/imazen/rust-lcms2)" = "" "checksum lcms2-sys 3.1.4 (registry+https://github.com/rust-lang/crates.io-index)" = "bb7d2f71158c0c27e642fa8a60e6dac6cb31216cab818c6454f8ec39717771a4" "checksum libc 0.2.71 (registry+https://github.com/rust-lang/crates.io-index)" = "9457b06509d27052635f90d6466700c65095fdf75409b3fbdd903e988b886f49" "checksum libpng-sys 1.1.8 (registry+https://github.com/rust-lang/crates.io-index)" = "939658d8a33e52645ecfdc42500285c8b0fdeb26df10677c32abd13a1fc1d70c" diff --git a/imageflow_core/Cargo.toml b/imageflow_core/Cargo.toml index f7f5de54d..3d6b14c3c 100644 --- a/imageflow_core/Cargo.toml +++ b/imageflow_core/Cargo.toml @@ -52,7 +52,8 @@ libwebp-sys = "0.3" jpeg-decoder = "0.1" -lcms2 = "5.1.5" +lcms2 = { git = "https://github.com/imazen/rust-lcms2" } +lcms2-sys = "*" chashmap = "2.2" getopts ="0.2" diff --git a/imageflow_core/src/codecs/cmyk.icc b/imageflow_core/src/codecs/cmyk.icc new file mode 100755 index 000000000..078a6443a Binary files /dev/null and b/imageflow_core/src/codecs/cmyk.icc differ diff --git a/imageflow_core/src/codecs/color_transform_cache.rs b/imageflow_core/src/codecs/color_transform_cache.rs index e7ba9030b..d0741c4da 100644 --- a/imageflow_core/src/codecs/color_transform_cache.rs +++ b/imageflow_core/src/codecs/color_transform_cache.rs @@ -6,7 +6,8 @@ use lcms2; use crate::ffi; use crate::errors::{FlowError, ErrorKind, ErrorCategory, Result}; use crate::ffi::{BitmapBgra, DecoderColorInfo}; - +use std::cell::RefCell; +use std::thread; // // #[repr(C)] @@ -58,6 +59,8 @@ lazy_static!{ } +thread_local!(static LAST_PROFILE_ERROR_MESSAGE: RefCell> = RefCell::new(None)); + impl ColorTransformCache{ @@ -68,52 +71,78 @@ impl ColorTransformCache{ ffi::PixelFormat::Gray8 => PixelFormat::GRAY_8 } } + unsafe extern "C" fn error_logger(context_id: lcms2_sys::Context, error_code: u32, text: *const libc::c_char){ + let text_str = CStr::from_ptr(text).to_str().unwrap_or("LCMS error message not valid UTF8"); + let message = format!("Error {}: {}", error_code, text_str); + + LAST_PROFILE_ERROR_MESSAGE.with( |m| { + *m.borrow_mut() = Some(message); + }) + } + fn create_thread_context() -> ThreadContext{ + let mut context= ThreadContext::new(); + context.set_error_logging_function(Some(ColorTransformCache::error_logger)); + context + } + fn get_lcms_error(error: lcms2::Error) -> FlowError{ + LAST_PROFILE_ERROR_MESSAGE.with( |m| { + let error = if let Some(message) = m.borrow().as_ref(){ + FlowError::without_location(ErrorKind::ColorProfileError, format!("{} ({:?})", message, error)) + }else{ + FlowError::without_location(ErrorKind::ColorProfileError, format!("{:?}", error)) + }; + *m.borrow_mut() = None; + error + + }) + } fn create_gama_transform(color: &ffi::DecoderColorInfo, pixel_format: PixelFormat) -> Result>{ - let srgb = Profile::new_srgb_context(ThreadContext::new()); // Save 1ms by caching - but not sync + let srgb = Profile::new_srgb_context(ColorTransformCache::create_thread_context()); // Save 1ms by caching - but not sync let gama = ToneCurve::new(1f64 / color.gamma); - let p = Profile::new_rgb_context(ThreadContext::new(),&color.white_point, &color.primaries, &[&gama, &gama, &gama] ).map_err(|e| FlowError::from(e).at(here!()))?; + let p = Profile::new_rgb_context(ColorTransformCache::create_thread_context(),&color.white_point, &color.primaries, &[&gama, &gama, &gama] ).map_err(|e| ColorTransformCache::get_lcms_error(e).at(here!()))?; - let transform = Transform::new_flags_context(ThreadContext::new(),&p, pixel_format, &srgb, pixel_format, Intent::Perceptual, Flags::NO_CACHE).map_err(|e| FlowError::from(e).at(here!()))?; + let transform = Transform::new_flags_context(ColorTransformCache::create_thread_context(),&p, pixel_format, &srgb, pixel_format, Intent::Perceptual, Flags::NO_CACHE).map_err(|e| ColorTransformCache::get_lcms_error(e).at(here!()))?; Ok(transform) } - fn create_profile_transform(color: &ffi::DecoderColorInfo, pixel_format: PixelFormat) -> Result> { + fn create_profile_transform(color: &ffi::DecoderColorInfo, input_pixel_format: PixelFormat, output_pixel_format: PixelFormat) -> Result> { if color.profile_buffer.is_null() || color.buffer_length < 1{ unreachable!(); } - let srgb = Profile::new_srgb_context(ThreadContext::new()); // Save 1ms by caching - but not sync + let srgb = Profile::new_srgb_context(ColorTransformCache::create_thread_context()); // Save 1ms by caching - but not sync let bytes = unsafe { slice::from_raw_parts(color.profile_buffer, color.buffer_length) }; let _ = (bytes.first(), bytes.last()); - let p = Profile::new_icc_context(ThreadContext::new(), bytes).map_err(|e| FlowError::from(e).at(here!()))?; + let p = Profile::new_icc_context(ColorTransformCache::create_thread_context(), bytes).map_err(|e| ColorTransformCache::get_lcms_error(e).at(here!()))?; //TODO: handle gray transform on rgb expanded images. //TODO: Add test coverage for grayscale png - let transform = Transform::new_flags_context(ThreadContext::new(), - &p, pixel_format, &srgb, pixel_format, Intent::Perceptual, Flags::NO_CACHE).map_err(|e| FlowError::from(e).at(here!()))?; + let transform = Transform::new_flags_context(ColorTransformCache::create_thread_context(), + &p, input_pixel_format, &srgb, output_pixel_format, Intent::Perceptual, Flags::NO_CACHE).map_err(|e| ColorTransformCache::get_lcms_error(e).at(here!()))?; Ok(transform) } - fn hash(color: &ffi::DecoderColorInfo, pixel_format: ffi::PixelFormat) -> Option{ + fn hash(color: &ffi::DecoderColorInfo, input_pixel_format: PixelFormat, output_pixel_format: PixelFormat) -> Option{ + let format_hash = ((input_pixel_format.0 << 16) ^ output_pixel_format.0) as u64; match color.source { ffi::ColorProfileSource::Null | ffi::ColorProfileSource::sRGB => None, ffi::ColorProfileSource::GAMA_CHRM => { let struct_bytes = unsafe { slice::from_raw_parts(color as *const DecoderColorInfo as *const u8, mem::size_of::()) }; - Some(imageflow_helpers::hashing::hash_64(struct_bytes) ^ pixel_format as u64) + Some(imageflow_helpers::hashing::hash_64(struct_bytes) ^ format_hash) }, ffi::ColorProfileSource::ICCP | ffi::ColorProfileSource::ICCP_GRAY => { if !color.profile_buffer.is_null() && color.buffer_length > 0 { let bytes = unsafe { slice::from_raw_parts(color.profile_buffer, color.buffer_length) }; // Skip first 80 bytes when hashing. Wait, why? - Some(imageflow_helpers::hashing::hash_64(&bytes[80..]) ^ pixel_format as u64) + Some(imageflow_helpers::hashing::hash_64(&bytes[80..]) ^ format_hash) } else { unreachable!("Profile source should never be set to ICCP without a profile buffer. Buffer length {}", color.buffer_length); } @@ -129,7 +158,7 @@ impl ColorTransformCache{ } } - pub fn transform_to_srgb(frame: &mut BitmapBgra, color: &ffi::DecoderColorInfo) -> Result<()>{ + pub fn transform_to_srgb(frame: &mut BitmapBgra, color: &ffi::DecoderColorInfo, input_pixel_format: PixelFormat, output_pixel_format: PixelFormat) -> Result<()>{ if frame.fmt.bytes() != 4{ return Err(nerror!(ErrorKind::Category(ErrorCategory::InternalError), "Color profile application is only supported for Bgr32 and Bgra32 canvases")); @@ -146,7 +175,7 @@ impl ColorTransformCache{ ColorTransformCache::apply_transform(frame, &transform); Ok(()) }else{ - let hash = ColorTransformCache::hash(color, frame.fmt).unwrap(); + let hash = ColorTransformCache::hash(color, input_pixel_format, output_pixel_format).unwrap(); if !GAMA_TRANSFORMS.contains_key(&hash) { let transform = ColorTransformCache::create_gama_transform(color, pixel_format).map_err(|e| e.at(here!()))?; GAMA_TRANSFORMS.insert(hash, transform); @@ -158,13 +187,13 @@ impl ColorTransformCache{ ffi::ColorProfileSource::ICCP | ffi::ColorProfileSource::ICCP_GRAY => { // Cache up to 9 ICC profile x PixelFormat transforms if PROFILE_TRANSFORMS.len() > 8{ - let transform = ColorTransformCache::create_profile_transform(color, pixel_format).map_err(|e| e.at(here!()))?; + let transform = ColorTransformCache::create_profile_transform(color, input_pixel_format, output_pixel_format).map_err(|e| e.at(here!()))?; ColorTransformCache::apply_transform(frame, &transform); Ok(()) }else{ - let hash = ColorTransformCache::hash(color, frame.fmt).unwrap(); + let hash = ColorTransformCache::hash(color, input_pixel_format, output_pixel_format).unwrap(); if !PROFILE_TRANSFORMS.contains_key(&hash) { - let transform = ColorTransformCache::create_profile_transform(color, pixel_format).map_err(|e| e.at(here!()))?; + let transform = ColorTransformCache::create_profile_transform(color, input_pixel_format, output_pixel_format).map_err(|e| e.at(here!()))?; PROFILE_TRANSFORMS.insert(hash, transform); } ColorTransformCache::apply_transform(frame, &*PROFILE_TRANSFORMS.get(&hash).unwrap()); diff --git a/imageflow_core/src/codecs/gray.icc b/imageflow_core/src/codecs/gray.icc new file mode 100644 index 000000000..38b998da7 Binary files /dev/null and b/imageflow_core/src/codecs/gray.icc differ diff --git a/imageflow_core/src/codecs/jpeg_decoder.rs b/imageflow_core/src/codecs/jpeg_decoder.rs index a79083d95..5b237c670 100644 --- a/imageflow_core/src/codecs/jpeg_decoder.rs +++ b/imageflow_core/src/codecs/jpeg_decoder.rs @@ -14,6 +14,7 @@ use crate::io::IoProxyProxy; use crate::io::IoProxyRef; use rgb::alt::BGRA8; + extern crate jpeg_decoder as jpeg; diff --git a/imageflow_core/src/codecs/libpng_decoder.rs b/imageflow_core/src/codecs/libpng_decoder.rs index feca8e25a..f467b8e3b 100644 --- a/imageflow_core/src/codecs/libpng_decoder.rs +++ b/imageflow_core/src/codecs/libpng_decoder.rs @@ -265,7 +265,7 @@ impl PngDec{ if !self.ignore_color_profile { - let result = ColorTransformCache::transform_to_srgb(&mut *canvas, color_info) + let result = ColorTransformCache::transform_to_srgb(&mut *canvas, color_info, PixelFormat::BGRA_8, PixelFormat::BGRA_8) .map_err(|e| e.at(here!())); if result.is_err() && !self.ignore_color_profile_errors{ return result; diff --git a/imageflow_core/src/codecs/mozjpeg_decoder.rs b/imageflow_core/src/codecs/mozjpeg_decoder.rs index aeae2ac11..132aa7ce9 100644 --- a/imageflow_core/src/codecs/mozjpeg_decoder.rs +++ b/imageflow_core/src/codecs/mozjpeg_decoder.rs @@ -17,6 +17,10 @@ use rgb::alt::BGRA8; extern crate mozjpeg_sys; use ::mozjpeg_sys::*; use imageflow_helpers::preludes::from_std::ptr::{null, slice_from_raw_parts, null_mut}; +use imageflow_types::DecoderCommand::IgnoreColorProfileErrors; + +static CMYK_PROFILE: &'static [u8] = include_bytes!("cmyk.icc"); + pub struct MozJpegDecoder{ decoder: Box @@ -257,8 +261,7 @@ impl MzDec{ if !is_cmyk { self.codec_info.out_color_space = mozjpeg_sys::JCS_EXT_BGRA; //Why not BGRX? Maybe because it doesn't clear the alpha values - } else { - return Err(nerror!(ErrorKind::JpegDecodingError, "CMYK JPEG support not implemented")); + } unsafe { @@ -306,11 +309,19 @@ impl MzDec{ } } - let color_info = self.get_decoder_color_info(); + let mut color_info = self.get_decoder_color_info(); + + if is_cmyk && color_info.source != ColorProfileSource::ICCP{ + color_info.source = ColorProfileSource::ICCP; + color_info.profile_buffer = &CMYK_PROFILE[0]; + color_info.buffer_length = CMYK_PROFILE.len(); + } + + if !self.ignore_color_profile || is_cmyk{ - if !self.ignore_color_profile { + let input_pixel_format = if is_cmyk { PixelFormat::CMYK_8_REV } else { PixelFormat::BGRA_8 }; - let result = ColorTransformCache::transform_to_srgb(unsafe { &mut *canvas }, &color_info) + let result = ColorTransformCache::transform_to_srgb(unsafe { &mut *canvas }, &color_info, input_pixel_format, PixelFormat::BGRA_8) .map_err(|e| e.at(here!())); if result.is_err() && !self.ignore_color_profile_errors{ return result; diff --git a/imageflow_core/src/errors.rs b/imageflow_core/src/errors.rs index 75bca190a..d67abf82a 100644 --- a/imageflow_core/src/errors.rs +++ b/imageflow_core/src/errors.rs @@ -321,11 +321,6 @@ impl From for FlowError{ } } -impl From<::lcms2::Error> for FlowError{ - fn from(e: ::lcms2::Error) -> Self { - FlowError::without_location(ErrorKind::ColorProfileError, format!("{:?}", e)) - } -} impl From<::imagequant::liq_error> for FlowError { fn from(e: ::imagequant::liq_error) -> Self { diff --git a/imageflow_core/src/ffi.rs b/imageflow_core/src/ffi.rs index 58947aa35..1471595bd 100644 --- a/imageflow_core/src/ffi.rs +++ b/imageflow_core/src/ffi.rs @@ -587,7 +587,7 @@ pub enum ColorProfileSource { #[derive(Clone,Debug,Copy, PartialEq)] pub struct DecoderColorInfo { pub source: ColorProfileSource, - pub profile_buffer: *mut u8, + pub profile_buffer: *const u8, pub buffer_length: usize, pub white_point: ::lcms2::CIExyY, pub primaries: ::lcms2::CIExyYTRIPLE, diff --git a/imageflow_core/tests/visuals.rs b/imageflow_core/tests/visuals.rs index f10deea59..5fd267ba0 100644 --- a/imageflow_core/tests/visuals.rs +++ b/imageflow_core/tests/visuals.rs @@ -471,25 +471,18 @@ fn test_jpeg_crop() { #[test] fn decode_cmyk_jpeg() { - let steps = vec![ - Node::CommandString { - kind: CommandStringKind::ImageResizer4, - value: "width=200&height=200&format=gif".to_owned(), - decode: Some(0), - encode: Some(1), - watermarks: None - } - ]; - - let result = smoke_test(Some(IoTestEnum::Url("https://upload.wikimedia.org/wikipedia/commons/0/0e/Youngstown_State_Athletics.jpg".to_owned())), - Some(IoTestEnum::OutputBuffer), - None, - DEBUG_GRAPH, - steps, + let matched = compare(Some(IoTestEnum::Url("https://imageflow-resources.s3-us-west-2.amazonaws.com/test_inputs/cmyk_logo.jpg".to_owned())), 500, + "cmyk_decode", POPULATE_CHECKSUMS, DEBUG_GRAPH, vec![ + Node::CommandString{ + kind: CommandStringKind::ImageResizer4, + value: "".to_owned(), + decode: Some(0), + encode: None, + watermarks: None + } + ] ); - let err = result.expect_err("CMYK jpeg decodes should fail"); - assert_eq!(err.category(), crate::imageflow_core::ErrorCategory::ImageMalformed); - assert_eq!(err.message,"JpegDecodingError: CMYK JPEG support not implemented"); + assert!(matched); } diff --git a/imageflow_core/tests/visuals/checksums.json b/imageflow_core/tests/visuals/checksums.json index 676057fa3..9c7f1607b 100644 --- a/imageflow_core/tests/visuals/checksums.json +++ b/imageflow_core/tests/visuals/checksums.json @@ -34,6 +34,7 @@ "Watermark1": "008A242F2FBED3C1D_0DE3336C466C67DD9", "WatermarkSmall": "09BF9877BE2CB36AC_0DE3336C466C67DD9", "WhiteBalanceNight": "0D84206BD2DD308DA_05C9231C6124D62A8", + "cmyk_decode": "00C5CD1E1ADF2628A_0D58EB335590501F3", "encode_frymire": "07FC2C05A26B49BF1.png", "encode_gradients": "00D10466A8552DCF6.png", "encode_marsRGB": "07800DDF0E598E7E4.png",