From b8147a2b068fe13d70fcbd59a032cd66b820bf6f Mon Sep 17 00:00:00 2001 From: 5ohue <86558263+5ohue@users.noreply.github.com> Date: Sat, 11 May 2024 21:29:36 +0300 Subject: [PATCH] Add artifact related functions This makes it possible to blend images with user configurable percent (see `set_image_artifact` documentation) --- src/types/mod.rs | 2 + src/types/statistic_type.rs | 46 ++++++++ src/wand/magick.rs | 207 +++++++++++++++++++++++++++++++++++- 3 files changed, 251 insertions(+), 4 deletions(-) create mode 100644 src/types/statistic_type.rs diff --git a/src/types/mod.rs b/src/types/mod.rs index 550e0b9..ccc17b5 100644 --- a/src/types/mod.rs +++ b/src/types/mod.rs @@ -15,6 +15,7 @@ mod pixel_interpolate_method; mod rendering_intent; mod resolution_type; mod resource_type; +mod statistic_type; pub use self::alpha_channel_option::AlphaChannelOption; pub use self::colorspace_type::ColorspaceType; @@ -33,3 +34,4 @@ pub use self::pixel_interpolate_method::PixelInterpolateMethod; pub use self::rendering_intent::RenderingIntent; pub use self::resolution_type::ResolutionType; pub use self::resource_type::ResourceType; +pub use self::statistic_type::StatisticType; diff --git a/src/types/statistic_type.rs b/src/types/statistic_type.rs new file mode 100644 index 0000000..2a73945 --- /dev/null +++ b/src/types/statistic_type.rs @@ -0,0 +1,46 @@ +use crate::bindings; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[repr(u32)] +pub enum StatisticType { + Undefined = bindings::StatisticType_UndefinedStatistic, + Gradient = bindings::StatisticType_GradientStatistic, + Maximum = bindings::StatisticType_MaximumStatistic, + Mean = bindings::StatisticType_MeanStatistic, + Median = bindings::StatisticType_MedianStatistic, + Minimum = bindings::StatisticType_MinimumStatistic, + Mode = bindings::StatisticType_ModeStatistic, + Nonpeak = bindings::StatisticType_NonpeakStatistic, + RootMeanSquare = bindings::StatisticType_RootMeanSquareStatistic, + StandardDeviation = bindings::StatisticType_StandardDeviationStatistic, + Contrast = bindings::StatisticType_ContrastStatistic, +} + +impl Default for StatisticType { + fn default() -> Self { + return StatisticType::Undefined; + } +} + +impl From for bindings::StatisticType { + fn from(value: StatisticType) -> Self { + return value as bindings::StatisticType; + } +} + +impl From for StatisticType { + fn from(value: bindings::StatisticType) -> Self { + /* + * SAFETY: + * + * `StatisticType` has the same repr as `bindings::StatisticType` - u32 + * + * If `value` is less than Contrast than it is in the vaild range and can be safely + * reinterpreted as `StatisticType` + */ + if value <= bindings::StatisticType_ContrastStatistic { + return unsafe { std::mem::transmute(value) }; + } + return StatisticType::default(); + } +} diff --git a/src/wand/magick.rs b/src/wand/magick.rs index a02876b..aac7b55 100644 --- a/src/wand/magick.rs +++ b/src/wand/magick.rs @@ -47,7 +47,8 @@ use crate::{ PixelInterpolateMethod, RenderingIntent, ResolutionType, - ResourceType + ResourceType, + StatisticType, }; wand_common!( @@ -414,11 +415,21 @@ impl MagickWand { } /// Apply sigmoidal contrast to the image - /// Midpoint is a number in range [0, 1] + /// + /// Adjusts the contrast of an image with a non-linear sigmoidal contrast algorithm. Increase + /// the contrast of the image using a sigmoidal transfer function without saturating highlights + /// or shadows. Contrast indicates how much to increase the contrast (0 is none; 3 is typical; + /// 20 is pushing it); mid-point indicates where midtones fall in the resultant image (0.0 is + /// white; 0.5 is middle-gray; 1.0 is black). Set sharpen to `true` to increase the image + /// contrast otherwise the contrast is reduced. + /// + /// * `sharpen`: increase or decrease image contrast + /// * `strength`: strength of the contrast, the larger the number the more 'threshold-like' it becomes. + /// * `midpoint`: midpoint of the function as a number in range [0, 1] pub fn sigmoidal_contrast_image( &self, sharpen: bool, - contrast: f64, + strength: f64, midpoint: f64, ) -> Result<()> { let quantum_range = self.quantum_range()?; @@ -427,7 +438,7 @@ impl MagickWand { bindings::MagickSigmoidalContrastImage( self.wand, sharpen.to_magick(), - contrast, + strength, midpoint * quantum_range, ) }; @@ -521,6 +532,37 @@ impl MagickWand { } } + /// Replace each pixel with corresponding statistic from the neighborhood of the specified width and height. + /// + /// * `statistic_type`: the statistic type (e.g. `StatisticType::Median`, `StatisticType::Mode`, etc.). + /// * `width`: the width of the pixel neighborhood. + /// * `height`: the height of the pixel neighborhood. + pub fn statistic_image( + &self, + statistic_type: StatisticType, + width: usize, + height: usize, + ) -> Result<()> { + match unsafe { + bindings::MagickStatisticImage( + self.wand, + statistic_type.into(), + width.into(), + height.into() + ) + } { + MagickTrue => Ok(()), + _ => Err(MagickError("failed to calculate statistics for image")), + } + } + + /// Calculate median for each pixel's neighborhood. + /// + /// See [statistic_image](Self::statistic_image) + pub fn median_blur_image(&self, width: usize, height: usize) -> Result<()> { + return self.statistic_image(StatisticType::Median, width, height); + } + /// Adaptively resize the currently selected image. pub fn adaptive_resize_image(&self, width: usize, height: usize) -> Result<()> { match unsafe { bindings::MagickAdaptiveResizeImage(self.wand, width, height) } { @@ -577,6 +619,131 @@ impl MagickWand { } } + /// Returns a value associated with the specified artifact. + /// + /// * `artifact`: the artifact. + pub fn get_image_artifact(&self, artifact: &str) -> Result { + let c_artifact = CString::new(artifact).map_err(|_| MagickError("artifact string contains null byte"))?; + + let result = unsafe { + bindings::MagickGetImageArtifact( + self.wand, + c_artifact.as_ptr() + ) + }; + + let value = if result.is_null() { + Err(MagickError("missing artifact")) + } else { + // convert (and copy) the C string to a Rust string + let cstr = unsafe { CStr::from_ptr(result) }; + Ok(cstr.to_string_lossy().into_owned()) + }; + + unsafe { + bindings::MagickRelinquishMemory(result as *mut c_void); + } + + value + } + + pub fn get_image_artifacts(&self, pattern: &str) -> Result> { + let c_pattern = CString::new(pattern).map_err(|_| MagickError("artifact string contains null byte"))?; + let mut num_of_artifacts: size_t = 0; + + let result = unsafe { + bindings::MagickGetImageArtifacts( + self.wand, + c_pattern.as_ptr(), + &mut num_of_artifacts + ) + }; + + let value = if result.is_null() { + Err(MagickError("no artifacts found")) + } else { + let mut artifacts: Vec = Vec::with_capacity(num_of_artifacts); + for i in 0..num_of_artifacts { + // convert (and copy) the C string to a Rust string + let cstr = unsafe { CStr::from_ptr(*result.add(i)) }; + artifacts.push(cstr.to_string_lossy().into_owned()); + } + + Ok(artifacts) + }; + + unsafe { + bindings::MagickRelinquishMemory(result as *mut c_void); + } + + value + } + + /// Sets a key-value pair in the image artifact namespace. Artifacts differ from properties. + /// Properties are public and are generally exported to an external image format if the format + /// supports it. Artifacts are private and are utilized by the internal ImageMagick API to + /// modify the behavior of certain algorithms. + /// + /// * `artifact`: the artifact. + /// * `value`: the value. + /// + /// # Example + /// + /// This example shows how you can blend an image with its blurred copy with 50% opacity by + /// setting "compose:args" to "50". This is equivalent to having `-define compose:args=50` when + /// using imagemagick cli. + /// + /// ``` + /// let mut wand1 = MagickWand::new(); + /// wand1.read_image("test.jpg")?; + /// let wand2 = wand1.clone(); + /// + /// wand1.median_blur_image(10, 10)?; + /// + /// wand1.set_image_artifact("compose:args", "50")?; + /// wand1.compose_images(&wand2, CompositeOperator::Blend, false, 0, 0)?; + /// + /// wand1.write_image("res.jpeg")?; + /// ``` + pub fn set_image_artifact( + &mut self, + artifact: &str, + value: &str + ) -> Result<()> { + let c_artifact = CString::new(artifact).map_err(|_| MagickError("artifact string contains null byte"))?; + let c_value = CString::new(value).map_err(|_| MagickError("value string contains null byte"))?; + + let result = unsafe { + bindings::MagickSetImageArtifact( + self.wand, + c_artifact.as_ptr(), + c_value.as_ptr() + ) + }; + + match result { + MagickTrue => Ok(()), + _ => Err(MagickError("failed to set image artifact")), + } + } + + /// Deletes a wand artifact. + /// + /// * `artifact`: the artifact. + pub fn delete_image_artifact(&mut self, artifact: &str) -> Result<()> { + let c_artifact = CString::new(artifact).map_err(|_| MagickError("artifact string contains null byte"))?; + + match unsafe { + bindings::MagickDeleteImageArtifact( + self.wand, + c_artifact.as_ptr() + ) + } { + MagickTrue => Ok(()), + _ => Err(MagickError("failed to delete image artifact")), + } + } + /// Retrieve the named image property value. pub fn get_image_property(&self, name: &str) -> Result { let c_name = CString::new(name).unwrap(); @@ -594,6 +761,38 @@ impl MagickWand { value } + pub fn get_image_properties(&self, pattern: &str) -> Result> { + let c_pattern = CString::new(pattern).map_err(|_| MagickError("artifact string contains null byte"))?; + let mut num_of_artifacts: size_t = 0; + + let result = unsafe { + bindings::MagickGetImageProperties( + self.wand, + c_pattern.as_ptr(), + &mut num_of_artifacts + ) + }; + + let value = if result.is_null() { + Err(MagickError("no artifacts found")) + } else { + let mut artifacts: Vec = Vec::with_capacity(num_of_artifacts); + for i in 0..num_of_artifacts { + // convert (and copy) the C string to a Rust string + let cstr = unsafe { CStr::from_ptr(*result.add(i)) }; + artifacts.push(cstr.to_string_lossy().into_owned()); + } + + Ok(artifacts) + }; + + unsafe { + bindings::MagickRelinquishMemory(result as *mut c_void); + } + + value + } + /// Set the named image property. pub fn set_image_property(&self, name: &str, value: &str) -> Result<()> { let c_name = CString::new(name).unwrap();