Files
photocraft/crates/doc/src/advanced.rs
T
b0680fd421 Advanced Blending: knockout, blend as group, transparency shapes, masks hide effects (FILE-215-16, part of #215) (#903)
* Advanced Blending: knockout, blend as group, transparency shapes, masks hide effects (part of #215)

Blending Options > Advanced Blending now works end to end:

- doc: AdvancedBlending on every layer (knockout none/shallow/deep, blend
  interior effects as group, blend clipped layers as group, transparency
  shapes layer, layer/vector mask hides effects), saved in .pcraft.
- PSD and layered TIFF: knko, infx, clbl, tsly, lmgm, vmgm read into the
  model and written back (in place, or added when not at the defaults);
  the TIFF byte-order transcoder handles lmgm/vmgm as byte flags.
- CPU compositor (compose/src/advanced.rs): shallow and deep knockouts
  (isolated groups and clipping groups stop a deep knockout), clipped
  layers blended individually, effects over the whole layer when
  Transparency Shapes is off, effects built from an unmasked copy when a
  mask hides them, interior effects under fill opacity.
- GPU: documents using Advanced Blending fall back to the CPU compositor.
- engine: layer.layerStyle.blendingOptions takes channels, knockout and the
  five switches; inspect reports them; Copy/Paste Layer Style carries them.
- UI: the Blending Options page laid out like Photoshop's (General,
  Advanced Blending, Blend If with split sliders), translated.

psd-tools corpus oracle 229 -> 236 (seven knockout files now pass; floor
raised); other corpora and all round trips unchanged. Scorecard
FILE-215-16 -> partial.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Polish catalog rows for Advanced Blending; regenerate docs/scorecard.md

pl.tsv landed on main and the coverage test needs every catalog to translate new UI strings.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Undo damage: compare Layer.advanced in layer_pixels_equal; regenerate scorecard

#810's exhaustive Layer destructure must classify the new advanced-blending
field; it changes the composite, so it is compared.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Regenerate docs/scorecard.md

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: leon <claude.udder842@passmail.net>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-08 20:08:03 -04:00

153 lines
6.1 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! Layer Style › Blending Options › Advanced Blending: knockout and the "as group" / "hides
//! effects" switches. Fill opacity ([`crate::Layer::fill_opacity`]), the channel restrictions
//! ([`crate::Layer::excluded_channels`]) and Blend If ([`crate::Layer::blend_if`]) live on the
//! layer itself; this struct holds the rest of the section.
//!
//! Defaults are Photoshop's: no knockout, interior effects blended separately, clipped layers
//! blended as a group, transparency shaping the layer's effects and knockout, and masks applied
//! before the effects are built (so the effects follow the masked shape).
//!
//! PSD: `knko` (0 none, 1 shallow, 2 deep), `infx`, `clbl`, `tsly`, `lmgm`, `vmgm`, each a
//! one-byte flag followed by three bytes of padding (Adobe Photoshop File Format specification,
//! "Additional Layer Information").
use serde::{Deserialize, Serialize};
/// Blending Options › Advanced Blending › Knockout: which layers beneath this one its shape
/// punches through. Fill opacity controls the strength (0 % fill shows the knocked-out area bare).
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum Knockout {
/// The layer blends with everything beneath it (the default).
#[default]
None,
/// Knocks out to the bottom of the enclosing layer group or clipping group (for a layer at
/// the top level: to the Background layer, or to transparency without one).
Shallow,
/// Knocks out to the Background layer (to transparency without one), through every group.
Deep,
}
impl Knockout {
/// The PSD `knko` value (0 none, 1 shallow, 2 deep).
pub fn to_psd(self) -> u8 {
match self {
Knockout::None => 0,
Knockout::Shallow => 1,
Knockout::Deep => 2,
}
}
/// From a PSD `knko` value; values above 2 (not written by Photoshop) read as deep.
pub fn from_psd(v: u8) -> Self {
match v {
0 => Knockout::None,
1 => Knockout::Shallow,
_ => Knockout::Deep,
}
}
/// The parameter / UI name (`none`, `shallow`, `deep`).
pub fn name(self) -> &'static str {
match self {
Knockout::None => "none",
Knockout::Shallow => "shallow",
Knockout::Deep => "deep",
}
}
/// Inverse of [`Knockout::name`] (case-insensitive).
pub fn from_name(s: &str) -> Option<Self> {
match s.to_ascii_lowercase().as_str() {
"none" => Some(Knockout::None),
"shallow" => Some(Knockout::Shallow),
"deep" => Some(Knockout::Deep),
_ => None,
}
}
}
/// The Advanced Blending switches of a layer (see the module docs). `Default` is Photoshop's.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(default, rename_all = "camelCase")]
pub struct AdvancedBlending {
/// Knockout: None / Shallow / Deep. PSD `knko`.
pub knockout: Knockout,
/// Blend Interior Effects as Group: Inner Glow, Satin and the Color, Gradient and Pattern
/// Overlays are combined with the layer's content before its fill opacity and blend mode
/// apply (off: fill opacity leaves them alone). PSD `infx`, default off.
pub blend_interior: bool,
/// Blend Clipped Layers as Group: the clipping group composites as a whole in the base
/// layer's blend mode (off: each clipped layer blends with the layers beneath the base in its
/// own mode, inside the base's shape). PSD `clbl`, default on.
pub blend_clipped: bool,
/// Transparency Shapes Layer: the layer's transparency limits its effects and knockout (off:
/// they cover the whole layer). PSD `tsly`, default on.
pub transparency_shapes: bool,
/// Layer Mask Hides Effects: the layer mask hides the effects instead of shaping them. PSD
/// `lmgm` ("layer mask as global mask"), default off.
pub layer_mask_hides_effects: bool,
/// Vector Mask Hides Effects: the vector mask hides the effects instead of shaping them. PSD
/// `vmgm`, default off.
pub vector_mask_hides_effects: bool,
}
impl Default for AdvancedBlending {
fn default() -> Self {
AdvancedBlending {
knockout: Knockout::None,
blend_interior: false,
blend_clipped: true,
transparency_shapes: true,
layer_mask_hides_effects: false,
vector_mask_hides_effects: false,
}
}
}
impl AdvancedBlending {
/// Whether every switch is at Photoshop's default.
pub fn is_default(&self) -> bool {
*self == Self::default()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn defaults_match_photoshop() {
let a = AdvancedBlending::default();
assert_eq!(a.knockout, Knockout::None);
assert!(!a.blend_interior);
assert!(a.blend_clipped);
assert!(a.transparency_shapes);
assert!(!a.layer_mask_hides_effects);
assert!(!a.vector_mask_hides_effects);
assert!(a.is_default());
assert!(!AdvancedBlending { blend_clipped: false, ..a }.is_default());
}
#[test]
fn knockout_psd_values_and_names() {
for k in [Knockout::None, Knockout::Shallow, Knockout::Deep] {
assert_eq!(Knockout::from_psd(k.to_psd()), k);
assert_eq!(Knockout::from_name(k.name()), Some(k));
}
assert_eq!(Knockout::from_psd(7), Knockout::Deep);
assert_eq!(Knockout::from_name("DEEP"), Some(Knockout::Deep));
assert_eq!(Knockout::from_name("medium"), None);
}
#[test]
fn partial_json_keeps_photoshop_defaults() {
let a: AdvancedBlending = serde_json::from_str(r#"{"knockout":"deep"}"#).unwrap();
assert_eq!(a, AdvancedBlending { knockout: Knockout::Deep, ..Default::default() });
let b: AdvancedBlending = serde_json::from_str("{}").unwrap();
assert!(b.is_default());
let json = serde_json::to_string(&AdvancedBlending { layer_mask_hides_effects: true, ..Default::default() }).unwrap();
assert!(json.contains("\"layerMaskHidesEffects\":true"), "{json}");
}
}