diff --git a/src/blend.rs b/src/blend.rs new file mode 100644 index 0000000..06d60a0 --- /dev/null +++ b/src/blend.rs @@ -0,0 +1,432 @@ +use std::cmp::min; + +use crate::sections::layer_and_mask_information_section::layer::BlendMode; + +// Multiplies the pixel's current alpha by the passed in `opacity` +pub(crate) fn apply_opacity(pixel: &mut [u8; 4], opacity: u8) { + let alpha = opacity as f32 / 255.; + pixel[3] = (pixel[3] as f32 * alpha) as u8; +} + +/// +/// https://www.w3.org/TR/compositing-1/#simplealphacompositing +/// `Cs = (1 - αb) x Cs + αb x B(Cb, Cs)` +/// `cs = Cs x αs` +/// `cb = Cb x αb` +/// `co = cs + cb x (1 - αs)` +/// Where +/// - Cs: is the source color +/// - Cb: is the backdrop color +/// - αs: is the source alpha +/// - αb: is the backdrop alpha +/// - B(Cb, Cs): is the mixing function +/// +/// `αo = αs + αb x (1 - αs)` +/// Where +/// - αo: the alpha value of the composite +/// - αs: the alpha value of the graphic element being composited +/// - αb: the alpha value of the backdrop +/// +/// Final: +/// `Co = co / αo` +/// +/// *The backdrop is the content behind the element and is what the element is composited with. This means that the backdrop is the result of compositing all previous elements. +pub(crate) fn blend_pixels( + top: [u8; 4], + bottom: [u8; 4], + blend_mode: BlendMode, + out: &mut [u8; 4], +) { + // TODO: make some optimizations + let alpha_s = top[3] as f32 / 255.; + let alpha_b = bottom[3] as f32 / 255.; + let alpha_output = alpha_s + alpha_b * (1. - alpha_s); + + let (r_s, g_s, b_s) = ( + top[0] as f32 / 255., + top[1] as f32 / 255., + top[2] as f32 / 255., + ); + let (r_b, g_b, b_b) = ( + bottom[0] as f32 / 255., + bottom[1] as f32 / 255., + bottom[2] as f32 / 255., + ); + + let blend_f = map_blend_mode(blend_mode); + let (r, g, b) = ( + composite(r_s, alpha_s, r_b, alpha_b, blend_f) * 255., + composite(g_s, alpha_s, g_b, alpha_b, blend_f) * 255., + composite(b_s, alpha_s, b_b, alpha_b, blend_f) * 255., + ); + + out[0] = (r.round() / alpha_output) as u8; + out[1] = (g.round() / alpha_output) as u8; + out[2] = (b.round() / alpha_output) as u8; + out[3] = (255. * alpha_output).round() as u8; +} + +type BlendFunction = dyn Fn(f32, f32) -> f32; + +/// Returns blend function for given BlendMode +fn map_blend_mode(blend_mode: BlendMode) -> &'static BlendFunction { + // Modes are sorted like in Photoshop UI + // TODO: make other modes + match blend_mode { + BlendMode::PassThrough => &pass_through, // only for groups + // -------------------------------------- + BlendMode::Normal => &normal, + BlendMode::Dissolve => &dissolve, + // -------------------------------------- + BlendMode::Darken => &darken, + BlendMode::Multiply => &multiply, + BlendMode::ColorBurn => &color_burn, + BlendMode::LinearBurn => &linear_burn, + BlendMode::DarkerColor => &darker_color, + // -------------------------------------- + BlendMode::Lighten => &lighten, + BlendMode::Screen => &screen, + BlendMode::ColorDodge => &color_dodge, + BlendMode::LinearDodge => &linear_dodge, + BlendMode::LighterColor => &lighter_color, + // -------------------------------------- + BlendMode::Overlay => &overlay, + BlendMode::SoftLight => &soft_light, + BlendMode::HardLight => &hard_light, + BlendMode::VividLight => &vivid_light, + BlendMode::LinearLight => &linear_light, + BlendMode::PinLight => &pin_light, + BlendMode::HardMix => &hard_mix, + // -------------------------------------- + BlendMode::Difference => &difference, + BlendMode::Exclusion => &exclusion, + BlendMode::Subtract => &subtract, + BlendMode::Divide => ÷, + // -------------------------------------- + BlendMode::Hue => &hue, + BlendMode::Saturation => &saturation, + BlendMode::Color => &color, + BlendMode::Luminosity => &luminosity, + } +} + +fn pass_through(color_b: f32, color_s: f32) -> f32 { + unimplemented!() +} + +/// https://www.w3.org/TR/compositing-1/#blendingnormal +/// This is the default attribute which specifies no blending. The blending formula simply selects the source color. +/// +/// `B(Cb, Cs) = Cs` +#[inline(always)] +fn normal(color_b: f32, color_s: f32) -> f32 { + color_s +} + +fn dissolve(color_b: f32, color_s: f32) -> f32 { + unimplemented!() +} + +// Darken modes + +/// https://www.w3.org/TR/compositing-1/#blendingdarken +/// Selects the darker of the backdrop and source colors. +/// +/// The backdrop is replaced with the source where the source is darker; otherwise, it is left unchanged. +/// +/// `B(Cb, Cs) = min(Cb, Cs)` +#[inline(always)] +fn darken(color_b: f32, color_s: f32) -> f32 { + color_b.min(color_s) +} + +/// https://www.w3.org/TR/compositing-1/#blendingmultiply +/// The source color is multiplied by the destination color and replaces the destination. +/// The resultant color is always at least as dark as either the source or destination color. +/// Multiplying any color with black results in black. Multiplying any color with white preserves the original color. +/// +/// `B(Cb, Cs) = Cb x Cs` +#[inline(always)] +fn multiply(color_b: f32, color_s: f32) -> f32 { + color_b * color_s +} + +/// https://www.w3.org/TR/compositing-1/#blendingcolorburn +/// +/// Darkens the backdrop color to reflect the source color. Painting with white produces no change. +/// +/// ```text +/// if(Cb == 1) +/// B(Cb, Cs) = 1 +/// else +/// B(Cb, Cs) = max(0, (1 - (1 - Cs) / Cb)) +///``` +#[inline(always)] +fn color_burn(color_b: f32, color_s: f32) -> f32 { + if color_b == 1. { + 1. + } else { + (1. - (1. - color_s) / color_b).max(0.) + } +} + +/// See: http://www.simplefilter.de/en/basics/mixmods.html +/// psd_tools impl: https://github.com/psd-tools/psd-tools/blob/master/src/psd_tools/composer/blend.py#L139 +/// +/// This variant of subtraction is also known as subtractive color blending. +/// The tonal values of fore- and background that sum up to less than 255 (i.e. 1.0) become pure black. +/// If the foreground image A is converted prior to the operation, the result is the mathematical subtraction. +/// +/// `B(Cb, Cs) = max(0, Cb + Cs - 1)` +#[inline(always)] +fn linear_burn(color_b: f32, color_s: f32) -> f32 { + (color_b - color_s - 1.).max(0.) +} + +fn darker_color(color_b: f32, color_s: f32) -> f32 { + unimplemented!() +} + +// Lighten modes + +/// https://www.w3.org/TR/compositing-1/#blendinglighten +/// Selects the lighter of the backdrop and source colors. +/// +/// The backdrop is replaced with the source where the source is lighter; otherwise, it is left unchanged. +/// +/// `B(Cb, Cs) = max(Cb, Cs)` +#[inline(always)] +fn lighten(color_b: f32, color_s: f32) -> f32 { + color_b.max(color_s) +} + +/// https://www.w3.org/TR/compositing-1/#blendingscreen +/// Multiplies the complements of the backdrop and source color values, then complements the result. +/// +/// The result color is always at least as light as either of the two constituent colors. +/// Screening any color with white produces white; screening with black leaves the original color unchanged. +/// The effect is similar to projecting multiple photographic slides simultaneously onto a single screen. +/// +/// `B(Cb, Cs) = 1 - [(1 - Cb) x (1 - Cs)] = Cb + Cs - (Cb x Cs)` +#[inline(always)] +fn screen(color_b: f32, color_s: f32) -> f32 { + color_b + color_s - (color_b * color_s) +} + +/// https://www.w3.org/TR/compositing-1/#blendingcolordodge +/// +/// Brightens the backdrop color to reflect the source color. Painting with black produces no changes. +/// +/// ```text +/// if(Cb == 0) +/// B(Cb, Cs) = 0 +/// else if(Cs == 1) +/// B(Cb, Cs) = 1 +/// else +/// B(Cb, Cs) = min(1, Cb / (1 - Cs)) +/// ``` +#[inline(always)] +fn color_dodge(color_b: f32, color_s: f32) -> f32 { + if color_b == 0. { + 0. + } else if color_s == 1. { + 1. + } else { + (color_b / (1. - color_s)).min(1.) + } +} + +/// See: http://www.simplefilter.de/en/basics/mixmods.html +/// +/// Adds the tonal values of fore- and background. +/// +/// Also: Add +/// `B(Cb, Cs) = Cb + Cs` +#[inline(always)] +fn linear_dodge(color_b: f32, color_s: f32) -> f32 { + (color_b + color_s).min(1.) +} + +fn lighter_color(color_b: f32, color_s: f32) -> f32 { + unimplemented!() +} + +// Contrast modes + +/// https://www.w3.org/TR/compositing-1/#blendingoverlay +/// Multiplies or screens the colors, depending on the backdrop color value. +/// +/// Source colors overlay the backdrop while preserving its highlights and shadows. +/// The backdrop color is not replaced but is mixed with the source color to reflect the lightness or darkness of the backdrop. +/// +/// `B(Cb, Cs) = HardLight(Cs, Cb)` +/// Overlay is the inverse of the hard-light blend mode. See the definition of hard-light for the formula. +#[inline(always)] +fn overlay(color_b: f32, color_s: f32) -> f32 { + hard_light(color_s, color_b) // inverted hard_light +} + +/// https://www.w3.org/TR/compositing-1/#blendingsoftlight +/// +/// Darkens or lightens the colors, depending on the source color value. +/// The effect is similar to shining a diffused spotlight on the backdrop. +/// +/// ```text +/// if(Cs <= 0.5) +/// B(Cb, Cs) = Cb - (1 - 2 x Cs) x Cb x (1 - Cb) +/// else +/// B(Cb, Cs) = Cb + (2 x Cs - 1) x (D(Cb) - Cb) +/// ``` +/// with +/// ```text +/// if(Cb <= 0.25) +/// D(Cb) = ((16 * Cb - 12) x Cb + 4) x Cb +/// else +/// D(Cb) = sqrt(Cb) +/// ``` +fn soft_light(color_b: f32, color_s: f32) -> f32 { + // FIXME: this function uses W3C algorithm which is differ from Photoshop's algorithm + // See: https://en.wikipedia.org/wiki/Blend_modes#Soft_Light + let d = if color_b <= 0.25 { + ((16. * color_b - 12.) * color_b + 4.) * color_b + } else { + color_b.sqrt() + }; + + if color_s <= 0.5 { + color_b - (1. - 2. * color_s) * color_b * (1. - color_b) + } else { + color_b + (2. * color_s - 1.) * (d - color_b) + } +} + +/// https://www.w3.org/TR/compositing-1/#blendinghardlight +/// +/// Multiplies or screens the colors, depending on the source color value. +/// The effect is similar to shining a harsh spotlight on the backdrop. +/// +/// ```text +/// if(Cs <= 0.5) +/// B(Cb, Cs) = Multiply(Cb, 2 x Cs) = 2 x Cb x Cs +/// else +/// B(Cb, Cs) = Screen(Cb, 2 x Cs -1) +/// ``` +/// See the definition of `multiply` and `screen` for their formulas. +#[inline(always)] +fn hard_light(color_b: f32, color_s: f32) -> f32 { + if color_s < 0.5 { + multiply(color_b, 2. * color_s) + } else { + screen(color_b, 2. * color_s - 1.) + } +} + +fn vivid_light(color_b: f32, color_s: f32) -> f32 { + unimplemented!() +} + +fn linear_light(color_b: f32, color_s: f32) -> f32 { + unimplemented!() +} + +#[inline(always)] +fn pin_light(color_b: f32, color_s: f32) -> f32 { + unimplemented!() +} + +#[inline(always)] +fn hard_mix(color_b: f32, color_s: f32) -> f32 { + unimplemented!() +} + +// Inversion modes + +/// https://www.w3.org/TR/compositing-1/#blendingdifference +/// +/// Subtracts the darker of the two constituent colors from the lighter color. +/// Painting with white inverts the backdrop color; painting with black produces no change. +/// +/// `B(Cb, Cs) = | Cb - Cs |` +#[inline(always)] +fn difference(color_b: f32, color_s: f32) -> f32 { + (color_b - color_s).abs() +} + +/// https://www.w3.org/TR/compositing-1/#blendingexclusion +/// +/// Produces an effect similar to that of the Difference mode but lower in contrast. +/// Painting with white inverts the backdrop color; painting with black produces no change +/// +/// `B(Cb, Cs) = Cb + Cs - 2 x Cb x Cs` +#[inline(always)] +fn exclusion(color_b: f32, color_s: f32) -> f32 { + color_b + color_s - 2. * color_b * color_s +} + +/// https://helpx.adobe.com/photoshop/using/blending-modes.html +/// +/// Looks at the color information in each channel and subtracts the blend color from the base color. +/// +/// `B(Cb, Cs) = Cb - Cs` +#[inline(always)] +fn subtract(color_b: f32, color_s: f32) -> f32 { + (color_b - color_s).max(0.) +} + +/// https://helpx.adobe.com/photoshop/using/blending-modes.html +/// +/// Looks at the color information in each channel and divides the blend color from the base color. +/// In 8- and 16-bit images, any resulting negative values are clipped to zero. +/// +/// `B(Cb, Cs) = Cb / Cs` +#[inline(always)] +fn divide(color_b: f32, color_s: f32) -> f32 { + if color_s == 0. { + color_b + } else { + color_b / color_s + } +} + +fn hue(color_b: f32, color_s: f32) -> f32 { + unimplemented!() +} + +fn saturation(color_b: f32, color_s: f32) -> f32 { + unimplemented!() +} + +fn color(color_b: f32, color_s: f32) -> f32 { + unimplemented!() +} + +fn luminosity(color_b: f32, color_s: f32) -> f32 { + unimplemented!() +} + +/// https://www.w3.org/TR/compositing-1/#generalformula +/// +/// `Cs = (1 - αb) x Cs + αb x B(Cb, Cs)` +/// `cs = Cs x αs` +/// `cb = Cb x αb` +/// `co = cs + cb x (1 - αs)` +/// Where +/// - Cs: is the source color +/// - Cb: is the backdrop color +/// - αs: is the source alpha +/// - αb: is the backdrop alpha +/// - B(Cb, Cs): is the mixing function +/// +/// *The backdrop is the content behind the element and is what the element is composited with. This means that the backdrop is the result of compositing all previous elements. +fn composite( + color_s: f32, + alpha_s: f32, + color_b: f32, + alpha_b: f32, + blend_f: &BlendFunction, +) -> f32 { + let color_s = (1. - alpha_b) * color_s + alpha_b * blend_f(color_b, color_s); + let cs = color_s * alpha_s; + let cb = color_b * alpha_b; + cs + cb * (1. - alpha_s) +} diff --git a/src/lib.rs b/src/lib.rs index 2f1be1c..fe65ef4 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -27,6 +27,7 @@ use crate::sections::MajorSections; use self::sections::file_header_section::FileHeaderSection; +mod blend; mod psd_channel; mod sections; @@ -196,17 +197,14 @@ impl Psd { } // Filter out layers based on the passed in filter. - let mut layers_to_flatten_bottom_to_top: Vec<(usize, &PsdLayer)> = self + let layers_to_flatten_top_to_bottom: Vec<(usize, &PsdLayer)> = self .layers() .iter() - .rev() .enumerate() + // here we filter transparent layers and invisible layers + .filter(|(_, layer)| (layer.opacity > 0 && layer.visible) || layer.clipping_mask) .filter(|(idx, layer)| filter((*idx, layer))) .collect(); - layers_to_flatten_bottom_to_top.reverse(); - - // index 0 = top layer ... index len = bottom layer - let layers_to_flatten_top_to_bottom = layers_to_flatten_bottom_to_top; let pixel_count = self.width() * self.height(); @@ -309,53 +307,37 @@ impl Psd { let pixel = &layer_rgba[start..end]; let mut copy = [0; 4]; copy.copy_from_slice(pixel); + + blend::apply_opacity(&mut copy, layer.opacity); copy }; // This pixel is fully opaque, return it - if pixel[3] == 255 { + let pixel = if pixel[3] == 255 && layer.opacity == 255 { pixel } else { // If this pixel has some transparency, blend it with the layer below it - - let mut final_pixel = [0; 4]; - - match flattened_layer_top_down_idx + 1 < layers_to_flatten_top_down.len() { + if flattened_layer_top_down_idx + 1 < layers_to_flatten_top_down.len() { + let mut final_pixel = [0; 4]; // This pixel has some transparency and there is a pixel below it, blend them - true => { - let pixel_below = self.flattened_pixel( - flattened_layer_top_down_idx + 1, - pixel_coord, - layers_to_flatten_top_down, - cached_layer_rgba, - ); - - blend_pixels(pixel, pixel_below, &mut final_pixel); - final_pixel - } + let pixel_below = self.flattened_pixel( + flattened_layer_top_down_idx + 1, + pixel_coord, + layers_to_flatten_top_down, + cached_layer_rgba, + ); + + blend::blend_pixels(pixel, pixel_below, layer.blend_mode, &mut final_pixel); + final_pixel + } else { // There is no pixel below this layer, so use it even though it has transparency - false => pixel, + pixel } - } + }; + pixel } } -// blend the two pixels. -// -// TODO: Take the layer's blend mode into account when blending layers. Right now -// we just use ONE_MINUS_SRC_ALPHA blending regardless of the layer. -// Didn't bother cleaning this up to be readable since we need to replace it -// anyways. Need to blend based on the layer's blend mode. -fn blend_pixels(top: [u8; 4], bottom: [u8; 4], out: &mut [u8; 4]) { - let top_alpha = top[3] as f32 / 255.; - - out[0] = (bottom[0] as f32 + ((top[0] as f32 - bottom[0] as f32) * top_alpha)) as u8; - out[1] = (bottom[1] as f32 + ((top[1] as f32 - bottom[1] as f32) * top_alpha)) as u8; - out[2] = (bottom[2] as f32 + ((top[2] as f32 - bottom[2] as f32) * top_alpha)) as u8; - - out[3] = 255; -} - // Methods for working with the final flattened image data impl Psd { /// Get the RGBA pixels for the PSD diff --git a/src/sections/layer_and_mask_information_section/layer.rs b/src/sections/layer_and_mask_information_section/layer.rs index cd68eb2..3b8ad05 100644 --- a/src/sections/layer_and_mask_information_section/layer.rs +++ b/src/sections/layer_and_mask_information_section/layer.rs @@ -26,37 +26,23 @@ pub struct LayerProperties { pub(crate) layer_bottom: i32, /// The position of the right of the layer pub(crate) layer_right: i32, + /// If true, the layer is marked as visible + pub(crate) visible: bool, + /// The opacity of the layer + pub(crate) opacity: u8, + /// If true, the layer is clipping mask + pub(crate) clipping_mask: bool, /// The width of the PSD pub(crate) psd_width: u32, /// The height of the PSD pub(crate) psd_height: u32, + /// Blending mode of the layer + pub(crate) blend_mode: BlendMode, /// If layer is nested, contains parent group ID, otherwise `None` pub(crate) group_id: Option, } impl LayerProperties { - pub fn new( - name: String, - layer_top: i32, - layer_left: i32, - layer_bottom: i32, - layer_right: i32, - psd_width: u32, - psd_height: u32, - group_id: Option, - ) -> Self { - LayerProperties { - name, - layer_top, - layer_left, - layer_bottom, - layer_right, - psd_width, - psd_height, - group_id, - } - } - pub fn from_layer_record( name: String, layer_record: &LayerRecord, @@ -64,16 +50,20 @@ impl LayerProperties { psd_height: u32, group_id: Option, ) -> Self { - Self::new( + LayerProperties { name, - layer_record.top, - layer_record.left, - layer_record.bottom, - layer_record.right, + layer_top: layer_record.top, + layer_left: layer_record.left, + layer_bottom: layer_record.bottom, + layer_right: layer_record.right, + opacity: layer_record.opacity, + clipping_mask: layer_record.clipping_base, + visible: layer_record.visible, + blend_mode: layer_record.blend_mode, psd_width, psd_height, group_id, - ) + } } /// Get the name of the layer @@ -93,6 +83,26 @@ impl LayerProperties { (self.layer_bottom - self.layer_top) as u16 + 1 } + /// If true, the layer is marked as visible + pub fn visible(&self) -> bool { + self.visible + } + + /// The opacity of the layer + pub fn opacity(&self) -> u8 { + self.opacity + } + + /// If true, the layer is clipping mask + pub fn is_clipping_mask(&self) -> bool { + self.clipping_mask + } + + /// Returns blending mode of the layer + pub fn blend_mode(&self) -> BlendMode { + self.blend_mode + } + /// If layer is nested, returns parent group ID, otherwise `None` pub fn parent_id(&self) -> Option { self.group_id @@ -172,6 +182,8 @@ pub enum PsdLayerError { channel )] MissingChannels { channel: PsdChannelKind }, + #[fail(display = r#"Unknown blending mode: {:#?}"#, mode)] + UnknownBlendingMode { mode: [u8; 4] }, } impl PsdLayer { @@ -253,6 +265,76 @@ impl GroupDivider { } } +/// Describes how to blend a layer with the layer below it +#[derive(Debug, Clone, Copy)] +#[allow(missing_docs)] +pub enum BlendMode { + PassThrough = 0, + Normal = 1, + Dissolve = 2, + Darken = 3, + Multiply = 4, + ColorBurn = 5, + LinearBurn = 6, + DarkerColor = 7, + Lighten = 8, + Screen = 9, + ColorDodge = 10, + LinearDodge = 11, + LighterColor = 12, + Overlay = 13, + SoftLight = 14, + HardLight = 15, + VividLight = 16, + LinearLight = 17, + PinLight = 18, + HardMix = 19, + Difference = 20, + Exclusion = 21, + Subtract = 22, + Divide = 23, + Hue = 24, + Saturation = 25, + Color = 26, + Luminosity = 27, +} + +impl BlendMode { + pub(super) fn match_mode(mode: [u8; 4]) -> Option { + match &mode { + b"pass" => Some(BlendMode::PassThrough), + b"norm" => Some(BlendMode::Normal), + b"diss" => Some(BlendMode::Dissolve), + b"dark" => Some(BlendMode::Darken), + b"mul " => Some(BlendMode::Multiply), + b"idiv" => Some(BlendMode::ColorBurn), + b"lbrn" => Some(BlendMode::LinearBurn), + b"dkCl" => Some(BlendMode::DarkerColor), + b"lite" => Some(BlendMode::Lighten), + b"scrn" => Some(BlendMode::Screen), + b"div " => Some(BlendMode::ColorDodge), + b"lddg" => Some(BlendMode::LinearDodge), + b"lgCl" => Some(BlendMode::LighterColor), + b"over" => Some(BlendMode::Overlay), + b"sLit" => Some(BlendMode::SoftLight), + b"hLit" => Some(BlendMode::HardLight), + b"vLit" => Some(BlendMode::VividLight), + b"lLit" => Some(BlendMode::LinearLight), + b"pLit" => Some(BlendMode::PinLight), + b"hMix" => Some(BlendMode::HardMix), + b"diff" => Some(BlendMode::Difference), + b"smud" => Some(BlendMode::Exclusion), + b"fsub" => Some(BlendMode::Subtract), + b"fdiv" => Some(BlendMode::Divide), + b"hue " => Some(BlendMode::Hue), + b"sat " => Some(BlendMode::Saturation), + b"colr" => Some(BlendMode::Color), + b"lum " => Some(BlendMode::Luminosity), + _ => None, + } + } +} + /// A layer record within the layer info section /// /// TODO: Set all ofo these pubs to get things working. Replace with private @@ -277,6 +359,14 @@ pub struct LayerRecord { pub(super) bottom: i32, /// The position of the right of the image pub(super) right: i32, + /// If true, the layer is marked as visible + pub(super) visible: bool, + /// The opacity of the layer + pub(super) opacity: u8, + /// If true, the layer is clipping mask + pub(super) clipping_base: bool, + /// Blending mode of the layer + pub(super) blend_mode: BlendMode, /// Group divider tag pub(super) divider_type: Option, } diff --git a/src/sections/layer_and_mask_information_section/mod.rs b/src/sections/layer_and_mask_information_section/mod.rs index 21a1f21..b3cb591 100644 --- a/src/sections/layer_and_mask_information_section/mod.rs +++ b/src/sections/layer_and_mask_information_section/mod.rs @@ -10,7 +10,7 @@ use crate::psd_channel::PsdChannelKind; use crate::sections::image_data_section::ChannelBytes; use crate::sections::layer_and_mask_information_section::container::NamedItems; use crate::sections::layer_and_mask_information_section::layer::{ - GroupDivider, LayerChannels, LayerRecord, PsdGroup, PsdLayer, + BlendMode, GroupDivider, LayerChannels, LayerRecord, PsdGroup, PsdLayer, PsdLayerError, }; use crate::sections::PsdCursor; @@ -358,17 +358,26 @@ fn read_layer_record(cursor: &mut PsdCursor) -> Result { // We do not currently parse the blend mode signature, skip it cursor.read_4()?; - // We do not currently parse the blend mode key, skip it - cursor.read_4()?; + let mut key = [0; 4]; + key.copy_from_slice(cursor.read_4()?); + let blend_mode = match BlendMode::match_mode(key) { + Some(v) => v, + None => return Err(PsdLayerError::UnknownBlendingMode { mode: key }.into()), + }; - // We do not currently parse the opacity, skip it - cursor.read_1()?; + let opacity = cursor.read_u8()?; - // We do not currently parse the clipping, skip it - cursor.read_1()?; + let clipping_base = cursor.read_u8()?; + let clipping_base = clipping_base == 0; - // We do not currently parse the flags, skip it - cursor.read_1()?; + // We do not currently parse all flags, only visible + // Flags: + // - bit 0 = transparency protected; + // - bit 1 = visible; + // - bit 2 = obsolete; + // - bit 3 = 1 for Photoshop 5.0 and later, tells if bit 4 has useful information; + // - bit 4 = pixel data irrelevant to appearance of document + let visible = cursor.read_u8()? & (1 << 1) != 0; // here we get second bit - visible // We do not currently parse the filter, skip it cursor.read_1()?; @@ -441,6 +450,10 @@ fn read_layer_record(cursor: &mut PsdCursor) -> Result { left, bottom, right, + visible, + opacity, + clipping_base, + blend_mode, divider_type, }) } diff --git a/tests/blend.rs b/tests/blend.rs new file mode 100644 index 0000000..53bdf32 --- /dev/null +++ b/tests/blend.rs @@ -0,0 +1,224 @@ +//! FIXME: Combine these all into one test that iterates through a vector of +//! (PathBuf, [f32; 4]) + +use failure::Error; +use psd::Psd; + +const BLEND_NORMAL_BLUE_RED_PIXEL: [u8; 4] = [85, 0, 170, 192]; +const BLEND_MULTIPLY_BLUE_RED_PIXEL: [u8; 4] = [85, 0, 85, 192]; +const BLEND_SCREEN_BLUE_RED_PIXEL: [u8; 4] = [170, 0, 170, 192]; +const BLEND_OVERLAY_BLUE_RED_PIXEL: [u8; 4] = [170, 0, 85, 192]; + +const BLEND_DARKEN_BLUE_RED_PIXEL: [u8; 4] = [85, 0, 85, 192]; +const BLEND_LIGHTEN_BLUE_RED_PIXEL: [u8; 4] = [170, 0, 170, 192]; + +const BLEND_COLOR_BURN_BLUE_RED_PIXEL: [u8; 4] = [170, 0, 85, 192]; +const BLEND_COLOR_DODGE_BLUE_RED_PIXEL: [u8; 4] = [170, 0, 85, 192]; + +const BLEND_LINEAR_BURN_BLUE_RED_PIXEL: [u8; 4] = [85, 0, 85, 192]; +const BLEND_LINEAR_DODGE_BLUE_RED_PIXEL: [u8; 4] = [170, 0, 170, 192]; + +const BLEND_HARD_LIGHT_BLUE_RED_PIXEL: [u8; 4] = [85, 0, 170, 192]; +const BLEND_SOFT_LIGHT_BLUE_RED_PIXEL: [u8; 4] = [170, 0, 85, 192]; +const BLEND_VIVID_LIGHT_BLUE_RED_PIXEL: [u8; 4] = [85, 0, 170, 192]; +const BLEND_LINEAR_LIGHT_BLUE_RED_PIXEL: [u8; 4] = [85, 0, 169, 192]; +const BLEND_PIN_LIGHT_BLUE_RED_PIXEL: [u8; 4] = [85, 0, 170, 192]; +const BLEND_HARD_MIX_BLUE_RED_PIXEL: [u8; 4] = [170, 0, 85, 192]; + +const BLEND_SUBTRACT_BLUE_RED_PIXEL: [u8; 4] = [170, 0, 85, 192]; +const BLEND_DIVIDE_BLUE_RED_PIXEL: [u8; 4] = [170, 0, 85, 192]; + +const BLEND_DIFFERENCE_BLUE_RED_PIXEL: [u8; 4] = [170, 0, 170, 192]; +const BLEND_EXCLUSION_BLUE_RED_PIXEL: [u8; 4] = [170, 0, 170, 192]; + +/// cargo test --test blend normal -- --exact +#[test] +fn normal() -> Result<(), Error> { + let psd = include_bytes!("./fixtures/blending/blue-red-1x1-normal.psd"); + let psd = Psd::from_bytes(psd)?; + + let image = psd.flatten_layers_rgba(&|_| true)?; + assert_eq!(image[0..4], BLEND_NORMAL_BLUE_RED_PIXEL); + + Ok(()) +} + +/// cargo test --test blend multiply -- --exact +#[test] +fn multiply() -> Result<(), Error> { + let psd = include_bytes!("./fixtures/blending/blue-red-1x1-multiply.psd"); + let psd = Psd::from_bytes(psd)?; + + let image = psd.flatten_layers_rgba(&|_| true)?; + assert_eq!(image[0..4], BLEND_MULTIPLY_BLUE_RED_PIXEL); + + Ok(()) +} + +/// cargo test --test blend screen -- --exact +#[test] +fn screen() -> Result<(), Error> { + let psd = include_bytes!("./fixtures/blending/blue-red-1x1-screen.psd"); + let psd = Psd::from_bytes(psd)?; + + let image = psd.flatten_layers_rgba(&|_| true)?; + assert_eq!(image[0..4], BLEND_SCREEN_BLUE_RED_PIXEL); + + Ok(()) +} + +/// cargo test --test blend overlay -- --exact +#[test] +fn overlay() -> Result<(), Error> { + let psd = include_bytes!("./fixtures/blending/blue-red-1x1-overlay.psd"); + let psd = Psd::from_bytes(psd)?; + + let image = psd.flatten_layers_rgba(&|_| true)?; + assert_eq!(image[0..4], BLEND_OVERLAY_BLUE_RED_PIXEL); + + Ok(()) +} + +/// cargo test --test blend darken -- --exact +#[test] +fn darken() -> Result<(), Error> { + let psd = include_bytes!("./fixtures/blending/blue-red-1x1-darken.psd"); + let psd = Psd::from_bytes(psd)?; + + let image = psd.flatten_layers_rgba(&|_| true)?; + assert_eq!(image[0..4], BLEND_DARKEN_BLUE_RED_PIXEL); + + Ok(()) +} + +/// cargo test --test blend lighten -- --exact +#[test] +fn lighten() -> Result<(), Error> { + let psd = include_bytes!("./fixtures/blending/blue-red-1x1-lighten.psd"); + let psd = Psd::from_bytes(psd)?; + + let image = psd.flatten_layers_rgba(&|_| true)?; + assert_eq!(image[0..4], BLEND_LIGHTEN_BLUE_RED_PIXEL); + + Ok(()) +} + +/// cargo test --test blend color_burn -- --exact +#[test] +fn color_burn() -> Result<(), Error> { + let psd = include_bytes!("./fixtures/blending/blue-red-1x1-color-burn.psd"); + let psd = Psd::from_bytes(psd)?; + + let image = psd.flatten_layers_rgba(&|_| true)?; + assert_eq!(image[0..4], BLEND_COLOR_BURN_BLUE_RED_PIXEL); + + Ok(()) +} + +/// cargo test --test blend color_dodge -- --exact +#[test] +fn color_dodge() -> Result<(), Error> { + let psd = include_bytes!("./fixtures/blending/blue-red-1x1-color-dodge.psd"); + let psd = Psd::from_bytes(psd)?; + + let image = psd.flatten_layers_rgba(&|_| true)?; + assert_eq!(image[0..4], BLEND_COLOR_DODGE_BLUE_RED_PIXEL); + + Ok(()) +} + +/// cargo test --test blend linear_burn -- --exact +#[test] +fn linear_burn() -> Result<(), Error> { + let psd = include_bytes!("./fixtures/blending/blue-red-1x1-linear-burn.psd"); + let psd = Psd::from_bytes(psd)?; + + let image = psd.flatten_layers_rgba(&|_| true)?; + assert_eq!(image[0..4], BLEND_LINEAR_BURN_BLUE_RED_PIXEL); + + Ok(()) +} + +/// cargo test --test blend linear_dodge -- --exact +#[test] +fn linear_dodge() -> Result<(), Error> { + let psd = include_bytes!("./fixtures/blending/blue-red-1x1-linear-dodge.psd"); + let psd = Psd::from_bytes(psd)?; + + let image = psd.flatten_layers_rgba(&|_| true)?; + assert_eq!(image[0..4], BLEND_LINEAR_DODGE_BLUE_RED_PIXEL); + + Ok(()) +} + +/// cargo test --test blend hard_light -- --exact +#[test] +fn hard_light() -> Result<(), Error> { + let psd = include_bytes!("./fixtures/blending/blue-red-1x1-hard-light.psd"); + let psd = Psd::from_bytes(psd)?; + + let image = psd.flatten_layers_rgba(&|_| true)?; + assert_eq!(image[0..4], BLEND_HARD_LIGHT_BLUE_RED_PIXEL); + + Ok(()) +} + +/// cargo test --test blend soft_light -- --exact +#[test] +fn soft_light() -> Result<(), Error> { + let psd = include_bytes!("./fixtures/blending/blue-red-1x1-soft-light.psd"); + let psd = Psd::from_bytes(psd)?; + + let image = psd.flatten_layers_rgba(&|_| true)?; + assert_eq!(image[0..4], BLEND_SOFT_LIGHT_BLUE_RED_PIXEL); + + Ok(()) +} + +/// cargo test --test blend divide -- --exact +#[test] +fn divide() -> Result<(), Error> { + let psd = include_bytes!("./fixtures/blending/blue-red-1x1-divide.psd"); + let psd = Psd::from_bytes(psd)?; + + let image = psd.flatten_layers_rgba(&|_| true)?; + assert_eq!(image[0..4], BLEND_DIVIDE_BLUE_RED_PIXEL); + + Ok(()) +} + +/// cargo test --test blend subtract -- --exact +#[test] +fn subtract() -> Result<(), Error> { + let psd = include_bytes!("./fixtures/blending/blue-red-1x1-subtract.psd"); + let psd = Psd::from_bytes(psd)?; + + let image = psd.flatten_layers_rgba(&|_| true)?; + assert_eq!(image[0..4], BLEND_SUBTRACT_BLUE_RED_PIXEL); + + Ok(()) +} + +/// cargo test --test blend difference -- --exact +#[test] +fn difference() -> Result<(), Error> { + let psd = include_bytes!("./fixtures/blending/blue-red-1x1-difference.psd"); + let psd = Psd::from_bytes(psd)?; + + let image = psd.flatten_layers_rgba(&|_| true)?; + assert_eq!(image[0..4], BLEND_DIFFERENCE_BLUE_RED_PIXEL); + + Ok(()) +} + +/// cargo test --test blend exclusion -- --exact +#[test] +fn exclusion() -> Result<(), Error> { + let psd = include_bytes!("./fixtures/blending/blue-red-1x1-exclusion.psd"); + let psd = Psd::from_bytes(psd)?; + + let image = psd.flatten_layers_rgba(&|_| true)?; + assert_eq!(image[0..4], BLEND_EXCLUSION_BLUE_RED_PIXEL); + + Ok(()) +} diff --git a/tests/fixtures/blending/blue-red-1x1-color-burn.psd b/tests/fixtures/blending/blue-red-1x1-color-burn.psd new file mode 100644 index 0000000..dae18d5 Binary files /dev/null and b/tests/fixtures/blending/blue-red-1x1-color-burn.psd differ diff --git a/tests/fixtures/blending/blue-red-1x1-color-dodge.psd b/tests/fixtures/blending/blue-red-1x1-color-dodge.psd new file mode 100644 index 0000000..6858122 Binary files /dev/null and b/tests/fixtures/blending/blue-red-1x1-color-dodge.psd differ diff --git a/tests/fixtures/blending/blue-red-1x1-darken.psd b/tests/fixtures/blending/blue-red-1x1-darken.psd new file mode 100644 index 0000000..b1bbaa8 Binary files /dev/null and b/tests/fixtures/blending/blue-red-1x1-darken.psd differ diff --git a/tests/fixtures/blending/blue-red-1x1-difference.psd b/tests/fixtures/blending/blue-red-1x1-difference.psd new file mode 100644 index 0000000..72a5d1f Binary files /dev/null and b/tests/fixtures/blending/blue-red-1x1-difference.psd differ diff --git a/tests/fixtures/blending/blue-red-1x1-divide.psd b/tests/fixtures/blending/blue-red-1x1-divide.psd new file mode 100644 index 0000000..3ed7720 Binary files /dev/null and b/tests/fixtures/blending/blue-red-1x1-divide.psd differ diff --git a/tests/fixtures/blending/blue-red-1x1-exclusion.psd b/tests/fixtures/blending/blue-red-1x1-exclusion.psd new file mode 100644 index 0000000..33d88af Binary files /dev/null and b/tests/fixtures/blending/blue-red-1x1-exclusion.psd differ diff --git a/tests/fixtures/blending/blue-red-1x1-hard-light.psd b/tests/fixtures/blending/blue-red-1x1-hard-light.psd new file mode 100644 index 0000000..b443a4f Binary files /dev/null and b/tests/fixtures/blending/blue-red-1x1-hard-light.psd differ diff --git a/tests/fixtures/blending/blue-red-1x1-lighten.psd b/tests/fixtures/blending/blue-red-1x1-lighten.psd new file mode 100644 index 0000000..a075b73 Binary files /dev/null and b/tests/fixtures/blending/blue-red-1x1-lighten.psd differ diff --git a/tests/fixtures/blending/blue-red-1x1-linear-burn.psd b/tests/fixtures/blending/blue-red-1x1-linear-burn.psd new file mode 100644 index 0000000..084c747 Binary files /dev/null and b/tests/fixtures/blending/blue-red-1x1-linear-burn.psd differ diff --git a/tests/fixtures/blending/blue-red-1x1-linear-dodge.psd b/tests/fixtures/blending/blue-red-1x1-linear-dodge.psd new file mode 100644 index 0000000..38ade80 Binary files /dev/null and b/tests/fixtures/blending/blue-red-1x1-linear-dodge.psd differ diff --git a/tests/fixtures/blending/blue-red-1x1-multiply.psd b/tests/fixtures/blending/blue-red-1x1-multiply.psd new file mode 100644 index 0000000..117c6b4 Binary files /dev/null and b/tests/fixtures/blending/blue-red-1x1-multiply.psd differ diff --git a/tests/fixtures/blending/blue-red-1x1-normal.psd b/tests/fixtures/blending/blue-red-1x1-normal.psd new file mode 100644 index 0000000..1e03aed Binary files /dev/null and b/tests/fixtures/blending/blue-red-1x1-normal.psd differ diff --git a/tests/fixtures/blending/blue-red-1x1-overlay.psd b/tests/fixtures/blending/blue-red-1x1-overlay.psd new file mode 100644 index 0000000..a2b02c9 Binary files /dev/null and b/tests/fixtures/blending/blue-red-1x1-overlay.psd differ diff --git a/tests/fixtures/blending/blue-red-1x1-screen.psd b/tests/fixtures/blending/blue-red-1x1-screen.psd new file mode 100644 index 0000000..fe3ac97 Binary files /dev/null and b/tests/fixtures/blending/blue-red-1x1-screen.psd differ diff --git a/tests/fixtures/blending/blue-red-1x1-soft-light.psd b/tests/fixtures/blending/blue-red-1x1-soft-light.psd new file mode 100644 index 0000000..20fa8fc Binary files /dev/null and b/tests/fixtures/blending/blue-red-1x1-soft-light.psd differ diff --git a/tests/fixtures/blending/blue-red-1x1-subtract.psd b/tests/fixtures/blending/blue-red-1x1-subtract.psd new file mode 100644 index 0000000..09b34ce Binary files /dev/null and b/tests/fixtures/blending/blue-red-1x1-subtract.psd differ diff --git a/tests/fixtures/green-clipping-10x10.psd b/tests/fixtures/green-clipping-10x10.psd new file mode 100644 index 0000000..88ad98a Binary files /dev/null and b/tests/fixtures/green-clipping-10x10.psd differ diff --git a/tests/layer_and_mask_information_section.rs b/tests/layer_and_mask_information_section.rs index 14829b5..dcf2737 100644 --- a/tests/layer_and_mask_information_section.rs +++ b/tests/layer_and_mask_information_section.rs @@ -36,6 +36,27 @@ fn layer_with_chinese_name() { psd.layer_by_name("圆角矩形").unwrap(); } +/// cargo test --test layer_and_mask_information_section layer_with_clipping -- --exact +#[test] +fn layer_with_clipping() { + let psd = include_bytes!("fixtures/green-clipping-10x10.psd"); + let psd = Psd::from_bytes(psd).unwrap(); + + assert_eq!(psd.layers().len(), 3); + assert_eq!( + psd.layer_by_name("Clipping base") + .unwrap() + .is_clipping_mask(), + true + ); + assert_eq!( + psd.layer_by_name("First clipped layer") + .unwrap() + .is_clipping_mask(), + false + ); +} + const TOP_LEVEL_ID: u32 = 1; /// cargo test --test layer_and_mask_information_section one_group_one_layer_inside -- --exact @@ -101,3 +122,111 @@ fn two_groups_two_layers_inside() { let group = psd.group_by_name("group2").unwrap(); assert_eq!(group.id(), TOP_LEVEL_ID + 1); } + +/// +/// group structure +/// +---------------+----------+---------+ +/// | name | group_id | parent | +/// +---------------+----------+---------+ +/// | group inside | 2 | Some(1) | refers to 'group outside' +/// | group outside | 1 | None | +/// +------------------------------------+ +/// +/// layer structure +/// +-------------+-----+---------+ +/// | name | idx | parent | +/// +-------------+-----+---------+ +/// | First Layer | 0 | Some(1) | refers to 'group inside' +/// +-------------+-----+---------+ +#[test] +fn one_group_inside_another() { + let psd = include_bytes!("fixtures/groups/green-1x1-one-group-inside-another.psd"); + let psd = Psd::from_bytes(psd).unwrap(); + + assert_eq!(psd.layers().len(), 1); + // parent group + children group + assert_eq!(psd.groups().len(), 2); + + // Check group + let group = psd.group_by_name("group outside").unwrap(); + assert_eq!(group.id(), TOP_LEVEL_ID); + + // Check subgroup + let children_group = psd.group_by_name("group inside").unwrap(); + assert_eq!(children_group.parent_id().unwrap(), group.id()); + + let layer = psd.layer_by_name("First Layer").unwrap(); + assert_eq!(children_group.id(), layer.parent_id().unwrap()); +} + +/// +/// PSD file structure +/// group: outside group, parent: `None` +/// group: first group inside, parent: `outside group` +/// layer: First Layer, parent: `first group inside` +/// +/// group: second group inside, parent: `outside group` +/// group: sub sub group, parent: `second group inside` +/// layer: Second Layer, parent: `sub sub group` +/// +/// layer: Third Layer, parent: `second group inside` +/// +/// group: third group inside, parent: `outside group` +/// +/// layer: Fourth Layer, parent: `outside group` +/// layer: Firth Layer, parent: `None` +/// +/// group: outside group 2, parent: `None` +/// layer: Sixth Layer, parent: `outside group 2` +/// +#[test] +fn one_group_with_two_subgroups() { + let psd = include_bytes!("fixtures/groups/green-1x1-one-group-with-two-subgroups.psd"); + let psd = Psd::from_bytes(psd).unwrap(); + + assert_eq!(6, psd.layers().len()); + assert_eq!(6, psd.groups().len()); + + // Check first top-level group + let outside_group = psd.group_by_name("outside group").unwrap(); + assert_eq!(outside_group.id(), 1); + + // Check first subgroup + let children_group = psd.group_by_name("first group inside").unwrap(); + assert_eq!(children_group.parent_id().unwrap(), outside_group.id()); + + let layer = psd.layer_by_name("First Layer").unwrap(); + assert_eq!(children_group.id(), layer.parent_id().unwrap()); + + // Check second subgroup + let children_group = psd.group_by_name("second group inside").unwrap(); + assert_eq!(children_group.parent_id().unwrap(), outside_group.id()); + + // Check `sub sub group` + let sub_sub_group = psd.group_by_name("sub sub group").unwrap(); + assert_eq!(sub_sub_group.parent_id().unwrap(), children_group.id()); + + let layer = psd.layer_by_name("Second Layer").unwrap(); + assert_eq!(sub_sub_group.id(), layer.parent_id().unwrap()); + + let layer = psd.layer_by_name("Third Layer").unwrap(); + assert_eq!(children_group.id(), layer.parent_id().unwrap()); + + // Check third subgroup + let children_group = psd.group_by_name("third group inside").unwrap(); + assert_eq!(children_group.parent_id().unwrap(), outside_group.id()); + + let layer = psd.layer_by_name("Fourth Layer").unwrap(); + assert_eq!(outside_group.id(), layer.parent_id().unwrap()); + + // Check top-level Firth Group + let layer = psd.layer_by_name("Firth Layer").unwrap(); + assert_eq!(layer.parent_id(), None); + + // Check second top-level group + let outside_group = psd.group_by_name("outside group 2").unwrap(); + assert_eq!(outside_group.id(), 6); + + let layer = psd.layer_by_name("Sixth Layer").unwrap(); + assert_eq!(layer.parent_id().unwrap(), outside_group.id()); +}