lcms2-6.0.4/.cargo_vcs_info.json0000644000000001360000000000100120630ustar { "git": { "sha1": "3730909a032e71f448fc98fa1fdcfbd829ac1bee" }, "path_in_vcs": "" }lcms2-6.0.4/Cargo.lock0000644000000063660000000000100100510ustar # This file is automatically @generated by Cargo. # It is not intended for manual editing. version = 3 [[package]] name = "bytemuck" version = "1.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "374d28ec25809ee0e23827c2ab573d729e293f281dfe393500e7ad618baa61c6" [[package]] name = "cc" version = "1.0.83" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0" dependencies = [ "jobserver", "libc", ] [[package]] name = "dunce" version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56ce8c6da7551ec6c462cbaf3bfbc75131ebbfa1c944aeaa9dab51ca1c5f0c3b" [[package]] name = "foreign-types" version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965" dependencies = [ "foreign-types-macros", "foreign-types-shared", ] [[package]] name = "foreign-types-macros" version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "foreign-types-shared" version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b" [[package]] name = "jobserver" version = "0.1.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8c37f63953c4c63420ed5fd3d6d398c719489b9f872b9fa683262f8edd363c7d" dependencies = [ "libc", ] [[package]] name = "lcms2" version = "6.0.4" dependencies = [ "bytemuck", "foreign-types", "lcms2-sys", ] [[package]] name = "lcms2-sys" version = "4.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "99c5fe876968a1685074a0978632afd4629d7ba40f0ac563388c1a296ce12536" dependencies = [ "cc", "dunce", "libc", "pkg-config", ] [[package]] name = "libc" version = "0.2.150" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "89d92a4743f9a61002fae18374ed11e7973f530cb3a3255fb354818118b2203c" [[package]] name = "pkg-config" version = "0.3.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964" [[package]] name = "proc-macro2" version = "1.0.70" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39278fbbf5fb4f646ce651690877f89d1c5811a3d4acb27700c1cb3cdb78fd3b" dependencies = [ "unicode-ident", ] [[package]] name = "quote" version = "1.0.33" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" dependencies = [ "proc-macro2", ] [[package]] name = "syn" version = "2.0.39" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "23e78b90f2fcf45d3e842032ce32e3f2d1545ba6636271dcbf24fa306d87be7a" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] [[package]] name = "unicode-ident" version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" lcms2-6.0.4/Cargo.toml0000644000000025470000000000100100710ustar # THIS FILE IS AUTOMATICALLY GENERATED BY CARGO # # When uploading crates to the registry Cargo will automatically # "normalize" Cargo.toml files for maximal compatibility # with all versions of Cargo and also rewrite `path` dependencies # to registry (e.g., crates.io) dependencies. # # If you are reading this file be aware that the original Cargo.toml # will likely look very different (and much more reasonable). # See Cargo.toml.orig for the original contents. [package] edition = "2021" name = "lcms2" version = "6.0.4" authors = ["Kornel Lesiński "] include = [ "src/**/*", "Cargo.toml", "README.md", "LICENSE", ] description = "ICC color profile handling. Rusty wrapper for Little CMS" homepage = "https://lib.rs/crates/lcms2" documentation = "https://docs.rs/lcms2" readme = "README.md" keywords = [ "icc", "profile", "color", "lcms", ] categories = [ "multimedia::images", "api-bindings", ] license = "MIT" repository = "https://github.com/kornelski/rust-lcms2.git" [package.metadata.docs.rs] rustdoc-args = ["--generate-link-to-definition"] targets = ["x86_64-unknown-linux-gnu"] [dependencies.bytemuck] version = "1.13.1" [dependencies.foreign-types] version = "0.5" [dependencies.lcms2-sys] version = "4.0.4" [features] static = ["lcms2-sys/static"] [badges.maintenance] status = "actively-developed" lcms2-6.0.4/Cargo.toml.orig000064400000000000000000000015171046102023000135460ustar 00000000000000[package] name = "lcms2" version = "6.0.4" authors = ["Kornel Lesiński "] description = "ICC color profile handling. Rusty wrapper for Little CMS" keywords = ["icc", "profile", "color", "lcms"] include = ["src/**/*", "Cargo.toml", "README.md", "LICENSE"] readme = "README.md" license = "MIT" homepage = "https://lib.rs/crates/lcms2" documentation = "https://docs.rs/lcms2" repository = "https://github.com/kornelski/rust-lcms2.git" categories = ["multimedia::images", "api-bindings"] edition = "2021" [dependencies] bytemuck = "1.13.1" foreign-types = "0.5" lcms2-sys = { path = "./sys", version = "4.0.4" } [features] static = ["lcms2-sys/static"] [package.metadata.docs.rs] targets = ["x86_64-unknown-linux-gnu"] rustdoc-args = ["--generate-link-to-definition"] [badges] maintenance = { status = "actively-developed" } lcms2-6.0.4/LICENSE000064400000000000000000000015671046102023000116710ustar 00000000000000© Kornel Lesiński Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so. THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. lcms2-6.0.4/README.md000064400000000000000000000055331046102023000121400ustar 00000000000000# [Little CMS](http://www.littlecms.com) wrapper for [Rust](https://www.rust-lang.org/) Convert and apply color profiles with a safe abstraction layer for the LCMS library. LCMS2 is a mature, fully-featured color management engine. These bindings have been stable for years, and used in production at scale. See [the API reference](https://docs.rs/lcms2/) for the Rust functions, and the LCMS2 documentation [in HTML](https://kornelski.github.io/rust-lcms2-sys/)/[or the original PDF](https://www.littlecms.com/LittleCMS2.15%20API.pdf) for more background information about the functions. ```rust use lcms2::*; fn example() -> Result<(), std::io::Error> { let icc_file = include_bytes!("custom_profile.icc"); // You can use Profile::new_file("path"), too let custom_profile = Profile::new_icc(icc_file)?; let srgb_profile = Profile::new_srgb(); let t = Transform::new(&custom_profile, PixelFormat::RGB_8, &srgb_profile, PixelFormat::RGB_8, Intent::Perceptual); // Pixel struct must have layout compatible with PixelFormat specified in new() let source_pixels: &[rgb::RGB] = …; t.transform_pixels(source_pixels, destination_pixels); // If input and output pixel formats are the same, you can overwrite them instead of copying t.transform_in_place(source_and_dest_pixels); Ok(()) } ``` To apply an ICC profile from a JPEG: ```rust if b"ICC_PROFILE\0" == &app2_marker_data[0..12] { let icc = &app2_marker_data[14..]; // Lazy assumption that the profile is smaller than 64KB let profile = Profile::new_icc(icc)?; let t = Transform::new(&profile, PixelFormat::RGB_8, &Profile::new_srgb(), PixelFormat::RGB_8, Intent::Perceptual); t.transform_in_place(&mut rgb); } ``` There's more in the `examples` directory. This crate requires Rust 1.64 or later. It's up to date with LCMS 2.15, and should work with a wide range of versions. ## Threads In LCMS all functions are in 2 flavors: global and `*THR()` functions. In this crate this is represented by having functions with `GlobalContext` and `ThreadContext`. Create profiles, transforms, etc. using `*_context()` constructors to give them their private context, which makes them sendable between threads (i.e. they're `Send`). By default `Transform` does not implement `Sync`, because LCMS2 has a thread-unsafe cache in the transform. You can set `Flags::NO_CACHE` to make it safe (this is checked at compile time). ## Upgrading from v5 If you're using a custom RGB type with `Transform`, implement [`bytemuck::Pod`](https://lib.rs/crates/bytemuck) and `Zeroable` for it. Make sure you use arrays or `#[repr(C)]` struct types for pixels. Rust tuples have a technically undefined layout, and can't be used as as a pixel format. ``` unsafe impl Pod for RGB {} unsafe impl Zeroable for RGB {} ``` You don't need to do this if you use the [`rgb` crate](https://lib.rs/crates/rgb). lcms2-6.0.4/src/ciecam.rs000064400000000000000000000037151046102023000132370ustar 00000000000000use crate::*; use std::mem::MaybeUninit; use std::ptr; /// CIE CAM02 #[repr(transparent)] pub struct CIECAM02 { handle: ffi::HANDLE, } impl CIECAM02 { /// A CAM02 object based on given viewing conditions. /// /// Such object may be used as a color appearance model and evaluated in forward and reverse directions. /// Viewing conditions structure is detailed in Table 43. The surround member has to be one of the values enumerated in Table 44. /// Degree of chromatic adaptation (d), can be specified in 0...1.0 range, or the model can be instructed to calculate it by using `D_CALCULATE` constant (-1). /// /// Viewing conditions. /// Please note those are CAM model viewing conditions, and not the ICC tag viewing conditions, which I'm naming `cmsICCViewingConditions` to make differences evident. Unfortunately, the tag cannot deal with surround La, Yb and D value so is basically useless to store CAM02 viewing conditions. pub fn new(conditions: ViewingConditions) -> LCMSResult { let handle = unsafe { ffi::cmsCIECAM02Init(ptr::null_mut(), &conditions) }; if !handle.is_null() { Ok(Self { handle }) } else { Err(Error::ObjectCreationError) } } /// Evaluates the CAM02 model in the forward direction pub fn forward(&mut self, input: &CIEXYZ) -> JCh { unsafe { let mut out = MaybeUninit::uninit(); ffi::cmsCIECAM02Forward(self.handle, input, out.as_mut_ptr()); out.assume_init() } } /// Evaluates the CAM02 model in the reverse direction pub fn reverse(&mut self, input: &JCh) -> CIEXYZ { unsafe { let mut out = MaybeUninit::uninit(); ffi::cmsCIECAM02Reverse(self.handle, input, out.as_mut_ptr()); out.assume_init() } } } impl Drop for CIECAM02 { fn drop(&mut self) { unsafe { ffi::cmsCIECAM02Done(self.handle); } } } lcms2-6.0.4/src/context.rs000064400000000000000000000210071046102023000134740ustar 00000000000000use crate::{ffi, Intent}; use std::cell::UnsafeCell; use std::collections::HashMap; use std::ffi::CStr; use std::fmt; use std::mem; use std::os::raw::c_void; use std::panic::RefUnwindSafe; use std::panic::UnwindSafe; use std::ptr; use std::rc::Rc; use std::sync::Arc; /// A special case for non-thread-aware functions. /// /// This context is used by default and you don't need to create it manually. #[doc(hidden)] pub struct GlobalContext { _not_thread_safe: UnsafeCell, } impl UnwindSafe for GlobalContext {} impl RefUnwindSafe for GlobalContext {} impl UnwindSafe for ThreadContext {} impl RefUnwindSafe for ThreadContext {} #[doc(hidden)] pub trait Context { fn as_ptr(&self) -> ffi::Context; } impl AsRef for GlobalContext { #[inline] fn as_ref(&self) -> &Self { self } } impl AsRef for ThreadContext { #[inline] fn as_ref(&self) -> &Self { self } } impl<'a> Context for &'a GlobalContext { #[inline] fn as_ptr(&self) -> ffi::Context { ptr::null_mut() } } impl Context for GlobalContext { #[inline] fn as_ptr(&self) -> ffi::Context { ptr::null_mut() } } #[doc(hidden)] struct YouMustUseThreadContextToShareBetweenThreads; unsafe impl Send for ThreadContext {} impl<'a> Context for &'a ThreadContext { #[inline] fn as_ptr(&self) -> ffi::Context { self.handle } } impl<'a> Context for Arc { #[inline] fn as_ptr(&self) -> ffi::Context { self.handle } } impl<'a> Context for Rc { #[inline] fn as_ptr(&self) -> ffi::Context { self.handle } } impl Context for ThreadContext { #[inline] fn as_ptr(&self) -> ffi::Context { self.handle } } /// Per-thread context for multi-threaded operation. /// /// There are situations where several instances of Little CMS engine have to coexist but on different conditions. /// For example, when the library is used as a DLL or a shared object, diverse applications may want to use different plug-ins. /// Another example is when multiple threads are being used in same task and the user wants to pass thread-dependent information to the memory allocators or the logging system. /// The context is a pointer to an internal structure that keeps track of all plug-ins and static data needed by the THR corresponding function. /// /// A context-aware app could allocate a new context by calling new() or duplicate a yet-existing one by using clone(). /// Each context can hold different plug-ins, defined by the Plugin parameter. The context can also hold loggers. /// /// Users may associate private data across a void pointer when creating the context, and can retrieve this pointer later. /// /// When you see an error "expected reference, found struct `lcms2::GlobalContext`", it means you've mixed global and thread-context objects. They don't work together. /// For example, if you create a `Transform` with a context (calling `new_*_context()`), then it will only support `Profile` with a context as well. #[repr(transparent)] pub struct ThreadContext { handle: ffi::Context, } impl GlobalContext { #[must_use] pub fn new() -> Self { Self { _not_thread_safe: UnsafeCell::new(YouMustUseThreadContextToShareBetweenThreads), } } pub fn unregister_plugins(&mut self) { unsafe { ffi::cmsUnregisterPlugins(); } } } impl ThreadContext { #[track_caller] #[inline] #[must_use] pub fn new() -> Self { unsafe { Self::new_handle(ffi::cmsCreateContext(ptr::null_mut(), ptr::null_mut())) } } #[track_caller] #[inline] unsafe fn new_handle(handle: ffi::Context) -> Self { assert!(!handle.is_null()); Self { handle } } #[must_use] pub fn user_data(&self) -> *mut c_void { unsafe { ffi::cmsGetContextUserData(self.handle) } } pub unsafe fn install_plugin(&mut self, plugin: *mut c_void) -> bool { 0 != ffi::cmsPluginTHR(self.handle, plugin) } pub fn unregister_plugins(&mut self) { unsafe { ffi::cmsUnregisterPluginsTHR(self.handle); } } #[track_caller] #[inline] #[must_use] pub fn supported_intents(&self) -> HashMap { let mut codes = [0u32; 32]; let mut descs = [ptr::null_mut(); 32]; let len = unsafe { debug_assert_eq!(mem::size_of::(), mem::size_of::()); ffi::cmsGetSupportedIntentsTHR(self.handle, 32, codes.as_mut_ptr(), descs.as_mut_ptr()) }; debug_assert!(len <= 32); codes.iter().zip(descs.iter()).take(len as usize).filter_map(|(&code, &desc)|{ use Intent::*; let code = match code { c if c == Perceptual as u32 => Perceptual, c if c == RelativeColorimetric as u32 => RelativeColorimetric, c if c == Saturation as u32 => Saturation, c if c == AbsoluteColorimetric as u32 => AbsoluteColorimetric, c if c == PreserveKOnlyPerceptual as u32 => PreserveKOnlyPerceptual, c if c == PreserveKOnlyRelativeColorimetric as u32 => PreserveKOnlyRelativeColorimetric, c if c == PreserveKOnlySaturation as u32 => PreserveKOnlySaturation, c if c == PreserveKPlanePerceptual as u32 => PreserveKPlanePerceptual, c if c == PreserveKPlaneRelativeColorimetric as u32 => PreserveKPlaneRelativeColorimetric, c if c == PreserveKPlaneSaturation as u32 => PreserveKPlaneSaturation, _ => return None, }; Some((code, unsafe { CStr::from_ptr(desc) })) }).collect() } /// Adaptation state for absolute colorimetric intent, on all but `cmsCreateExtendedTransform`. #[must_use] pub fn adaptation_state(&self) -> f64 { unsafe { ffi::cmsSetAdaptationStateTHR(self.handle, -1.) } } /// Sets adaptation state for absolute colorimetric intent in the given context. Adaptation state applies on all but `cmsCreateExtendedTransformTHR`(). /// Little CMS can handle incomplete adaptation states. /// /// Degree on adaptation 0=Not adapted, 1=Complete adaptation, in-between=Partial adaptation. pub fn set_adaptation_state(&mut self, value: f64) { unsafe { ffi::cmsSetAdaptationStateTHR(self.handle, value); } } /// Sets the codes used to mark out-out-gamut on Proofing transforms for a given context. Values are meant to be encoded in 16 bits. /// /// `AlarmCodes`: `Array [16]` of codes. ALL 16 VALUES MUST BE SPECIFIED, set to zero unused channels. #[inline] pub fn set_alarm_codes(&mut self, codes: [u16; ffi::MAXCHANNELS]) { unsafe { ffi::cmsSetAlarmCodesTHR(self.handle, codes.as_ptr()) } } /// Gets the current codes used to mark out-out-gamut on Proofing transforms for the given context. Values are meant to be encoded in 16 bits. #[must_use] #[inline] pub fn alarm_codes(&self) -> [u16; ffi::MAXCHANNELS] { let mut tmp = [0u16; ffi::MAXCHANNELS]; unsafe { ffi::cmsGetAlarmCodesTHR(self.handle, tmp.as_mut_ptr()); } tmp } /// Sets a function to be called if there is an error. pub fn set_error_logging_function(&mut self, handler: ffi::LogErrorHandlerFunction) { unsafe { ffi::cmsSetLogErrorHandlerTHR(self.handle, handler); } } } impl Clone for ThreadContext { #[inline] fn clone(&self) -> Self { unsafe { Self::new_handle(ffi::cmsDupContext(self.handle, ptr::null_mut())) } } } impl Drop for ThreadContext { fn drop(&mut self) { unsafe { ffi::cmsDeleteContext(self.handle) } } } impl Default for GlobalContext { #[inline] fn default() -> Self { Self::new() } } impl Default for ThreadContext { #[inline] fn default() -> Self { Self::new() } } impl fmt::Debug for ThreadContext { #[cold] fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { f.write_str("ThreadContext") } } impl fmt::Debug for GlobalContext { #[cold] fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { f.write_str("GlobalContext") } } #[test] fn context() { let mut c = ThreadContext::new(); assert!(c.user_data().is_null()); c.unregister_plugins(); assert!(crate::Profile::new_icc_context(&c, &[]).is_err()); assert!(c.supported_intents().contains_key(&Intent::RelativeColorimetric)); let _ = GlobalContext::default(); } lcms2-6.0.4/src/error.rs000064400000000000000000000021531046102023000131420ustar 00000000000000use foreign_types::ForeignType; use std::error::Error as StdError; use std::fmt; #[derive(Debug, Copy, Clone, Eq, PartialEq)] pub enum Error { ObjectCreationError, MissingData, InvalidString, } impl Error { #[inline] pub(crate) unsafe fn if_null(handle: *mut ::CType) -> LCMSResult where T: ForeignType { if !handle.is_null() { Ok(T::from_ptr(handle)) } else { Err(Error::ObjectCreationError) } } } /// This is a regular `Result` type with LCMS-specific `Error` pub type LCMSResult = Result; impl fmt::Display for Error { #[cold] fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.write_str(match *self { Error::ObjectCreationError => "Could not create the object.\nThe reason is not known, but it's usually caused by wrong input parameters.", Error::InvalidString => "String is not valid. Contains unsupported characters or is too long.", Error::MissingData => "Requested data is empty or does not exist.", }) } } impl StdError for Error { } lcms2-6.0.4/src/eval.rs000064400000000000000000000031221046102023000127350ustar 00000000000000use crate::ffi; pub trait FloatOrU16: Sized + Copy { unsafe fn eval_tone_curve(self, handle: *const ffi::ToneCurve) -> Self; unsafe fn eval_pipeline(handle: *const ffi::Pipeline, input: &[Self], out: &mut [Self]); unsafe fn stage_alloc_clut(contextid: ffi::Context, ngridpoints: u32, inputchan: u32, outputchan: u32, table: *const Self) -> *mut ffi::Stage; } impl FloatOrU16 for f32 { #[inline] unsafe fn eval_tone_curve(self, handle: *const ffi::ToneCurve) -> Self { ffi::cmsEvalToneCurveFloat(handle, self) } #[inline] unsafe fn eval_pipeline(handle: *const ffi::Pipeline, input: &[Self], out: &mut [Self]) { ffi::cmsPipelineEvalFloat(input.as_ptr(), out.as_mut_ptr(), handle); } #[inline] unsafe fn stage_alloc_clut(contextid: ffi::Context, ngridpoints: u32, inputchan: u32, outputchan: u32, table: *const Self) -> *mut ffi::Stage { ffi::cmsStageAllocCLutFloat(contextid, ngridpoints, inputchan, outputchan, table) } } impl FloatOrU16 for u16 { #[inline] unsafe fn eval_tone_curve(self, handle: *const ffi::ToneCurve) -> Self { ffi::cmsEvalToneCurve16(handle, self) } #[inline] unsafe fn eval_pipeline(handle: *const ffi::Pipeline, input: &[Self], out: &mut [Self]) { ffi::cmsPipelineEval16(input.as_ptr(), out.as_mut_ptr(), handle); } #[inline] unsafe fn stage_alloc_clut(contextid: ffi::Context, ngridpoints: u32, inputchan: u32, outputchan: u32, table: *const Self) -> *mut ffi::Stage { ffi::cmsStageAllocCLut16bit(contextid, ngridpoints, inputchan, outputchan, table) } } lcms2-6.0.4/src/ext.rs000064400000000000000000000204341046102023000126130ustar 00000000000000use crate::{ffi, CIELab, CIExyY, ColorSpaceSignature, CIEXYZ}; use ffi::PixelType; use std::mem::MaybeUninit; pub trait ColorSpaceSignatureExt: Sized + Copy { /// Returns channel count for a given color space. /// /// Returns 3 on error (sic). fn channels(self) -> u32; /// Converts from ICC color space notation (LCMS PDF Table 10) to Little CMS color space notation (LCMS PDF Table 36). fn pixel_type(self) -> PixelType; } impl ColorSpaceSignatureExt for ColorSpaceSignature { #[inline] fn channels(self) -> u32 { unsafe { ffi::cmsChannelsOf(self) } } #[inline] fn pixel_type(self) -> PixelType { PixelType(unsafe { ffi::_cmsLCMScolorSpace(self) } as u32) } } /// Chromatic adaptation pub trait CIEXYZExt: Sized { /// Adapts a color to a given illuminant. Original color is expected to have /// a `source_white_point` white point. fn adapt_to_illuminant(&self, source_white_point: &CIEXYZ, illuminant: &CIEXYZ) -> Option; /// Colorimetric space conversion. fn to_lab(&self, white_point: &CIEXYZ) -> CIELab; /// Decodes a XYZ value, encoded on ICC convention fn from_encoded(icc: &[u16; 3]) -> Self; } impl CIEXYZExt for CIEXYZ { #[inline] fn adapt_to_illuminant(&self, source_white_point: &CIEXYZ, illuminant: &CIEXYZ) -> Option { unsafe { let mut res = MaybeUninit::::uninit(); let ok = ffi::cmsAdaptToIlluminant(res.as_mut_ptr(), source_white_point, illuminant, self) != 0; if ok { Some(res.assume_init()) } else { None } } } #[inline] fn to_lab(&self, white_point: &CIEXYZ) -> CIELab { unsafe { let mut out = MaybeUninit::::uninit(); ffi::cmsXYZ2Lab(white_point, out.as_mut_ptr(), self); out.assume_init() } } #[inline] fn from_encoded(icc: &[u16; 3]) -> Self { unsafe { let mut out = MaybeUninit::::uninit(); ffi::cmsXYZEncoded2Float(out.as_mut_ptr(), icc.as_ptr()); out.assume_init() } } } /// White point pub trait CIExzYExt: Sized { /// Correlates a black body temperature in ºK from given chromaticity. fn temp(&self) -> Option; } impl CIExzYExt for CIExyY { #[inline] fn temp(&self) -> Option { let mut out = 0.; if 0 != unsafe { ffi::cmsTempFromWhitePoint(&mut out, self) } { Some(out) } else { None } } } /// Delta E pub trait CIELabExt: Sized { /// Delta-E 2000 is the first major revision of the dE94 equation. /// /// Unlike dE94, which assumes that L\* correctly reflects the perceived differences in lightness, dE2000 varies the weighting of L\* depending on where in the lightness range the color falls. /// dE2000 is still under consideration and does not seem to be widely supported in graphics arts applications. /// /// Returns: /// The CIE2000 dE metric value. fn cie2000_delta_e(&self, other: &CIELab, kl: f64, kc: f64, kh: f64) -> f64; /// A technical committee of the CIE (TC1-29) published an equation in 1995 called CIE94. /// The equation is similar to CMC but the weighting functions are largely based on RIT/DuPont tolerance data derived from automotive paint experiments where sample surfaces are smooth. /// It also has ratios, labeled kL (lightness) and Kc (chroma) and the commercial factor (cf) but these tend to be preset in software and are not often exposed for the user (as it is the case in Little CMS). /// Returns: /// The CIE94 dE metric value. fn cie94_delta_e(&self, other: &CIELab) -> f64; /// BFD delta E metric. fn bfd_delta_e(&self, other: &CIELab) -> f64; /// The dE76 metric value. /// /// The L\*a\*b\* color space was devised in 1976 and, at the same time delta-E 1976 (dE76) came into being. /// If you can imagine attaching a string to a color point in 3D Lab space, dE76 describes the sphere that is described by all the possible directions you could pull the string. /// If you hear people speak of just plain 'delta-E' they are probably referring to dE76. It is also known as dE-Lab and dE-ab. /// /// One problem with dE76 is that Lab itself is not 'perceptually uniform' as its creators had intended. /// So different amounts of visual color shift in different color areas of Lab might have the same dE76 number. /// Conversely, the same amount of color shift might result in different dE76 values. /// Another issue is that the eye is most sensitive to hue differences, then chroma and finally lightness and dE76 does not take this into account. fn delta_e(&self, other: &CIELab) -> f64; /// In 1984 the CMC (Colour Measurement Committee of the Society of Dyes and Colourists of Great Britain) developed and adopted an equation based on LCH numbers. /// /// Intended for the textiles industry, CMC l:c allows the setting of lightness (l) and chroma (c) factors. As the eye is more sensitive to chroma, the default ratio for l:c is 2:1 allowing for 2x the difference in lightness than chroma (numbers). There is also a 'commercial factor' (cf) which allows an overall varying of the size of the tolerance region according to accuracy requirements. A cf=1.0 means that a delta-E CMC value <1.0 is acceptable. /// CMC l:c is designed to be used with D65 and the CIE Supplementary Observer. Commonly-used values for l:c are 2:1 for acceptability and 1:1 for the threshold of imperceptibility. fn cmc_delta_e(&self, other: &CIELab, k: f64, c: f64) -> f64; /// amin, amax, bmin, bmax: boundaries of gamut rectangle fn desaturate(&mut self, amin: f64, amax: f64, bmin: f64, bmax: f64) -> bool; /// Encodes a Lab value, from a CIELab value to ICC v4 convention. fn encoded(&self) -> [u16; 3]; /// Encodes a Lab value, from a CIELab value to ICC v2 convention. fn encoded_v2(&self) -> [u16; 3]; /// Decodes a Lab value, encoded on ICC v4 convention fn from_encoded(icc: &[u16; 3]) -> Self; /// Decodes a Lab value, encoded on ICC v2 convention fn from_encoded_v2(icc: &[u16; 3]) -> Self; /// Colorimetric space conversion. fn to_xyz(&self, white_point: &CIEXYZ) -> CIEXYZ; } impl CIELabExt for CIELab { #[inline] fn cie2000_delta_e(&self, other: &CIELab, kl: f64, kc: f64, kh: f64) -> f64 { unsafe { ffi::cmsCIE2000DeltaE(self, other, kl, kc, kh) } } #[inline] fn cie94_delta_e(&self, other: &CIELab) -> f64 { unsafe { ffi::cmsCIE94DeltaE(self, other) } } #[inline] fn bfd_delta_e(&self, other: &CIELab) -> f64 { unsafe { ffi::cmsBFDdeltaE(self, other) } } #[inline] fn delta_e(&self, other: &CIELab) -> f64 { unsafe { ffi::cmsDeltaE(self, other) } } #[inline] fn cmc_delta_e(&self, other: &CIELab, k: f64, c: f64) -> f64 { unsafe { ffi::cmsCMCdeltaE(self, other, k, c) } } #[inline] fn desaturate(&mut self, amin: f64, amax: f64, bmin: f64, bmax: f64) -> bool { unsafe { 0 != ffi::cmsDesaturateLab(self, amax, amin, bmax, bmin) } } #[inline] fn encoded(&self) -> [u16; 3] { let mut out = [0u16; 3]; unsafe { ffi::cmsFloat2LabEncoded(out.as_mut_ptr(), self) } out } #[inline] fn encoded_v2(&self) -> [u16; 3] { let mut out = [0u16; 3]; unsafe { ffi::cmsFloat2LabEncodedV2(out.as_mut_ptr(), self) } out } #[inline] fn from_encoded(icc: &[u16; 3]) -> Self { unsafe { let mut out = MaybeUninit::::uninit(); ffi::cmsLabEncoded2Float(out.as_mut_ptr(), icc.as_ptr()); out.assume_init() } } #[inline] fn from_encoded_v2(icc: &[u16; 3]) -> Self { unsafe { let mut out = MaybeUninit::::uninit(); ffi::cmsLabEncoded2FloatV2(out.as_mut_ptr(), icc.as_ptr()); out.assume_init() } } #[inline] fn to_xyz(&self, white_point: &CIEXYZ) -> CIEXYZ { unsafe { let mut out = MaybeUninit::::uninit(); ffi::cmsLab2XYZ(white_point, out.as_mut_ptr(), self); out.assume_init() } } } #[test] fn temp() { assert!(crate::white_point_from_temp(4000.).is_some()); } lcms2-6.0.4/src/flags.rs000064400000000000000000000113011046102023000131000ustar 00000000000000use crate::ffi; use std::ops; #[derive(Debug, Copy, Clone)] /// Flags for creating `Transform`. Can be OR-ed together with `|`. /// /// There's a special `NO_CACHE` flag that enables sharing transform between threads. pub struct Flags(pub u32, T); impl Flags { /// Inhibit 1-pixel cache. This is required to make `Transform` implement `Sync` pub const NO_CACHE: Flags = Flags(ffi::FLAGS_NOCACHE, DisallowCache); /// Inhibit optimizations pub const NO_OPTIMIZE: Flags = Flags(ffi::FLAGS_NOOPTIMIZE, AllowCache); /// Don't transform anyway pub const NULL_TRANSFORM: Flags = Flags(ffi::FLAGS_NULLTRANSFORM, AllowCache); /// Proofing flags /// Out of Gamut alarm pub const GAMUT_CHECK: Flags = Flags(ffi::FLAGS_GAMUTCHECK, AllowCache); /// Do softproofing pub const SOFT_PROOFING: Flags = Flags(ffi::FLAGS_SOFTPROOFING, AllowCache); // Misc pub const BLACKPOINT_COMPENSATION: Flags = Flags(ffi::FLAGS_BLACKPOINTCOMPENSATION, AllowCache); /// Don't fix scum dot pub const NO_WHITE_ON_WHITE_FIXUP: Flags = Flags(ffi::FLAGS_NOWHITEONWHITEFIXUP, AllowCache); /// Use more memory to give better accurancy pub const HIGHRES_PRECALC: Flags = Flags(ffi::FLAGS_HIGHRESPRECALC, AllowCache); /// Use less memory to minimize resources pub const LOWRES_PRECALC: Flags = Flags(ffi::FLAGS_LOWRESPRECALC, AllowCache); /// For devicelink creation /// Create 8 bits devicelinks pub const DEVICELINK_8BITS: Flags = Flags(ffi::FLAGS_8BITS_DEVICELINK, AllowCache); /// Guess device class (for transform2devicelink) pub const GUESS_DEVICE_CLASS: Flags = Flags(ffi::FLAGS_GUESSDEVICECLASS, AllowCache); /// Keep profile sequence for devicelink creation pub const KEEP_SEQUENCE: Flags = Flags(ffi::FLAGS_KEEP_SEQUENCE, AllowCache); /// Specific to a particular optimizations /// Force CLUT optimization pub const FORCE_CLUT: Flags = Flags(ffi::FLAGS_FORCE_CLUT, AllowCache); /// create postlinearization tables if possible pub const CLUT_POST_LINEARIZATION: Flags = Flags(ffi::FLAGS_CLUT_POST_LINEARIZATION, AllowCache); /// create prelinearization tables if possible pub const CLUT_PRE_LINEARIZATION: Flags = Flags(ffi::FLAGS_CLUT_PRE_LINEARIZATION, AllowCache); /// Specific to unbounded mode /// Prevent negative numbers in floating point transforms pub const NO_NEGATIVES: Flags = Flags(ffi::FLAGS_NONEGATIVES, AllowCache); /// Alpha channels are copied on `cmsDoTransform()` pub const COPY_ALPHA: Flags = Flags(ffi::FLAGS_COPY_ALPHA, AllowCache); /// CRD special pub const NO_DEFAULT_RESOURCE_DEF: Flags = Flags(ffi::FLAGS_NODEFAULTRESOURCEDEF, AllowCache); } impl ops::BitOr> for Flags { type Output = Flags; fn bitor(self, other: Flags) -> Flags { Flags(self.0 | other.0, DisallowCache) } } impl ops::BitOr> for Flags { type Output = Flags; fn bitor(self, other: Flags) -> Flags { Flags(self.0 | other.0, other.1) } } impl Flags { pub(crate) fn bits(&self) -> u32 { self.0 } pub(crate) fn allow_cache(&self) -> Flags { Flags(self.0, AllowCache) } pub fn has(&self, flag: Flags) -> bool { 0 != (self.0 & flag.0) } } impl Default for Flags { /// Default flags /// /// By default allows non-thread-safe cache, which improves performance, but limits transforms to use by one thread only. #[inline] fn default() -> Self { Flags(0, AllowCache) } } #[derive(Copy, Clone, Debug)] #[doc(hidden)] /// Special marker used to vary `Flags` at compile time pub struct DisallowCache; #[derive(Copy, Clone, Debug)] #[doc(hidden)] /// Special marker used to vary `Flags` at compile time pub struct AllowCache; /// Used by `Flags` to keep track whether `Flags::NO_CACHE` has been used, /// which enables thread-safe sharing of `Transform`s. /// /// Valid values are `AllowCache` and `DisallowCache`, /// but you won't need to use them directly. Just use `Flags`' constants. pub trait CacheFlag: Sized {} impl CacheFlag for AllowCache {} impl CacheFlag for DisallowCache {} #[test] fn flags() { let _ = Flags::default(); let mut t = Flags::COPY_ALPHA | Flags::NO_OPTIMIZE; t = t | Flags::CLUT_PRE_LINEARIZATION; assert!(t.has(Flags::CLUT_PRE_LINEARIZATION)); assert!(t.has(Flags::COPY_ALPHA)); assert!(t.has(Flags::NO_OPTIMIZE)); assert!(!t.has(Flags::DEVICELINK_8BITS)); assert!(!t.has(Flags::GAMUT_CHECK)); let _ = Flags::default() | Flags::NO_CACHE; let _ = Flags::NO_CACHE | Flags::CLUT_PRE_LINEARIZATION; } lcms2-6.0.4/src/lib.rs000064400000000000000000000106111046102023000125550ustar 00000000000000//! See [Little CMS full documentation](https://kornelski.github.io/rust-lcms2-sys/) for more in-depth information about LCMS functions. //! //! The main types you need to use in this crate are `Profile` and `Transform` #![doc(html_logo_url = "https://kornelski.github.io/rust-lcms2/lcms_logo.png")] #![doc(html_root_url = "https://kornelski.github.io/rust-lcms2")] #![allow(clippy::cast_possible_truncation)] #![allow(clippy::cast_possible_wrap)] #![allow(clippy::doc_markdown)] #![allow(clippy::enum_glob_use)] #![allow(clippy::if_not_else)] #![allow(clippy::map_unwrap_or)] #![allow(clippy::missing_errors_doc)] #![allow(clippy::missing_panics_doc)] #![allow(clippy::missing_safety_doc)] #![allow(clippy::module_name_repetitions)] #![allow(clippy::redundant_closure_for_method_calls)] #![allow(clippy::too_many_arguments)] #![allow(clippy::unreadable_literal)] #![allow(clippy::upper_case_acronyms)] #![allow(clippy::wildcard_imports)] use lcms2_sys as ffi; mod ciecam; mod context; mod error; mod eval; mod ext; mod flags; mod locale; mod mlu; mod namedcolorlist; mod pipeline; mod profile; mod stage; mod tag; mod tonecurve; mod transform; use std::marker::PhantomData; /// `Transform` requires pixel types to implement these traits. /// /// This is necesary to prevent unsafe writes to abitrary types with pointers or padding. pub use bytemuck::{Pod, Zeroable}; pub use crate::ciecam::*; pub use crate::context::{GlobalContext, ThreadContext}; pub use crate::error::*; pub use crate::ext::*; pub use crate::flags::*; pub use crate::locale::*; pub use crate::mlu::*; pub use crate::namedcolorlist::*; pub use crate::pipeline::*; pub use crate::profile::*; pub use crate::stage::*; pub use crate::tonecurve::*; pub use crate::transform::*; pub use crate::ffi::CIELab; /// Part of [`CIExyYTRIPLE`] pub use crate::ffi::CIExyY; /// For [`Profile::new_rgb`] pub use crate::ffi::CIExyYTRIPLE; #[doc(hidden)] pub use crate::ffi::JCh; pub use crate::ffi::CIEXYZ; pub use crate::ffi::ColorSpaceSignature; pub use crate::ffi::InfoType; pub use crate::ffi::Intent; pub use crate::ffi::PixelFormat; pub use crate::ffi::ProfileClassSignature; pub use crate::ffi::TagSignature; pub use crate::ffi::VideoSignalType; pub use crate::ffi::ViewingConditions; #[derive(Debug)] #[non_exhaustive] /// Value of a tag in an ICC profile pub enum Tag<'a> { CIExyYTRIPLE(&'a ffi::CIExyYTRIPLE), CIEXYZ(&'a ffi::CIEXYZ), ICCData(&'a ffi::ICCData), ICCMeasurementConditions(&'a ffi::ICCMeasurementConditions), ICCViewingConditions(&'a ffi::ICCViewingConditions), /// Unicode string MLU(&'a mlu::MLURef), /// A palette NamedColorList(&'a NamedColorListRef), Pipeline(&'a PipelineRef), Screening(&'a ffi::Screening), SEQ(&'a ffi::SEQ), Intent(Intent), ColorimetricIntentImageState(ffi::ColorimetricIntentImageState), Technology(ffi::TechnologySignature), ToneCurve(&'a ToneCurveRef), UcrBg(&'a ffi::UcrBg), VcgtCurves([&'a ToneCurveRef; 3]), VideoSignal(&'a ffi::VideoSignalType), /// Unknown format or missing data None, } /// LCMS version #[must_use] pub fn version() -> u32 { unsafe { ffi::cmsGetEncodedCMMversion() as u32 } } /// Temperature <-> Chromaticity (Black body) /// Color temperature is a characteristic of visible light that has important applications. /// /// The color temperature of a light source is determined by comparing its chromaticity with that of an ideal black-body radiator. /// The temperature (usually measured in kelvin, K) is that source's color temperature at which the heated black-body radiator matches the color of the light source for a black body source. /// Higher color temperatures (5,000 K or more) are cool (bluish white) colors, and lower color temperatures (2,700–3,000 K) warm (yellowish white through red) colors. /// /// See `CIExzYExt::temp()` #[must_use] pub fn white_point_from_temp(temp: f64) -> Option { let mut res = CIExyY{x:0.,y:0.,Y:0.}; let ok = unsafe { ffi::cmsWhitePointFromTemp(&mut res, temp) != 0 }; if ok { Some(res) } else { None } } #[allow(non_snake_case)] #[must_use] pub fn xyY2XYZ(xyY: &CIExyY) -> CIEXYZ { let mut xyz = CIEXYZ::default(); unsafe { crate::ffi::cmsxyY2XYZ(&mut xyz, xyY); } xyz } #[allow(non_snake_case)] #[must_use] pub fn XYZ2xyY(xyz: &CIEXYZ) -> CIExyY { let mut xyY = CIExyY::default(); unsafe { crate::ffi::cmsXYZ2xyY(&mut xyY, xyz); } xyY } lcms2-6.0.4/src/locale.rs000064400000000000000000000060501046102023000132500ustar 00000000000000use std::cmp; use std::fmt; use std::fmt::Write; use std::os::raw::c_char; /// Language code from ISO-639/2 and region code from ISO-3166. #[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd)] pub struct Locale { language: [c_char; 3], country: [c_char; 3], } impl Locale { /// A string in format: 2-letter language name, separator, 2-letter country name, e.g. "en_US" #[must_use] pub fn new(locale_name: &str) -> Self { let (language_str, country_str) = locale_name.split_at(cmp::min(locale_name.len(), 3)); let mut locale = Locale { language: [0; 3], country: [0; 3], }; for (c, s) in locale.language.iter_mut().zip(language_str.bytes().take(2)) { *c = s as c_char; } for (c, s) in locale.country.iter_mut().zip(country_str.bytes().take(2)) { *c = s as c_char; } locale } /// Default/unspecified/any locale #[must_use] #[inline] pub fn none() -> Self { Locale { language: [0; 3], country: [0; 3], } } pub(crate) fn language_ptr(&self) -> *const c_char { &self.language as _ } pub(crate) fn country_ptr(&self) -> *const c_char { &self.country as _ } pub(crate) fn language_ptr_mut(&mut self) -> *mut c_char { std::ptr::addr_of!(self.language) as _ } pub(crate) fn country_ptr_mut(&mut self) -> *mut c_char { std::ptr::addr_of!(self.country) as _ } } impl<'a> From<&'a str> for Locale { fn from(s: &'a str) -> Self { Locale::new(s) } } impl Default for Locale { fn default() -> Self { Locale::none() } } impl fmt::Debug for Locale { #[cold] fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { ::fmt(self, f) } } impl fmt::Display for Locale { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { for &c in self.language.iter().take_while(|&&c| c != 0) { f.write_char(c as u8 as char)?; } f.write_char('_')?; for &c in self.country.iter().take_while(|&&c| c != 0) { f.write_char(c as u8 as char)?; } Ok(()) } } #[test] fn locale() { let l = Locale::new(""); assert_eq!([0 as c_char; 3], l.language); assert_eq!([0 as c_char; 3], l.country); let l = Locale::none(); assert_eq!([0 as c_char; 3], l.language); assert_eq!([0 as c_char; 3], l.country); let l = Locale::new("Ab"); assert_eq!(['A' as c_char, 'b' as c_char, 0], l.language); assert_eq!([0 as c_char; 3], l.country); let l = Locale::new("Ab-X"); assert_eq!(['A' as c_char, 'b' as c_char, 0], l.language); assert_eq!(['X' as c_char, 0, 0], l.country); let l = Locale::new("overlong"); assert_eq!(['o' as c_char, 'v' as c_char, 0], l.language); assert_eq!(['r' as c_char, 'l' as c_char, 0], l.country); unsafe { assert_eq!('o' as c_char, *l.language_ptr()); } unsafe { assert_eq!('r' as c_char, *l.country_ptr()); } } lcms2-6.0.4/src/mlu.rs000064400000000000000000000140271046102023000126110ustar 00000000000000use crate::ffi::wchar_t; use crate::{ffi, Error, LCMSResult, Locale}; use foreign_types::{foreign_type, ForeignType, ForeignTypeRef}; use std::char::{decode_utf16, REPLACEMENT_CHARACTER}; use std::ffi::CString; use std::fmt; use std::mem; use std::ptr; foreign_type! { /// This represents owned Multi Localized Unicode type. Most methods are implemented on `MLURef`. /// This is a borrwed Multi Localized Unicode type. It holds Unicode strings associated with `Locale`. pub unsafe type MLU { type CType = ffi::MLU; fn drop = ffi::cmsMLUfree; } } impl MLU { /// Allocates an empty multilocalized unicode object. #[track_caller] #[inline] #[must_use] pub fn new(items: usize) -> Self { unsafe { let handle = ffi::cmsMLUalloc(ptr::null_mut(), items as u32); assert!(!handle.is_null()); MLU::from_ptr(handle) } } } impl MLURef { /// Fills an ASCII (7 bit) entry for the given Language and country. pub fn set_text_ascii(&mut self, text: &str, locale: Locale) -> bool { let Ok(cstr) = CString::new(text) else { return false }; unsafe { ffi::cmsMLUsetASCII( (self as *mut Self).cast(), locale.language_ptr(), locale.country_ptr(), cstr.as_ptr(), ) != 0 } } /// Fills a UNICODE wide char (16 bit) entry for the given Language and country. pub fn set_text(&mut self, text: &str, locale: Locale) -> bool { let chars: Vec<_> = text .chars() .map(|c| c as wchar_t) .chain([0 as wchar_t]) .collect(); unsafe { ffi::cmsMLUsetWide( (self as *mut Self).cast(), locale.language_ptr(), locale.country_ptr(), chars[..].as_ptr(), ) != 0 } } /// Gets an ASCII (7 bit) entry for the given Language and country. pub fn text_ascii(&self, locale: Locale) -> LCMSResult { let len = unsafe { ffi::cmsMLUgetASCII( self.as_ptr(), locale.language_ptr(), locale.country_ptr(), ptr::null_mut(), 0, ) }; if len == 0 { return Err(Error::MissingData); } let mut buf = vec![0u8; len as usize]; unsafe { ffi::cmsMLUgetASCII( self.as_ptr(), locale.language_ptr(), locale.country_ptr(), buf[..].as_mut_ptr().cast(), len, ); if let Some(0) = buf.pop() { // terminating zero String::from_utf8(buf).map_err(|_| Error::InvalidString) } else { Err(Error::InvalidString) } } } /// Gets a Unicode entry for the given Language and country pub fn text(&self, locale: Locale) -> LCMSResult { let len_bytes = unsafe { ffi::cmsMLUgetWide( self.as_ptr(), locale.language_ptr(), locale.country_ptr(), ptr::null_mut(), 0, ) }; let len_wchars = len_bytes as usize / mem::size_of::(); if len_wchars == 0 || (len_bytes & 1) != 0 { return Err(Error::MissingData); } let mut buf = vec![0 as wchar_t; len_wchars]; unsafe { ffi::cmsMLUgetWide( self.as_ptr(), locale.language_ptr(), locale.country_ptr(), buf[..].as_mut_ptr().cast::(), len_bytes, ); if let Some(0) = buf.pop() { // terminating zero Ok(decode_utf16(buf.into_iter().map(|c| c as u16)) .map(|r| r.unwrap_or(REPLACEMENT_CHARACTER)) .collect()) } else { Err(Error::InvalidString) } } } /// Obtains the translations stored in a given multilocalized unicode object. #[must_use] pub fn tanslations(&self) -> Vec { let count = unsafe { ffi::cmsMLUtranslationsCount(self.as_ptr()) }; let mut out = vec![Locale::none(); count as usize]; let mut i = 0; out.retain_mut(|locale| { let ok = unsafe { ffi::cmsMLUtranslationsCodes( self.as_ptr(), i, locale.language_ptr_mut(), locale.country_ptr_mut(), ) != 0 }; i += 1; ok }); out } /// Obtains the translation rule for given multilocalized unicode object. pub fn tanslation(&self, locale: Locale) -> LCMSResult { let mut out = Locale::none(); if unsafe { ffi::cmsMLUgetTranslation( self.as_ptr(), locale.language_ptr(), locale.country_ptr(), out.language_ptr_mut(), out.country_ptr_mut(), ) != 0 } { Ok(out) } else { Err(Error::MissingData) } } } impl fmt::Debug for MLURef { #[cold] fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let t = self.text(Locale::none()); write!( f, "MLU({:?} {:?})", if let Ok(ref t) = t { t } else { "None" }, self.tanslations() ) } } #[test] fn mlu() { let _ = MLU::new(0); let mut m = MLU::new(1); assert!(m.set_text("Hello 世界!", Locale::none())); assert_eq!(Ok("Hello 世界!".to_owned()), m.text(Locale::none())); assert!(!m.set_text_ascii("エッロル", Locale::none())); assert!(m.set_text("a", Locale::new("en_US"))); assert_eq!("a", m.text_ascii(Locale::new("en_US")).unwrap()); let mut m = MLU::new(1); assert!(m.set_text_ascii("OK", Locale::none())); assert_eq!("OK", m.text_ascii(Locale::none()).unwrap()); } lcms2-6.0.4/src/namedcolorlist.rs000064400000000000000000000074601046102023000150360ustar 00000000000000use crate::{ffi, Error, LCMSResult}; use foreign_types::{foreign_type, ForeignTypeRef}; use std::ffi::{CStr, CString}; use std::fmt; use std::os::raw::c_char; use std::ptr; #[derive(Clone, Debug, Eq, PartialEq)] /// Color in the palette pub struct NamedColorInfo { pub name: String, pub prefix: String, pub suffix: String, pub pcs: [u16; 3], pub colorant: [u16; 16], } foreign_type! { /// Palette of colors with names pub unsafe type NamedColorList { type CType = ffi::NAMEDCOLORLIST; fn drop = ffi::cmsFreeNamedColorList; } } impl NamedColorList { pub fn new(spot_colors: usize, colorant_count: usize, prefix: &str, suffix: &str) -> LCMSResult { let prefix = CString::new(prefix).map_err(|_| Error::InvalidString)?; let suffix = CString::new(suffix).map_err(|_| Error::InvalidString)?; unsafe { Error::if_null(ffi::cmsAllocNamedColorList( ptr::null_mut(), spot_colors as u32, colorant_count as u32, prefix.as_ptr().cast(), suffix.as_ptr().cast(), )) // char sign difference } } } impl NamedColorListRef { /// Number of colors in the palette #[inline] fn len(&self) -> usize { unsafe { ffi::cmsNamedColorCount(self.as_ptr()) as usize } } /// Find color by name #[must_use] pub fn index_of(&self, color_name: &str) -> usize { let s = CString::new(color_name).unwrap(); unsafe { ffi::cmsNamedColorIndex(self.as_ptr(), s.as_ptr()).try_into().unwrap() } } /// Get color info #[must_use] pub fn get(&self, index: usize) -> Option { let mut name = [0 as c_char; 256]; let mut prefix = [0 as c_char; 33]; let mut suffix = [0 as c_char; 33]; let mut pcs = [0u16; 3]; let mut colorant = [0u16; 16]; let ok = unsafe { 0 != ffi::cmsNamedColorInfo( self.as_ptr(), index as u32, name.as_mut_ptr(), prefix.as_mut_ptr(), suffix.as_mut_ptr(), pcs.as_mut_ptr(), colorant.as_mut_ptr(), ) }; if ok { Some(unsafe {NamedColorInfo { name: CStr::from_ptr(name.as_ptr()).to_string_lossy().into_owned(), prefix: CStr::from_ptr(prefix.as_ptr()).to_string_lossy().into_owned(), suffix: CStr::from_ptr(suffix.as_ptr()).to_string_lossy().into_owned(), pcs, colorant, }}) } else { None } } #[must_use] pub fn colors(&self) -> Vec { (0..self.len()).filter_map(|i| self.get(i)).collect() } /// Push a color at the end of the palette pub fn append(&mut self, color_name: &str, mut pcs: [u16; 3], mut colorant: [u16; ffi::MAXCHANNELS]) -> bool { let Ok(s) = CString::new(color_name) else { return false }; unsafe { 0 != ffi::cmsAppendNamedColor(self.as_ptr(), s.as_ptr(), pcs.as_mut_ptr(), colorant.as_mut_ptr()) } } } impl<'a> fmt::Debug for NamedColorListRef { #[cold] fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { if let Some(c) = self.get(0) { write!(f, "NamedColorList({} colors: {}{}{}, etc.)", self.len(), c.prefix, c.name, c.suffix) } else { f.write_str("NamedColorList(0)") } } } #[test] fn named() { let mut n = NamedColorList::new(10, 3, "hello", "world").unwrap(); assert!(n.append("yellow", [1,2,3], [1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16])); assert_eq!(None, n.get(10000)); let c = n.get(0).unwrap(); assert_eq!("yellow", c.name); assert_eq!("hello", c.prefix); assert_eq!([1,2,3], c.pcs); } lcms2-6.0.4/src/pipeline.rs000064400000000000000000000075211046102023000136220ustar 00000000000000use crate::eval::FloatOrU16; use crate::stage::{StageRef, StagesIter}; use crate::{ffi, Error, LCMSResult}; use foreign_types::{foreign_type, ForeignTypeRef}; use std::fmt; use std::ptr; foreign_type! { /// Pipelines are a convenient way to model complex operations on image data. /// /// Each pipeline may contain an arbitrary number of stages. Each stage performs a single operation. /// Pipelines may be optimized to be executed on a certain format (8 bits, for example) and can be saved as LUTs in ICC profiles. /// /// This is an owned version of `PipelineRef`. #[doc(hidden)] pub unsafe type Pipeline { type CType = ffi::Pipeline; fn drop = ffi::cmsPipelineFree; fn clone = ffi::cmsPipelineDup; } } impl Pipeline { /// Allocates an empty pipeline. Final Input and output channels must be specified at creation time. pub fn new(input_channels: usize, output_channels: usize) -> LCMSResult { unsafe { Error::if_null(ffi::cmsPipelineAlloc(ptr::null_mut(), input_channels as u32, output_channels as u32)) } } } impl PipelineRef { /// Appends pipeline given as argument at the end of this pipeline. Channel count must match. pub fn cat(&mut self, append: &PipelineRef) -> bool { if append.input_channels() != self.output_channels() { return false; } unsafe { ffi::cmsPipelineCat((self as *mut Self).cast(), append.as_ptr()) != 0 } } #[must_use] pub fn stage_count(&self) -> usize { unsafe { ffi::cmsPipelineStageCount(self.as_ptr()) as usize } } #[must_use] pub fn first_stage(&self) -> Option<&StageRef> { unsafe { let f = ffi::cmsPipelineGetPtrToFirstStage(self.as_ptr()); if !f.is_null() { Some(ForeignTypeRef::from_ptr(f)) } else { None } } } #[must_use] pub fn last_stage(&self) -> Option<&StageRef> { unsafe { let f = ffi::cmsPipelineGetPtrToLastStage(self.as_ptr()); if !f.is_null() { Some(ForeignTypeRef::from_ptr(f)) } else { None } } } #[must_use] pub fn stages(&self) -> StagesIter<'_> { StagesIter(self.first_stage()) } pub fn set_8bit(&mut self, on: bool) -> bool { unsafe { ffi::cmsPipelineSetSaveAs8bitsFlag((self as *mut Self).cast(), i32::from(on)) != 0 } } #[must_use] pub fn input_channels(&self) -> usize { unsafe { ffi::cmsPipelineInputChannels(self.as_ptr()) as usize } } #[must_use] pub fn output_channels(&self) -> usize { unsafe { ffi::cmsPipelineOutputChannels(self.as_ptr()) as usize } } // Evaluates a pipeline usin u16 of f32 numbers. With u16 it's optionally using the optimized path. pub fn eval(&self, input: &[Value], output: &mut [Value]) { assert_eq!(self.input_channels(), input.len()); assert_eq!(self.output_channels(), output.len()); unsafe { self.eval_unchecked(input, output); } } // You must ensure that input and output have length sufficient for channels #[inline] pub unsafe fn eval_unchecked(&self, input: &[Value], output: &mut [Value]) { Value::eval_pipeline(self.as_ptr(), input, output); } } impl fmt::Debug for PipelineRef { #[cold] fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "Pipeline({}->{}ch, {} stages)", self.input_channels(), self.output_channels(), self.stage_count()) } } #[test] fn pipeline() { let p = Pipeline::new(123, 12); assert!(p.is_err()); let p = Pipeline::new(4, 3).unwrap(); assert_eq!(0, p.stage_count()); assert_eq!(4, p.input_channels()); assert_eq!(3, p.output_channels()); } lcms2-6.0.4/src/profile.rs000064400000000000000000000574331046102023000134640ustar 00000000000000use crate::context::Context; use crate::*; use foreign_types::ForeignTypeRef; use std::default::Default; use std::fmt; use std::fs::File; use std::io; use std::io::Read; use std::mem::MaybeUninit; use std::os::raw::c_void; use std::path::Path; use std::ptr; /// An ICC color profile #[repr(transparent)] pub struct Profile { pub(crate) handle: ffi::HPROFILE, _context_ref: PhantomData, } unsafe impl<'a, C: Send> Send for Profile {} /// These are the basic functions on opening profiles. /// For simpler operation, you must open two profiles using `new_file`, and then create a transform with these open profiles with `Transform`. /// Using this transform you can color correct your bitmaps. impl Profile { /// Parse ICC profile from the in-memory array #[inline] pub fn new_icc(data: &[u8]) -> LCMSResult { Self::new_icc_context(GlobalContext::new(), data) } /// Load ICC profile file from disk #[inline] pub fn new_file>(path: P) -> io::Result { Self::new_file_context(GlobalContext::new(), path) } /// Create an ICC virtual profile for sRGB space. sRGB is a standard RGB color space created cooperatively by HP and Microsoft in 1996 for use on monitors, printers, and the Internet. #[inline] #[must_use] pub fn new_srgb() -> Self { Self::new_srgb_context(GlobalContext::new()) } /// This function creates a display RGB profile based on White point, primaries and transfer functions. It populates following tags; this conform a standard RGB Display Profile, and then I add (As per addendum II) chromaticity tag. /// /// 1. `ProfileDescriptionTag` /// 2. `MediaWhitePointTag` /// 3. `RedColorantTag` /// 4. `GreenColorantTag` /// 5. `BlueColorantTag` /// 6. `RedTRCTag` /// 7. `GreenTRCTag` /// 8. `BlueTRCTag` /// 9. Chromatic adaptation Tag /// 10. `ChromaticityTag` #[inline] pub fn new_rgb( white_point: &CIExyY, primaries: &CIExyYTRIPLE, transfer_function: &[&ToneCurve], ) -> LCMSResult { Self::new_rgb_context( GlobalContext::new(), white_point, primaries, transfer_function, ) } /// This function creates a gray profile based on White point and transfer function. It populates following tags; this conform a standard gray display profile. /// /// 1. `ProfileDescriptionTag` /// 2. `MediaWhitePointTag` /// 3. `GrayTRCTag` #[inline] pub fn new_gray(white_point: &CIExyY, curve: &ToneCurve) -> LCMSResult { Self::new_gray_context(GlobalContext::new(), white_point, curve) } /// Creates a XYZ XYZ identity, marking it as v4 ICC profile. `WhitePoint` used in Absolute colorimetric intent is D50. #[inline] #[must_use] pub fn new_xyz() -> Self { Self::new_handle(unsafe { ffi::cmsCreateXYZProfile() }).unwrap() } /// Creates a fake NULL profile. This profile return 1 channel as always 0. Is useful only for gamut checking tricks. #[inline] #[must_use] pub fn new_null() -> Self { Self::new_handle(unsafe { ffi::cmsCreateNULLProfile() }).unwrap() } /// Creates an empty profile object, ready to be populated by the programmer. /// /// WARNING: The obtained profile without adding any information is not directly useable. #[inline] #[must_use] pub fn new_placeholder() -> Self { Self::new_handle(unsafe { ffi::cmsCreateProfilePlaceholder(ptr::null_mut()) }).unwrap() } /// This is a devicelink operating in CMYK for ink-limiting. Currently only `cmsSigCmykData` is supported. /// Limit: Amount of ink limiting in % (0..400%) pub fn ink_limiting(color_space: ColorSpaceSignature, limit: f64) -> LCMSResult { Self::new_handle(unsafe { ffi::cmsCreateInkLimitingDeviceLink(color_space, limit) }) } /// Generates a device-link profile from a given color transform. This profile can then be used by any other function accepting profile handle. /// Depending on the specified version number, the implementation of the devicelink may vary. Accepted versions are in range 1.0…4.3 #[inline] pub fn new_device_link(transform: &Transform, version: f64, flags: Flags) -> LCMSResult { Self::new_handle(unsafe { ffi::cmsTransform2DeviceLink(transform.handle, version, flags.bits()) }) } } impl Profile { /// Create ICC file in memory buffer pub fn icc(&self) -> LCMSResult> { unsafe { let mut len = 0; if ffi::cmsSaveProfileToMem(self.handle, std::ptr::null_mut(), &mut len) == 0 { return Err(Error::ObjectCreationError); } let mut data = vec![0u8; len as usize]; if len == 0 || ffi::cmsSaveProfileToMem(self.handle, data.as_mut_ptr().cast::(), &mut len) == 0 { return Err(Error::ObjectCreationError); } Ok(data) } } /// Gets the device class signature from profile header. #[inline] #[must_use] pub fn device_class(&self) -> ProfileClassSignature { unsafe { ffi::cmsGetDeviceClass(self.handle) } } /// Sets the device class signature in profile header. #[inline] pub fn set_device_class(&mut self, cls: ProfileClassSignature) { unsafe { ffi::cmsSetDeviceClass(self.handle, cls) } } /// Returns the profile ICC version in the same format as it is stored in the header. #[inline] #[must_use] pub fn encoded_icc_version(&self) -> u32 { unsafe { ffi::cmsGetEncodedICCversion(self.handle) } } #[inline] pub fn set_encoded_icc_version(&self, v: u32) { unsafe { ffi::cmsSetEncodedICCversion(self.handle, v) } } /// Gets the attribute flags. Currently defined values correspond to the low 4 bytes of the 8 byte attribute quantity. /// /// * `Reflective` /// * `Transparency` /// * `Glossy` /// * `Matte` #[inline] #[must_use] pub fn header_attributes(&self) -> u64 { let mut flags = 0; unsafe { ffi::cmsGetHeaderAttributes(self.handle, &mut flags); } flags } /// Sets the attribute flags in the profile header. #[inline] pub fn set_header_attributes(&mut self, flags: u64) { unsafe { ffi::cmsSetHeaderAttributes(self.handle, flags); } } #[inline] #[must_use] pub fn header_creator(&self) -> u32 { unsafe { ffi::cmsGetHeaderCreator(self.handle) } } /// Get header flags of given ICC profile object. /// /// The profile flags field does contain flags to indicate various hints for the CMM such as distributed processing and caching options. /// The least-significant 16 bits are reserved for the ICC. Flags in bit positions 0 and 1 shall be used as indicated in Table 7 of LCMS PDF. #[inline] #[must_use] pub fn header_flags(&self) -> u32 { unsafe { ffi::cmsGetHeaderFlags(self.handle) } } /// Sets header flags of given ICC profile object. Valid flags are defined in Table 7 of LCMS PDF. #[inline] pub fn set_header_flags(&mut self, flags: u32) { unsafe { ffi::cmsSetHeaderFlags(self.handle, flags); } } /// Returns the manufacturer signature as described in the header. /// /// This funcionality is widely superseded by the manufaturer tag. Of use only in elder profiles. #[inline] #[must_use] pub fn header_manufacturer(&self) -> u32 { unsafe { ffi::cmsGetHeaderManufacturer(self.handle) } } /// Sets the manufacturer signature in the header. /// /// This funcionality is widely superseded by the manufaturer tag. Of use only in elder profiles. #[deprecated(note = "This funcionality is widely superseded by the manufaturer tag")] #[inline] pub fn set_header_manufacturer(&mut self, m: u32) { unsafe { ffi::cmsSetHeaderManufacturer(self.handle, m) } } /// Returns the model signature as described in the header. /// /// This funcionality is widely superseded by the model tag. Of use only in elder profiles. #[inline] #[must_use] pub fn header_model(&self) -> u32 { unsafe { ffi::cmsGetHeaderModel(self.handle) } } /// Sets the model signature in the profile header. /// /// This funcionality is widely superseded by the model tag. Of use only in elder profiles. #[deprecated(note = "This funcionality is widely superseded by the model tag")] #[inline] pub fn set_header_model(&mut self, model: u32) { unsafe { ffi::cmsSetHeaderModel(self.handle, model); } } /// Gets the profile header rendering intent. /// /// From the ICC spec: “The rendering intent field shall specify the rendering intent which should be used /// (or, in the case of a Devicelink profile, was used) when this profile is (was) combined with another profile. /// In a sequence of more than two profiles, it applies to the combination of this profile and the next profile in the sequence and not to the entire sequence. /// Typically, the user or application will set the rendering intent dynamically at runtime or embedding time. /// Therefore, this flag may not have any meaning until the profile is used in some context, e.g. in a Devicelink or an embedded source profile.” #[inline] #[must_use] pub fn header_rendering_intent(&self) -> Intent { unsafe { ffi::cmsGetHeaderRenderingIntent(self.handle) } } #[inline] pub fn set_header_rendering_intent(&mut self, intent: Intent) { unsafe { ffi::cmsSetHeaderRenderingIntent(self.handle, intent) } } /// Gets the profile connection space used by the given profile, using the ICC convention. #[inline] #[must_use] pub fn pcs(&self) -> ColorSpaceSignature { unsafe { ffi::cmsGetPCS(self.handle) } } /// Sets the profile connection space signature in profile header, using ICC convention. #[inline] pub fn set_pcs(&mut self, pcs: ColorSpaceSignature) { unsafe { ffi::cmsSetPCS(self.handle, pcs) } } #[must_use] pub fn info(&self, info: InfoType, locale: Locale) -> Option { let size = unsafe { ffi::cmsGetProfileInfo(self.handle, info, locale.language_ptr(), locale.country_ptr(), std::ptr::null_mut(), 0) }; if 0 == size { return None; } let wchar_bytes = std::mem::size_of::(); let mut data = vec![0; size as usize / wchar_bytes]; unsafe { let len = data.len() * wchar_bytes; let res = ffi::cmsGetProfileInfo(self.handle, info, locale.language_ptr(), locale.country_ptr(), data.as_mut_ptr(), len as u32); if 0 == res { return None; } } Some(data.into_iter() .take_while(|&c| c > 0) .map(|c| std::char::from_u32(c as u32).unwrap()) .collect()) } /// Returns the profile ICC version. The version is decoded to readable floating point format. #[inline] #[must_use] pub fn version(&self) -> f64 { unsafe { ffi::cmsGetProfileVersion(self.handle) } } /// Sets the ICC version in profile header. The version is given to this function as a float n.m #[inline] pub fn set_version(&mut self, ver: f64) { unsafe { ffi::cmsSetProfileVersion(self.handle, ver); } } #[inline] #[must_use] pub fn tag_signatures(&self) -> Vec { unsafe { (0..ffi::cmsGetTagCount(self.handle)) .map(|n| ffi::cmsGetTagSignature(self.handle, n as u32)) .collect() } } #[inline] #[must_use] pub fn detect_black_point(&self, intent: Intent) -> Option { unsafe { let mut b = CIEXYZ::default(); if ffi::cmsDetectBlackPoint(&mut b, self.handle, intent, 0) != 0 { Some(b) } else { None } } } #[inline] #[must_use] pub fn detect_destination_black_point(&self, intent: Intent) -> Option { unsafe { let mut b = CIEXYZ::default(); if ffi::cmsDetectDestinationBlackPoint(&mut b, self.handle, intent, 0) != 0 { Some(b) } else { None } } } #[inline] #[must_use] pub fn detect_tac(&self) -> f64 { unsafe { ffi::cmsDetectTAC(self.handle) } } /// Gets the color space used by the given profile, using the ICC convention. #[inline] #[must_use] pub fn color_space(&self) -> ColorSpaceSignature { unsafe { let v = ffi::cmsGetColorSpace(self.handle); if 0 != v as u32 {v} else {ColorSpaceSignature::Sig1colorData} } } /// Sets the profile connection space signature in profile header, using ICC convention. #[inline] pub fn set_color_space(&mut self, sig: ColorSpaceSignature) { unsafe { ffi::cmsSetColorSpace(self.handle, sig) } } #[inline] #[must_use] pub fn is_clut(&self, intent: Intent, used_direction: u32) -> bool { unsafe { ffi::cmsIsCLUT(self.handle, intent, used_direction) != 0 } } #[inline] #[must_use] pub fn is_intent_supported(&self, intent: Intent, used_direction: u32) -> bool { unsafe { ffi::cmsIsIntentSupported(self.handle, intent, used_direction) != 0 } } #[inline] #[must_use] pub fn is_matrix_shaper(&self) -> bool { unsafe { ffi::cmsIsMatrixShaper(self.handle) != 0 } } #[inline] #[must_use] pub fn has_tag(&self, sig: TagSignature) -> bool { unsafe { ffi::cmsIsTag(self.handle, sig) != 0 } } #[inline] #[must_use] pub fn read_tag(&self, sig: TagSignature) -> Tag<'_> { unsafe { Tag::new(sig, ffi::cmsReadTag(self.handle, sig) as *const u8) } } #[inline] pub fn write_tag(&mut self, sig: TagSignature, tag: Tag<'_>) -> bool { unsafe { ffi::cmsWriteTag(self.handle, sig, tag.data_for_signature(sig).cast()) != 0 } } #[inline] pub fn remove_tag(&mut self, sig: TagSignature) -> bool { unsafe { ffi::cmsWriteTag(self.handle, sig, std::ptr::null()) != 0 } } #[inline] pub fn link_tag(&mut self, sig: TagSignature, dst: TagSignature) -> bool { unsafe { ffi::cmsLinkTag(self.handle, sig, dst) != 0 } } /// Retrieves the Profile ID stored in the profile header. #[inline] #[must_use] pub fn profile_id(&self) -> ffi::ProfileID { unsafe { debug_assert_eq!(16, std::mem::size_of::()); let mut id = MaybeUninit::::uninit(); ffi::cmsGetHeaderProfileID(self.handle, id.as_mut_ptr().cast()); id.assume_init() } } /// Computes a MD5 checksum and stores it as Profile ID in the profile header. #[inline] pub fn set_default_profile_id(&mut self) { unsafe { ffi::cmsMD5computeID(self.handle); } } #[inline] pub fn set_profile_id(&mut self, id: ffi::ProfileID) { unsafe { ffi::cmsSetHeaderProfileID(self.handle, std::ptr::addr_of!(id) as *mut _); } } pub fn save_profile_to_file(&mut self, path: &Path) -> io::Result<()> { let profile = self.icc().map_err(|e| io::Error::new(io::ErrorKind::Other, e))?; std::fs::write(path, profile) } } /// Per-context functions that can be used with a `ThreadContext` impl Profile { #[inline] pub fn new_icc_context(context: impl AsRef, data: &[u8]) -> LCMSResult { if data.is_empty() { return Err(Error::MissingData); } Self::new_handle(unsafe { ffi::cmsOpenProfileFromMemTHR(context.as_ref().as_ptr(), data.as_ptr().cast::(), data.len() as u32) }) } #[inline] pub fn new_file_context>(context: impl AsRef, path: P) -> io::Result { let mut buf = Vec::new(); File::open(path)?.read_to_end(&mut buf)?; Self::new_icc_context(context, &buf).map_err(|_| io::ErrorKind::Other.into()) } #[inline] pub fn new_srgb_context(context: impl AsRef) -> Self { Self::new_handle(unsafe { ffi::cmsCreate_sRGBProfileTHR(context.as_ref().as_ptr()) }).unwrap() } #[inline] #[track_caller] pub fn new_rgb_context(context: impl AsRef, white_point: &CIExyY, primaries: &CIExyYTRIPLE, transfer_function: &[&ToneCurve]) -> LCMSResult { assert_eq!(3, transfer_function.len()); Self::new_handle(unsafe { ffi::cmsCreateRGBProfileTHR(context.as_ref().as_ptr(), white_point, primaries, [transfer_function[0].as_ptr().cast_const(), transfer_function[1].as_ptr().cast_const(), transfer_function[2].as_ptr().cast_const()] .as_ptr()) }) } #[inline] pub fn new_gray_context(context: impl AsRef, white_point: &CIExyY, curve: &ToneCurve) -> LCMSResult { Self::new_handle(unsafe { ffi::cmsCreateGrayProfileTHR(context.as_ref().as_ptr(), white_point, curve.as_ptr()) }) } /// This is a devicelink operating in the target colorspace with as many transfer functions as components. /// Number of tone curves must be sufficient for the color space. #[inline] pub unsafe fn new_linearization_device_link_context(context: impl AsRef, color_space: ColorSpaceSignature, curves: &[ToneCurveRef]) -> LCMSResult { let v: Vec<_> = curves.iter().map(|c| c.as_ptr().cast_const()).collect(); Self::new_handle(ffi::cmsCreateLinearizationDeviceLinkTHR(context.as_ref().as_ptr(), color_space, v.as_ptr())) } /// Creates an abstract devicelink operating in Lab for Bright/Contrast/Hue/Saturation and white point translation. /// White points are specified as temperatures ºK /// /// `nLUTPoints` : Resulting color map resolution /// Bright: Bright increment. May be negative /// Contrast : Contrast increment. May be negative. /// Hue : Hue displacement in degree. /// Saturation: Saturation increment. May be negative /// `TempSrc`: Source white point temperature /// `TempDest`: Destination white point temperature. /// To prevent white point adjustment, set Temp to None #[inline] pub fn new_bchsw_abstract_context(context: impl AsRef, lut_points: usize, bright: f64, contrast: f64, hue: f64, saturation: f64, temp_src_dst: Option<(u32, u32)>) -> LCMSResult { let (temp_src, temp_dest) = temp_src_dst.unwrap_or((0,0)); Self::new_handle(unsafe { ffi::cmsCreateBCHSWabstractProfileTHR(context.as_ref().as_ptr(), lut_points as _, bright, contrast, hue, saturation, temp_src as _, temp_dest as _) }) } #[inline] fn new_handle(handle: ffi::HPROFILE) -> LCMSResult { if handle.is_null() { return Err(Error::ObjectCreationError); } Ok(Profile { handle, _context_ref: PhantomData, }) } /// This is a devicelink operating in CMYK for ink-limiting. Currently only `cmsSigCmykData` is supported. /// Limit: Amount of ink limiting in % (0..400%) #[inline] pub fn ink_limiting_context(context: impl AsRef, color_space: ColorSpaceSignature, limit: f64) -> LCMSResult { Self::new_handle(unsafe { ffi::cmsCreateInkLimitingDeviceLinkTHR(context.as_ref().as_ptr(), color_space, limit) }) } /// Creates a XYZ XYZ identity, marking it as v4 ICC profile. `WhitePoint` used in Absolute colorimetric intent is D50. #[inline] pub fn new_xyz_context(context: impl AsRef) -> Self { Self::new_handle(unsafe { ffi::cmsCreateXYZProfileTHR(context.as_ref().as_ptr()) }).unwrap() } /// Creates a fake NULL profile. This profile return 1 channel as always 0. Is useful only for gamut checking tricks. #[inline] pub fn new_null_context(context: impl AsRef) -> Self { Self::new_handle(unsafe { ffi::cmsCreateNULLProfileTHR(context.as_ref().as_ptr()) }).unwrap() } /// Creates a Lab Lab identity, marking it as v2 ICC profile. /// /// Adjustments for accomodating PCS endoing shall be done by Little CMS when using this profile. pub fn new_lab2_context(context: impl AsRef, white_point: &CIExyY) -> LCMSResult { Self::new_handle(unsafe { ffi::cmsCreateLab2ProfileTHR(context.as_ref().as_ptr(), white_point) }) } /// Creates a Lab Lab identity, marking it as v4 ICC profile. #[inline] pub fn new_lab4_context(context: impl AsRef, white_point: &CIExyY) -> LCMSResult { Self::new_handle(unsafe { ffi::cmsCreateLab4ProfileTHR(context.as_ref().as_ptr(), white_point) }) } } impl Drop for Profile { fn drop(&mut self) { unsafe { ffi::cmsCloseProfile(self.handle); } } } #[test] fn tags_read() { let prof = Profile::new_srgb(); assert!(prof.read_tag(TagSignature::BToD0Tag).is_none()); assert_eq!(CIEXYZ::d50().X, match prof.read_tag(TagSignature::MediaWhitePointTag) { Tag::CIEXYZ(xyz) => xyz.X, _ => panic!(), }); } #[test] fn tags_write() { let mut p = Profile::new_placeholder(); let mut mlu = MLU::new(1); mlu.set_text_ascii("Testing", Locale::new("en_GB")); assert!(p.write_tag(TagSignature::CopyrightTag, Tag::MLU(&mlu))); let xyz = CIEXYZ{X:1., Y:2., Z:3.}; assert!(p.write_tag(TagSignature::RedColorantTag, Tag::CIEXYZ(&xyz))); assert!(p.has_tag(TagSignature::CopyrightTag)); assert!(p.has_tag(TagSignature::RedColorantTag)); assert!(!p.has_tag(TagSignature::BlueColorantTag)); assert_eq!(&xyz, match p.read_tag(TagSignature::RedColorantTag) { Tag::CIEXYZ(d) => d, _ => panic!(), }); assert_eq!(Ok("Testing".to_owned()), match p.read_tag(TagSignature::CopyrightTag) { Tag::MLU(mlu) => mlu.text(Locale::none()), _ => panic!(), }); } impl fmt::Debug for Profile { #[cold] fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { let mut s = f.debug_struct("Profile"); let l = Locale::none(); s.field("Description", &self.info(InfoType::Description, l)); s.field("Manufacturer", &self.info(InfoType::Manufacturer, l)); s.field("Model", &self.info(InfoType::Model, l)); s.field("Copyright", &self.info(InfoType::Copyright, l)); s.finish() } } #[test] fn setters() { let mut p = Profile::new_placeholder(); assert_eq!(ColorSpaceSignature::Sig1colorData, p.color_space()); p.set_color_space(ColorSpaceSignature::RgbData); assert_eq!(ColorSpaceSignature::RgbData, p.color_space()); } #[test] fn icc() { let prof = Profile::new_xyz(); assert!(prof.icc().unwrap().len() > 300); assert!(format!("{prof:?}").contains("XYZ identity")); } #[test] fn bad_icc() { let err = Profile::new_icc(&[1, 2, 3]); assert!(err.is_err()); } #[test] fn unwind_safety() { let profile = &Profile::new_xyz(); std::panic::catch_unwind(|| { let _p = profile; }).unwrap(); } lcms2-6.0.4/src/stage.rs000064400000000000000000000110041046102023000131070ustar 00000000000000use crate::context::Context; use crate::eval::FloatOrU16; use crate::{ffi, Error, GlobalContext, LCMSResult, ToneCurveRef}; use foreign_types::{foreign_type, ForeignTypeRef}; use std::fmt; use std::ptr; foreign_type! { /// Stage functions /// /// Stages are single-step operations that can be chained to create pipelines. /// Actual stage types does include matrices, tone curves, Look-up interpolation and user-defined. /// There are functions to create new stage types and a plug-in type to allow stages to be saved in multi profile elements tag types. /// See the plug-in API for further details. /// /// This is an owned version of `Stage`. pub unsafe type Stage { type CType = ffi::Stage; fn drop = ffi::cmsStageFree; } } impl Stage { /// Creates an empty (identity) stage that does no operation. /// /// May be needed in order to save the pipeline as AToB/BToA tags in ICC profiles. #[must_use] pub fn new_identity(channels: u32) -> Stage { unsafe {Error::if_null( ffi::cmsStageAllocIdentity(GlobalContext::new().as_ptr(), channels) )}.unwrap() } /// Creates a stage that contains nChannels tone curves, one per channel. pub fn new_tone_curves(curves: &[&ToneCurveRef]) -> LCMSResult { let ptrs: Vec<_> = curves.iter().map(|c| c.as_ptr().cast_const()).collect(); unsafe { Error::if_null(ffi::cmsStageAllocToneCurves( GlobalContext::new().as_ptr(), ptrs.len() as u32, ptrs.as_ptr(), )) } } /// Creates a stage that contains a matrix plus an optional offset. /// /// Note that Matrix is specified in double precision, whilst CLUT has only float precision. /// That is because an ICC profile can encode matrices with far more precision that CLUTS. pub fn new_matrix(matrix2d: &[f64], rows: usize, cols: usize, offsets: Option<&[f64]>) -> LCMSResult { if matrix2d.len() < rows * cols { return Err(Error::MissingData); } if let Some(offsets) = offsets { if offsets.len() < cols { return Err(Error::MissingData); } } unsafe { Error::if_null(ffi::cmsStageAllocMatrix( GlobalContext::new().as_ptr(), rows as u32, cols as u32, matrix2d.as_ptr(), offsets.map(|p| p.as_ptr()).unwrap_or(ptr::null()), )) } } /// Creates a stage that contains a float or 16 bits multidimensional lookup table (CLUT). /// /// Each dimension has same resolution. The CLUT can be initialized by specifying values in Table parameter. /// The recommended way is to set Table to None and use `sample_clut` with a callback, because this way the implementation is independent of the selected number of grid points. pub fn new_clut(grid_point_nodes: usize, input_channels: u32, output_channels: u32, table: Option<&[Value]>) -> LCMSResult { if let Some(table) = table { if table.len() < grid_point_nodes { return Err(Error::MissingData) } } unsafe {Error::if_null( Value::stage_alloc_clut(GlobalContext::new().as_ptr(), grid_point_nodes as u32, input_channels, output_channels, table.map(|p|p.as_ptr()).unwrap_or(ptr::null())) )} } } impl StageRef { #[must_use] pub fn input_channels(&self) -> usize { unsafe { ffi::cmsStageInputChannels(self.as_ptr()) as usize } } #[must_use] pub fn output_channels(&self) -> usize { unsafe { ffi::cmsStageOutputChannels(self.as_ptr()) as usize } } #[must_use] pub fn stage_type(&self) -> ffi::StageSignature { unsafe { ffi::cmsStageType(self.as_ptr()) } } } pub struct StagesIter<'a>(pub Option<&'a StageRef>); impl<'a> Iterator for StagesIter<'a> { type Item = &'a StageRef; fn next(&mut self) -> Option { let it = self.0; if let Some(mpe) = it { self.0 = unsafe { let s = ffi::cmsStageNext(mpe.as_ptr()); if s.is_null() { None } else { Some(ForeignTypeRef::from_ptr(s)) } }; } it } } impl fmt::Debug for StageRef { #[cold] fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "Stage({:?})", self.stage_type()) } } lcms2-6.0.4/src/tag.rs000064400000000000000000000221521046102023000125650ustar 00000000000000use crate::*; use foreign_types::ForeignTypeRef; unsafe fn cast(ptr: *const u8) -> &'static T { assert!(0 == ptr.align_offset(std::mem::align_of::()), "Tag data pointer must be aligned"); &*ptr.cast::() } /// `from_ptr()` methods require mut for no good reason unsafe fn aligned_mut(ptr: *const u8) -> *mut T { assert!(0 == ptr.align_offset(std::mem::align_of::()), "Tag data pointer must be aligned"); ptr as *mut T } impl<'a> Tag<'a> { #[must_use] pub fn is_none(&self) -> bool { match *self { Tag::None => true, _ => false, } } #[must_use] pub unsafe fn data_for_signature(&self, sig: TagSignature) -> *const u8 { use crate::TagSignature::*; match (sig, self) { (RedColorantTag, &Tag::CIEXYZ(data)) | (BlueColorantTag, &Tag::CIEXYZ(data)) | (GreenColorantTag, &Tag::CIEXYZ(data)) | (LuminanceTag, &Tag::CIEXYZ(data)) | (MediaBlackPointTag, &Tag::CIEXYZ(data)) | (MediaWhitePointTag, &Tag::CIEXYZ(data)) => { data as *const ffi::CIEXYZ as *const u8 }, (ViewingCondDescTag, &Tag::MLU(data)) | (CharTargetTag, &Tag::MLU(data)) | (CopyrightTag, &Tag::MLU(data)) | (DeviceMfgDescTag, &Tag::MLU(data)) | (DeviceModelDescTag, &Tag::MLU(data)) | (ProfileDescriptionTag, &Tag::MLU(data)) | (ProfileDescriptionMLTag, &Tag::MLU(data)) | (ScreeningDescTag, &Tag::MLU(data)) => { data.as_ptr() as *const _ }, (ChromaticityTag, &Tag::CIExyYTRIPLE(data)) | (ChromaticAdaptationTag, &Tag::CIExyYTRIPLE(data)) => { data as *const ffi::CIExyYTRIPLE as *const u8 }, (ColorantTableTag, &Tag::NamedColorList(data)) | (ColorantTableOutTag, &Tag::NamedColorList(data)) | (CrdInfoTag, &Tag::NamedColorList(data)) | (NamedColor2Tag, &Tag::NamedColorList(data)) => data.as_ptr() as *const u8, (DataTag, &Tag::ICCData(data)) | (Ps2CRD0Tag, &Tag::ICCData(data)) | (Ps2CRD1Tag, &Tag::ICCData(data)) | (Ps2CRD2Tag, &Tag::ICCData(data)) | (Ps2CRD3Tag, &Tag::ICCData(data)) | (Ps2CSATag, &Tag::ICCData(data)) => data as *const _ as *const u8, (Ps2RenderingIntentTag, &Tag::ICCData(data)) => data as *const ffi::ICCData as *const u8, (AToB0Tag, &Tag::Pipeline(data)) | (AToB1Tag, &Tag::Pipeline(data)) | (AToB2Tag, &Tag::Pipeline(data)) | (BToA0Tag, &Tag::Pipeline(data)) | (BToA1Tag, &Tag::Pipeline(data)) | (BToA2Tag, &Tag::Pipeline(data)) | (DToB0Tag, &Tag::Pipeline(data)) | (DToB1Tag, &Tag::Pipeline(data)) | (DToB2Tag, &Tag::Pipeline(data)) | (DToB3Tag, &Tag::Pipeline(data)) | (BToD0Tag, &Tag::Pipeline(data)) | (BToD1Tag, &Tag::Pipeline(data)) | (BToD2Tag, &Tag::Pipeline(data)) | (BToD3Tag, &Tag::Pipeline(data)) | (GamutTag, &Tag::Pipeline(data)) | (Preview0Tag, &Tag::Pipeline(data)) | (Preview1Tag, &Tag::Pipeline(data)) | (Preview2Tag, &Tag::Pipeline(data)) => { data.as_ptr() as *const _ }, (BlueTRCTag, &Tag::ToneCurve(data)) | (GrayTRCTag, &Tag::ToneCurve(data)) | (GreenTRCTag, &Tag::ToneCurve(data)) | (RedTRCTag, &Tag::ToneCurve(data)) => { data.as_ptr() as *const _ }, (ColorimetricIntentImageStateTag, &Tag::ColorimetricIntentImageState(ref owned)) => { owned as *const ffi::ColorimetricIntentImageState as *const u8 }, (PerceptualRenderingIntentGamutTag, &Tag::Intent(ref owned)) | (SaturationRenderingIntentGamutTag, &Tag::Intent(ref owned)) => { owned as *const ffi::Intent as *const u8 }, (TechnologyTag, &Tag::Technology(ref data)) => data as *const _ as *const u8, (MeasurementTag, &Tag::ICCMeasurementConditions(data)) => { data as *const ffi::ICCMeasurementConditions as *const u8 }, (ProfileSequenceDescTag, &Tag::SEQ(data)) | (ProfileSequenceIdTag, &Tag::SEQ(data)) => { data as *const ffi::SEQ as *const u8 }, (ScreeningTag, &Tag::Screening(data)) => data as *const ffi::Screening as *const u8, (UcrBgTag, &Tag::UcrBg(data)) => data as *const ffi::UcrBg as *const u8, (VcgtTag, &Tag::VcgtCurves(ref arr)) => arr as *const [_; 3] as *const _, (ViewingConditionsTag, &Tag::ICCViewingConditions(data)) => { data as *const ffi::ICCViewingConditions as *const u8 }, (CicpTag, &Tag::VideoSignal(data)) => { data as *const ffi::VideoSignalType as *const u8 }, (sig, _) => panic!("Signature type {sig:?} does not support this tag data type"), } } #[must_use] pub unsafe fn new(sig: TagSignature, data: *const u8) -> Self { if data.is_null() { return Tag::None; } use crate::TagSignature::*; match sig { BlueColorantTag | GreenColorantTag | LuminanceTag | MediaBlackPointTag | MediaWhitePointTag | RedColorantTag => Tag::CIEXYZ(cast(data)), CharTargetTag | CopyrightTag | DeviceMfgDescTag | DeviceModelDescTag | ProfileDescriptionTag | ProfileDescriptionMLTag | ScreeningDescTag | ViewingCondDescTag => Tag::MLU(MLURef::from_ptr(aligned_mut(data))), ChromaticityTag | ChromaticAdaptationTag => Tag::CIExyYTRIPLE(cast(data)), ColorantTableTag | ColorantTableOutTag | CrdInfoTag | NamedColor2Tag => Tag::NamedColorList(NamedColorListRef::from_ptr(aligned_mut(data))), DataTag | Ps2CRD0Tag | Ps2CRD1Tag | Ps2CRD2Tag | Ps2CRD3Tag | Ps2CSATag | Ps2RenderingIntentTag => Tag::ICCData(cast(data)), AToB0Tag | AToB1Tag | AToB2Tag | BToA0Tag | BToA1Tag | BToA2Tag | DToB0Tag | DToB1Tag | DToB2Tag | DToB3Tag | BToD0Tag | BToD1Tag | BToD2Tag | BToD3Tag | GamutTag | Preview0Tag | Preview1Tag | Preview2Tag => Tag::Pipeline(PipelineRef::from_ptr(aligned_mut(data))), BlueTRCTag | GrayTRCTag | GreenTRCTag | RedTRCTag => Tag::ToneCurve(ToneCurveRef::from_ptr(aligned_mut(data))), ColorimetricIntentImageStateTag => { Tag::ColorimetricIntentImageState(data.cast::().read_unaligned()) }, PerceptualRenderingIntentGamutTag | SaturationRenderingIntentGamutTag => { Tag::Intent(data.cast::().read_unaligned()) }, TechnologyTag => Tag::Technology(data.cast::().read_unaligned()), MeasurementTag => Tag::ICCMeasurementConditions(cast(data)), ProfileSequenceDescTag | ProfileSequenceIdTag => Tag::SEQ(cast(data)), ScreeningTag => Tag::Screening(cast(data)), UcrBgTag => Tag::UcrBg(cast(data)), VcgtTag => { let ptrs = data as *const *mut u8; // array of pointers Tag::VcgtCurves([ ToneCurveRef::from_ptr(aligned_mut(*ptrs.offset(0))), ToneCurveRef::from_ptr(aligned_mut(*ptrs.offset(1))), ToneCurveRef::from_ptr(aligned_mut(*ptrs.offset(2))), ]) }, ViewingConditionsTag => Tag::ICCViewingConditions(cast(data)), _ => Tag::None, } } } #[test] fn tone_curves_tag() { let mut icc = Profile::new_srgb(); let _ = icc.read_tag(TagSignature::VcgtTag); icc.remove_tag(TagSignature::ProfileDescriptionTag); let mut desc = MLU::new(1); desc.set_text("Test ICC", Locale::none()); icc.write_tag(TagSignature::ProfileDescriptionTag, Tag::MLU(&desc)); let tc = ToneCurve::new(2.0); let tc_refs: [&ToneCurveRef; 3] = [&tc, &tc, &tc]; let before = Tag::VcgtCurves(tc_refs); icc.write_tag(TagSignature::VcgtTag, before); let before = Tag::VcgtCurves(tc_refs); let after = icc.read_tag(TagSignature::VcgtTag); match (before, after) { (Tag::VcgtCurves(a), Tag::VcgtCurves(b)) => { assert_eq!(a[0].estimated_entries(), b[0].estimated_entries()); assert_eq!(a[1].estimated_entries(), b[1].estimated_entries()); assert_eq!(a[2].estimated_entries(), b[2].estimated_entries()); } _ => panic!("bad tags"), } icc.icc().unwrap(); } lcms2-6.0.4/src/tonecurve.rs000064400000000000000000000200141046102023000140170ustar 00000000000000use crate::eval::FloatOrU16; use crate::{ffi, Error, LCMSResult}; use foreign_types::{foreign_type, ForeignType, ForeignTypeRef}; use std::fmt; use std::ptr; foreign_type! { /// Tone curves are powerful constructs that can contain curves specified in diverse ways. /// /// The curve is stored in segments, where each segment can be sampled or specified by parameters. A 16.bit simplification of the *whole* curve is kept for optimization purposes. For float operation, each segment is evaluated separately. Plug-ins may be used to define new parametric schemes. /// /// Owned version of `ToneCurveRef` pub unsafe type ToneCurve { type CType = ffi::ToneCurve; fn drop = ffi::cmsFreeToneCurve; fn clone = ffi::cmsDupToneCurve; } } impl ToneCurve { /// Simplified wrapper to `new_parametric`. Builds a parametric curve of type 1. #[must_use] pub fn new(gamma: f64) -> Self { unsafe { Self::from_ptr(ffi::cmsBuildGamma(ptr::null_mut(), gamma)) } } /// Builds a tone curve based on a table of 16-bit values. Tone curves built with this function are restricted to 0…1.0 domain. #[track_caller] #[inline] #[must_use] pub fn new_tabulated(values: &[u16]) -> Self { assert!(values.len() < std::i32::MAX as usize); unsafe { Self::new_handle(ffi::cmsBuildTabulatedToneCurve16( ptr::null_mut(), values.len() as _, values.as_ptr(), )) } } /// Builds a tone curve based on a table of floating point values. Tone curves built with this function are **not** restricted to 0…1.0 domain. #[track_caller] #[inline] #[must_use] pub fn new_tabulated_float(values: &[f32]) -> Self { assert!(values.len() < std::i32::MAX as usize); unsafe { Self::new_handle(ffi::cmsBuildTabulatedToneCurveFloat( ptr::null_mut(), values.len() as u32, values.as_ptr(), )) } } /// See Table 52 in LCMS documentation for descriptino of the types. /// /// 1. Exponential /// 2. CIE 122-1966 /// 3. IEC 61966-3 /// 4. IEC 61966-2.1 (sRGB) /// 5. See PDF /// 6. Identical to 5, unbounded. /// 7. See PDF /// 8. See PDF /// 108. (108) S-Shaped sigmoidal /// /// Always use 10-parameter slice for plug-in types. /// /// If `curve_type` is negative, then the curve is analytically inverted. pub fn new_parametric(curve_type: i16, params: &[f64]) -> LCMSResult { let params_min_len = match curve_type.abs() { 1 => 1, 2 => 3, 3 => 4, 4 => 5, 5 => 7, 6 => 4, 7 => 5, 8 => 6, 108 => 1, _ => 10, }; if params.len() < params_min_len { return Err(Error::MissingData); } unsafe { Error::if_null(ffi::cmsBuildParametricToneCurve(ptr::null_mut(), curve_type.into(), params.as_ptr())) } } #[track_caller] #[inline] unsafe fn new_handle(handle: *mut ffi::ToneCurve) -> Self { assert!(!handle.is_null()); Self::from_ptr(handle) } } impl ToneCurveRef { /// Creates a tone curve that is the inverse of given tone curve. #[inline] #[must_use] pub fn reversed(&self) -> ToneCurve { unsafe { ToneCurve::from_ptr(ffi::cmsReverseToneCurve(self.as_ptr())) } } /// Creates a tone curve that is the inverse of given tone curve. In the case it couldn’t be analytically reversed, a tablulated curve of `nResultSamples` is created. #[inline] #[must_use] pub fn reversed_samples(&self, samples: usize) -> ToneCurve { unsafe { ToneCurve::from_ptr(ffi::cmsReverseToneCurveEx(samples as _, self.as_ptr())) } } /// Composites two tone curves in the form Y^-1(X(t)) /// (self is X, the argument is Y) #[must_use] pub fn join(&self, y: &ToneCurveRef, points: usize) -> ToneCurve { unsafe { ToneCurve::from_ptr(ffi::cmsJoinToneCurve( ptr::null_mut(), self.as_ptr(), y.as_ptr(), points as u32, )) } } /// Returns TRUE if the tone curve contains more than one segment, FALSE if it has only one segment. #[inline] #[must_use] pub fn is_multisegment(&self) -> bool { unsafe { ffi::cmsIsToneCurveMultisegment(self.as_ptr()) != 0 } } /// Returns an estimation of cube being an identity (1:1) in the [0..1] domain. Does not take unbounded parts into account. This is just a coarse approximation, with no mathematical validity. #[inline] #[must_use] pub fn is_linear(&self) -> bool { unsafe { ffi::cmsIsToneCurveLinear(self.as_ptr()) != 0 } } /// Returns an estimation of monotonicity of curve in the [0..1] domain. Does not take unbounded parts into account. This is just a coarse approximation, with no mathematical validity. #[inline] #[must_use] pub fn is_monotonic(&self) -> bool { unsafe { ffi::cmsIsToneCurveMonotonic(self.as_ptr()) != 0 } } /// Does not take unbounded parts into account. #[inline] #[must_use] pub fn is_descending(&self) -> bool { unsafe { ffi::cmsIsToneCurveDescending(self.as_ptr()) != 0 } } #[inline] #[must_use] pub fn parametric_type(&self) -> i32 { unsafe { ffi::cmsGetToneCurveParametricType(self.as_ptr()) } } /// Estimates the apparent gamma of the tone curve by using least squares fitting. /// Precision: The maximum standard deviation allowed on the residuals, 0.01 is a fair value, set it to a big number to fit any curve, mo matter how good is the fit. #[inline] #[must_use] pub fn estimated_gamma(&self, precision: f64) -> Option { let g = unsafe { ffi::cmsEstimateGamma(self.as_ptr(), precision) }; if g <= -1.0 { None } else { Some(g) } } /// Smoothes tone curve according to the lambda parameter. From: Eilers, P.H.C. (1994) Smoothing and interpolation with finite differences. in: Graphic Gems IV, Heckbert, P.S. (ed.), Academic press. #[inline] pub fn smooth(&mut self, lambda: f64) -> bool { unsafe { ffi::cmsSmoothToneCurve((self as *mut Self).cast(), lambda) != 0 } } /// Tone curves do maintain a shadow low-resolution tabulated representation of the curve. This function returns a pointer to this table. #[must_use] pub fn estimated_entries(&self) -> &[u16] { unsafe { let len = ffi::cmsGetToneCurveEstimatedTableEntries(self.as_ptr()) as usize; let data = ffi::cmsGetToneCurveEstimatedTable(self.as_ptr()); std::slice::from_raw_parts(data, len) } } /// Evaluates the given number (u16 or f32) across the given tone curve. /// /// This function is significantly faster for u16, since it uses a pre-computed 16-bit lookup table. #[inline] pub fn eval(&self, v: ToneCurveValue) -> ToneCurveValue { unsafe { v.eval_tone_curve(self.as_ptr()) } } } impl fmt::Debug for ToneCurveRef { #[cold] fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "ToneCurve({} entries, gamma ~{:.1})", self.estimated_entries().len(), self.estimated_gamma(1.).unwrap_or(0.)) } } #[test] fn tones() { let _ = ToneCurve::new(0.); let _ = ToneCurve::new(1230.); let _ = ToneCurve::new(-10.); let g = ToneCurve::new(1. / 2.2); let r: &ToneCurveRef = &g; let mut z: ToneCurve = r.to_owned(); assert!(g.estimated_gamma(0.1).is_some()); assert_eq!(1., g.eval(1.)); assert_eq!(0, g.eval(0u16)); assert!(!z.is_linear()); assert!(z.is_monotonic()); assert!(!z.is_descending()); assert!(z.reversed().is_monotonic()); assert!(z.smooth(0.5)); assert_eq!(0, g.estimated_entries()[0]); assert_eq!(std::u16::MAX, *g.estimated_entries().last().unwrap()); assert!(ToneCurve::new_parametric(7, &[0.]).is_err()); } lcms2-6.0.4/src/transform.rs000064400000000000000000000434201046102023000140260ustar 00000000000000use crate::context::Context; use crate::*; use std::fmt; use std::marker::PhantomData; use std::mem::MaybeUninit; use std::os::raw::c_void; /// Conversion between two ICC profiles. /// /// The transform ensures type safety and thread safety at compile time. To do this, it has a few generic types associated with it. /// Usually, you don't need to specify any of the generic parameters (like `InputPixelFormat`/`OutputPixelFormat`) explicitly, /// because they are inferred from calls to constructors and `transform_pixels` or `transform_in_place`. /// /// If you get error such as: /// /// > cannot infer type for `InputPixelFormat` /// > type annotations required: cannot resolve `_: std::marker::Copy` /// /// then don't worry! Write some code that calls `transform_pixels()`, /// because this is the function that makes the type of the transform clear. /// /// In case you need to store the transform in a struct or return from a function, the full type of `Transform` is: /// /// ```rust,ignore /// Transform /// ``` /// /// * `InputPixelFormat` — e.g. `(u8,u8,u8)` or struct `RGB`, etc. /// The type must have appropriate number of bytes per pixel (i.e. you can't just use `[u8]` for everything). /// * `OutputPixelFormat` — similar to `InputPixelFormat`. If both are the same, then `transform_in_place()` function works. /// * `Context` — it's `GlobalContext` for the default non-thread-safe version, or `ThreadContext` for thread-safe version. /// * `Flags` — `AllowCache` or `DisallowCache`. If you disallow cache, then the transform will be accessible from multiple threads. /// /// Thread-safety: /// /// * Transform is `Send` if you create it with `ThreadContext` (use `new_*_context()` functions). /// * Transform is `Sync` if you create it without cache. Set flags to `Flags::NO_CACHE`. /// #[repr(transparent)] pub struct Transform { pub(crate) handle: ffi::HTRANSFORM, _from: PhantomData, _to: PhantomData, _context_ref: PhantomData, _flags_ref: PhantomData, } unsafe impl Send for Transform {} unsafe impl Sync for Transform {} impl Transform { /// Creates a color transform for translating bitmaps. /// /// Pixel types used in later `transform_pixels` calls must be either arrays or `Pod` `#[repr(C)]` structs that have appropriate number of bytes per pixel, /// or `u8` specifically. `[u8]` slices are treated as a special case that is allowed for any pixel type. /// /// Basic, non-tread-safe version. /// /// * Input: Handle to a profile object capable to work in input direction /// * `InputFormat`: A bit-field format specifier /// * Output: Handle to a profile object capable to work in output direction /// * `OutputFormat`: A bit-field format specifier /// * Intent: Rendering intent /// /// See documentation of these types for more detail. #[inline] pub fn new(input: &Profile, in_format: PixelFormat, output: &Profile, out_format: PixelFormat, intent: Intent) -> LCMSResult { Self::new_flags(input, in_format, output, out_format, intent, Flags::default()) } /// Non-thread-safe. See [`Transform::new`]. #[inline] pub fn new_flags(input: &Profile, in_format: PixelFormat, output: &Profile, out_format: PixelFormat, intent: Intent, flags: Flags) -> LCMSResult { Self::new_flags_context(GlobalContext::new(), input, in_format, output, out_format, intent, flags.allow_cache()) } /// A proofing transform does emulate the colors that would appear as the image were rendered on a specific device. /// The obtained transform emulates the device described by the "Proofing" profile. Useful to preview final result without rendering to the physical medium. /// /// That is, for example, with a proofing transform I can see how will look a photo of my little daughter if rendered on my HP printer. Since most printer profiles does include some sort of gamut-remapping, it is likely colors will not look as the original. Using a proofing transform, it can be done by using the appropriate function. Note that this is an important feature for final users, it is worth of all color-management stuff if the final media is not cheap. /// /// To enable proofing and gamut check you need to include following flags: /// /// * `Flags::GAMUTCHECK`: Color out of gamut are flagged to a fixed color defined by the function `cmsSetAlarmCodes` /// * `Flags::SOFTPROOFING`: does emulate the Proofing device. /// /// Pixel types used in later `transform_pixels` calls must be either arrays or `Pod` `#[repr(C)]` structs that have appropriate number of bytes per pixel, /// or `u8` specifically. `[u8]` slices are treated as a special case that is allowed for any pixel type. #[inline] pub fn new_proofing(input: &Profile, in_format: PixelFormat, output: &Profile, out_format: PixelFormat, proofing: &Profile, intent: Intent, proofng_intent: Intent, flags: Flags) -> LCMSResult { Self::new_proofing_context(GlobalContext::new(), input, in_format, output, out_format, proofing, intent, proofng_intent, flags) } /// Multiprofile transforms /// /// User passes in an array of handles to open profiles. The returned color transform do "smelt" all profiles in a single devicelink. /// Color spaces must be paired with the exception of Lab/XYZ, which can be interchanged. /// /// Pixel types used in later `transform_pixels` calls must be either arrays or `Pod` `#[repr(C)]` structs that have appropriate number of bytes per pixel, /// or `u8` specifically. `[u8]` slices are treated as a special case that is allowed for any pixel type. #[inline] pub fn new_multiprofile(profiles: &[&Profile], in_format: PixelFormat, out_format: PixelFormat, intent: Intent, flags: Flags) -> LCMSResult { Self::new_multiprofile_context(GlobalContext::new(), profiles, in_format, out_format, intent, flags) } } impl Transform { /// Read pixels and write them back to the same slice. Input and output pixel types must be identical. /// /// This processes up to `u32::MAX` pixels. /// /// Pixel types must be either arrays or `Pod` `#[repr(C)]` structs that have appropriate number of bytes per pixel, /// or `u8` specifically. `[u8]` slices are treated as a special case that is allowed for any pixel type. /// When `[u8]` is used, it will panic if the byte length of the slice is not a round number of pixels. #[inline] #[track_caller] pub fn transform_in_place(&self, srcdst: &mut [PixelFormat]) { let num_pixels = self.num_pixels(srcdst.len(), srcdst.len()); unsafe { ffi::cmsDoTransform(self.handle, srcdst.as_ptr().cast::(), srcdst.as_mut_ptr().cast::(), num_pixels); } } } impl Transform { // Same as `new()`, but allows specifying thread-safe context (enables `Send`) // // For `Sync`, see `new_flags_context` and `Flags::NO_CACHE` #[inline] pub fn new_context(context: impl AsRef, input: &Profile, in_format: PixelFormat, output: &Profile, out_format: PixelFormat, intent: Intent) -> LCMSResult { Self::new_flags_context(context, input, in_format, output, out_format, intent, Flags::default()) } } impl Transform { #[inline] unsafe fn new_handle(handle: ffi::HTRANSFORM) -> LCMSResult { if handle.is_null() { Err(Error::ObjectCreationError) } else { Ok(Transform { handle, _from: PhantomData, _to: PhantomData, _context_ref: PhantomData, _flags_ref: PhantomData, }) } } #[track_caller] fn check_formats(in_format: PixelFormat, out_format: PixelFormat) { Self::check_format::(in_format, true); Self::check_format::(out_format, false); } #[track_caller] fn check_format(format: PixelFormat, input: bool) { let io = if input {"input"} else {"output"}; assert!(!format.planar(), "Planar {format:?} {io} format not supported"); // Special-case u8 if is_u8::

() { return; } assert_eq!(format.bytes_per_pixel(), std::mem::size_of::

(), "{format:?} has {} bytes per pixel, but the {io} format has {}", format.bytes_per_pixel(), std::mem::size_of::

()); } /// Description of the input pixel format this transform has been created for #[inline] #[must_use] pub fn input_pixel_format(&self) -> PixelFormat { unsafe { ffi::cmsGetTransformInputFormat(self.handle) } } /// Description of the output pixel format this transform has been created for #[inline] #[must_use] pub fn output_pixel_format(&self) -> PixelFormat { unsafe { ffi::cmsGetTransformOutputFormat(self.handle) } } #[inline] #[track_caller] fn num_pixels(&self, mut src_len: usize, mut dst_len: usize) -> u32 { if is_u8::() { let bpp = self.input_pixel_format().bytes_per_pixel(); if bpp > 1 { assert_eq!(0, src_len % bpp, "Input [u8] slice's length {src_len} is not a multiple of {bpp}"); src_len /= bpp; } } if is_u8::() { let bpp = self.output_pixel_format().bytes_per_pixel(); if bpp > 1 { assert_eq!(0, dst_len % bpp, "Output [u8] slice's length {dst_len} is not a multiple of {bpp}"); dst_len /= bpp; } } src_len.min(dst_len).min(u32::MAX as usize) as u32 } /// This function translates bitmaps according of parameters setup when creating the color transform. /// /// If slices differ in length, the smaller amount of pixels is processed. /// This processes up to `u32::MAX` pixels. /// /// Pixel types must be either arrays or `Pod` `#[repr(C)]` structs that have appropriate number of bytes per pixel, /// or `u8` specifically. `[u8]` slices are treated as a special case that is allowed for any pixel type. /// When `[u8]` is used, it will panic if the byte length of the slice is not a round number of pixels. #[inline] #[track_caller] pub fn transform_pixels(&self, src: &[InputPixelFormat], dst: &mut [OutputPixelFormat]) { let num_pixels = self.num_pixels(src.len(), dst.len()); unsafe { ffi::cmsDoTransform(self.handle, src.as_ptr().cast::(), dst.as_mut_ptr().cast::(), num_pixels); } } /// This function translates bitmaps according of parameters setup when creating the color transform. /// /// It allows destination to be uninitailized, and returns the same slice, initialized. /// /// # Panics /// /// If the source slice is shorter than the destination, or larger than `u32::MAX`. /// When `[u8]` is used, it will panic if the byte length of the slice is not a round number of pixels. #[inline] #[track_caller] pub fn transform_pixels_uninit<'dst>(&self, src: &[InputPixelFormat], dst: &'dst mut [MaybeUninit]) -> &'dst mut [OutputPixelFormat] { let num_pixels = self.num_pixels(src.len(), dst.len()); assert_eq!(num_pixels as usize, dst.len()); unsafe { ffi::cmsDoTransform(self.handle, src.as_ptr().cast::(), dst.as_mut_ptr().cast::(), num_pixels); // assume_init for slices is currently still unstable std::mem::transmute::<&'dst mut [MaybeUninit], &'dst mut [OutputPixelFormat]>(dst) } } #[inline] #[track_caller] pub fn new_flags_context(context: impl AsRef, input: &Profile, in_format: PixelFormat, output: &Profile, out_format: PixelFormat, intent: Intent, flags: Flags) -> LCMSResult { Self::check_formats(in_format, out_format); unsafe { Self::new_handle(ffi::cmsCreateTransformTHR(context.as_ref().as_ptr(), input.handle, in_format, output.handle, out_format, intent, flags.bits())) } } #[inline] #[track_caller] pub fn new_proofing_context(context: impl AsRef, input: &Profile, in_format: PixelFormat, output: &Profile, out_format: PixelFormat, proofing: &Profile, intent: Intent, proofng_intent: Intent, flags: Flags) -> LCMSResult { Self::check_formats(in_format, out_format); unsafe { Self::new_handle(ffi::cmsCreateProofingTransformTHR(context.as_ref().as_ptr(), input.handle, in_format, output.handle, out_format, proofing.handle, intent, proofng_intent, flags.bits())) } } #[inline] #[track_caller] pub fn new_multiprofile_context(context: impl AsRef, profiles: &[&Profile], in_format: PixelFormat, out_format: PixelFormat, intent: Intent, flags: Flags) -> LCMSResult { Self::check_formats(in_format, out_format); let mut handles: Vec<_> = profiles.iter().map(|p| p.handle).collect(); unsafe { Self::new_handle( ffi::cmsCreateMultiprofileTransformTHR(context.as_ref().as_ptr(), handles.as_mut_ptr(), handles.len() as u32, in_format, out_format, intent, flags.bits()), ) } } } impl Transform { #[inline] #[must_use] pub fn input_format(&self) -> PixelFormat { unsafe { ffi::cmsGetTransformInputFormat(self.handle) as PixelFormat } } #[inline] #[must_use] pub fn output_format(&self) -> PixelFormat { unsafe { ffi::cmsGetTransformOutputFormat(self.handle) as PixelFormat } } } impl Transform { /// Adaptation state for absolute colorimetric intent, on all but `cmsCreateExtendedTransform`. /// /// See `ThreadContext::adaptation_state()` #[inline] #[must_use] pub fn global_adaptation_state() -> f64 { unsafe { ffi::cmsSetAdaptationState(-1.) } } /// Sets adaptation state for absolute colorimetric intent, on all but `cmsCreateExtendedTransform`. /// Little CMS can handle incomplete adaptation states. /// /// See `ThreadContext::set_adaptation_state()` /// /// Degree on adaptation 0=Not adapted, 1=Complete adaptation, in-between=Partial adaptation. #[deprecated(note = "Use `ThreadContext::set_adaptation_state()`")] pub fn set_global_adaptation_state(value: f64) { unsafe { ffi::cmsSetAdaptationState(value); } } /// Sets the global codes used to mark out-out-gamut on Proofing transforms. Values are meant to be encoded in 16 bits. /// `AlarmCodes`: Array [16] of codes. ALL 16 VALUES MUST BE SPECIFIED, set to zero unused channels. /// /// See `ThreadContext::set_alarm_codes()` #[deprecated(note = "Use `ThreadContext::set_alarm_codes()`")] pub fn set_global_alarm_codes(codes: [u16; ffi::MAXCHANNELS]) { unsafe { ffi::cmsSetAlarmCodes(codes.as_ptr()) } } /// Gets the current global codes used to mark out-out-gamut on Proofing transforms. Values are meant to be encoded in 16 bits. /// /// See `ThreadContext::alarm_codes()` #[must_use] pub fn global_alarm_codes() -> [u16; ffi::MAXCHANNELS] { let mut tmp = [0u16; ffi::MAXCHANNELS]; unsafe { ffi::cmsGetAlarmCodes(tmp.as_mut_ptr()); } tmp } } fn is_u8() -> bool { std::mem::size_of::

() == 1 && std::mem::align_of::

() == 1 && std::any::TypeId::of::

() == std::any::TypeId::of::() } impl Drop for Transform { fn drop(&mut self) { unsafe { ffi::cmsDeleteTransform(self.handle); } } } impl fmt::Debug for Transform { #[cold] fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { let mut s = f.debug_struct(&format!( "Transform<{}, {}, {}, {}>", std::any::type_name::(), std::any::type_name::(), std::any::type_name::(), std::any::type_name::(), )); s.field("input_format", &self.input_format()); s.field("output_format", &self.output_format()); s.finish() } }