Files
photocraft/crates/io/src/effects_map.rs
T

685 lines
31 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 effects ↔ PSD `lfx2` (object-based effects descriptor) and legacy `lrFX`.
//!
//! Key names follow the descriptors Photoshop writes (documented by
//! reverse-engineering in MIT-licensed psd-tools / ag-psd).
use photocraft_color::{BlendMode, Color};
use photocraft_doc::adjust::CurvePoint;
use photocraft_doc::{
Bevel, BevelContour, BevelStyle, BevelTechnique, BevelTexture, Contour, Effect, FxCommon, FxPaint, Glow, GlowSource, GlowTechnique, Gradient, Satin,
Shadow, StrokeFx, StrokePosition,
};
use photocraft_psd::descriptor::{Descriptor, Id, UnicodeString, Value, VersionedDescriptor};
use crate::blocks::{
bool_of, color_from_desc, color_to_desc, enum_of, get_desc, gradient_desc, gradient_style, gradient_style_value, num, pattern_placement, pattern_ref,
pattern_ref_desc, with_pattern_placement,
};
const BLEND_NAMES: [(BlendMode, &str); 28] = [
(BlendMode::Normal, "Nrml"),
(BlendMode::Dissolve, "Dslv"),
(BlendMode::Darken, "Drkn"),
(BlendMode::Multiply, "Mltp"),
(BlendMode::ColorBurn, "CBrn"),
(BlendMode::LinearBurn, "linearBurn"),
(BlendMode::DarkerColor, "darkerColor"),
(BlendMode::Lighten, "Lghn"),
(BlendMode::Screen, "Scrn"),
(BlendMode::ColorDodge, "CDdg"),
(BlendMode::LinearDodge, "linearDodge"),
(BlendMode::LighterColor, "lighterColor"),
(BlendMode::Overlay, "Ovrl"),
(BlendMode::SoftLight, "SftL"),
(BlendMode::HardLight, "HrdL"),
(BlendMode::VividLight, "vividLight"),
(BlendMode::LinearLight, "linearLight"),
(BlendMode::PinLight, "pinLight"),
(BlendMode::HardMix, "hardMix"),
(BlendMode::Difference, "Dfrn"),
(BlendMode::Exclusion, "Xclu"),
(BlendMode::Subtract, "blendSubtraction"),
(BlendMode::Divide, "blendDivide"),
(BlendMode::Hue, "H "),
(BlendMode::Saturation, "Strt"),
(BlendMode::Color, "Clr "),
(BlendMode::Luminosity, "Lmns"),
(BlendMode::PassThrough, "passThrough"),
];
/// Long (string-ID) names some writers use instead of the four-character codes, for the modes
/// whose code is a short ID (`BlnM` `multiply` = `Mltp`). The others already use their long name.
const BLEND_LONG_NAMES: [(BlendMode, &str); 17] = [
(BlendMode::Normal, "normal"),
(BlendMode::Dissolve, "dissolve"),
(BlendMode::Darken, "darken"),
(BlendMode::Multiply, "multiply"),
(BlendMode::ColorBurn, "colorBurn"),
(BlendMode::Lighten, "lighten"),
(BlendMode::Screen, "screen"),
(BlendMode::ColorDodge, "colorDodge"),
(BlendMode::Overlay, "overlay"),
(BlendMode::SoftLight, "softLight"),
(BlendMode::HardLight, "hardLight"),
(BlendMode::Difference, "difference"),
(BlendMode::Exclusion, "exclusion"),
(BlendMode::Hue, "hue"),
(BlendMode::Saturation, "saturation"),
(BlendMode::Color, "color"),
(BlendMode::Luminosity, "luminosity"),
];
/// A blend-mode enum from its four-character code, its long string ID, or the layer record's key:
/// Photoshop 2026 writes `vLit`, `lddg`, `lbrn`… as an effect's `Md ` for some modes (#8567).
pub(crate) fn blend_from_id(v: &[u8]) -> Option<BlendMode> {
BLEND_NAMES
.iter()
.chain(&BLEND_LONG_NAMES)
.find(|(_, n)| n.as_bytes() == v)
.map(|(m, _)| *m)
.or_else(|| <[u8; 4]>::try_from(v).ok().and_then(BlendMode::from_psd_key))
}
fn blend_of(d: &Descriptor, key: &str, default: BlendMode) -> BlendMode {
enum_of(d, key).and_then(blend_from_id).unwrap_or(default)
}
/// A `BlnM` enum with the long string ID where the mode has one (`multiply`, `overlay`), as
/// Photoshop writes smart-filter blend options; the four-character code otherwise.
pub(crate) fn blend_long_value(m: BlendMode) -> Value {
let n = BLEND_LONG_NAMES.iter().chain(&BLEND_NAMES).find(|(b, _)| *b == m).map_or("normal", |(_, n)| n);
Value::Enumerated { type_id: Id::new("BlnM"), value: Id::new(n) }
}
pub(crate) fn blend_value(m: BlendMode) -> Value {
let n = BLEND_NAMES.iter().find(|(b, _)| *b == m).map_or("Nrml", |(_, n)| n);
Value::Enumerated { type_id: Id::new("BlnM"), value: Id::new(n) }
}
fn f(d: &Descriptor, key: &str, default: f32) -> f32 {
num(d.get(key)).map_or(default, |v| v as f32)
}
fn pct(d: &Descriptor, key: &str, default: f32) -> f32 {
f(d, key, default * 100.0) / 100.0
}
fn color(d: &Descriptor, key: &str) -> Color {
get_desc(d, key).and_then(color_from_desc).unwrap_or(Color::BLACK)
}
fn contour(d: &Descriptor, key: &str) -> Contour {
let Some(c) = get_desc(d, key) else { return Contour::Linear };
let name = match c.get("Nm ") {
Some(Value::Text(t)) => t.to_string_lossy(),
_ => String::new(),
};
let mut points = Vec::new();
let mut corners = Vec::new();
if let Some(Value::List(items)) = c.get("Crv ") {
for it in items {
if let Value::Descriptor(p) = it {
points.push(CurvePoint { input: f(p, "Hrzn", 0.0) / 255.0, output: f(p, "Vrtc", 0.0) / 255.0 });
// `Cnty` (continuity): false marks a corner point; absent means smooth.
corners.push(matches!(p.get("Cnty"), Some(Value::Boolean(false))));
}
}
}
if !corners.contains(&true) {
corners.clear();
}
let linear = points.len() == 2 && points[0].input == 0.0 && points[0].output == 0.0 && points[1].input == 1.0 && points[1].output == 1.0;
if points.is_empty() || (linear && (name.is_empty() || name == "Linear")) { Contour::Linear } else { Contour::Custom { name, points, corners } }
}
/// Indices of the contour points a PSD keeps, in order: all of them up to [`PSD_CONTOUR_POINTS`],
/// else both ends and then, one at a time, the point farthest (in output) from the line between
/// the kept points around it, so peaks and valleys (Ring - Double's two rings) survive.
fn psd_contour_points(points: &[CurvePoint]) -> Vec<usize> {
if points.len() <= PSD_CONTOUR_POINTS {
return (0..points.len()).collect();
}
let mut kept = vec![0, points.len() - 1];
while kept.len() < PSD_CONTOUR_POINTS {
let mut farthest: Option<(f32, usize)> = None;
for pair in kept.windows(2) {
let &[a, b] = pair else { continue };
let (Some(pa), Some(pb)) = (points.get(a), points.get(b)) else { continue };
let span = pb.input - pa.input;
for (i, p) in points.iter().enumerate().take(b).skip(a + 1) {
// Only equal inputs (a vertical step) have no span: inputs a few ulps apart still do.
let t = if span != 0.0 && span.is_finite() { (p.input - pa.input) / span } else { 0.0 };
let off = (p.output - (pa.output + t * (pb.output - pa.output))).abs();
if off.is_finite() && farthest.is_none_or(|(f, _)| off > f) {
farthest = Some((off, i));
}
}
}
let Some((_, i)) = farthest else { break };
kept.insert(kept.partition_point(|&k| k < i), i);
}
kept
}
/// The most points Photoshop reads in an effect's contour (`ShpC` › `Crv `): a layer whose effects
/// carry a longer one fails to load ("A problem was encountered reading layer … program error"),
/// and Photoshop then drops all of its effects; 16 loads (#9556).
const PSD_CONTOUR_POINTS: usize = 16;
fn contour_value(c: &Contour) -> Value {
let (name, points) = match c {
Contour::Linear => ("Linear".to_string(), vec![CurvePoint { input: 0.0, output: 0.0 }, CurvePoint { input: 1.0, output: 1.0 }]),
Contour::Custom { name, points, .. } => (name.clone(), points.clone()),
};
let pts = psd_contour_points(&points)
.into_iter()
.filter_map(|i| points.get(i).map(|p| (i, p)))
.map(|(i, p)| {
let d = Descriptor::new("CrPt").with("Hrzn", Value::Double(f64::from(p.input * 255.0))).with("Vrtc", Value::Double(f64::from(p.output * 255.0)));
// Photoshop writes no `Cnty` (continuity) on smooth points; a corner carries `Cnty` false.
Value::Descriptor(if c.is_corner(i) { d.with("Cnty", Value::Boolean(false)) } else { d })
})
.collect();
Value::Descriptor(Descriptor::new("ShpC").with("Nm ", Value::Text(UnicodeString::new_nul(&name))).with("Crv ", Value::List(pts)))
}
fn common(d: &Descriptor, blend: BlendMode, opacity: f32) -> FxCommon {
FxCommon { enabled: !matches!(d.get("enab"), Some(Value::Boolean(false))), blend: blend_of(d, "Md ", blend), opacity: pct(d, "Opct", opacity) }
}
fn gradient_from(d: &Descriptor) -> Gradient {
let (stops, opacity_stops) = get_desc(d, "Grad").map(|g| crate::blocks::gradient_stops_with(g, crate::blocks::gradient_method(d))).unwrap_or_default();
let off = get_desc(d, "Ofst");
Gradient {
stops: if stops.is_empty() { Gradient::default().stops } else { stops },
opacity_stops,
style: gradient_style(d),
angle: f(d, "Angl", 90.0),
scale: pct(d, "Scl ", 1.0),
reverse: bool_of(d, "Rvrs"),
align: !matches!(d.get("Algn"), Some(Value::Boolean(false))),
offset: off.map_or((0.0, 0.0), |o| (pct(o, "Hrzn", 0.0), pct(o, "Vrtc", 0.0))),
}
}
fn parse_shadow(d: &Descriptor, inner: bool) -> Shadow {
Shadow {
common: common(d, BlendMode::Multiply, 0.75),
color: color(d, "Clr "),
angle: f(d, "lagl", 120.0),
use_global_light: !matches!(d.get("uglg"), Some(Value::Boolean(false))),
distance: f(d, "Dstn", 5.0),
spread: pct(d, "Ckmt", 0.0),
size: f(d, "blur", 5.0),
contour: contour(d, "TrnS"),
anti_alias: bool_of(d, "AntA"),
noise: pct(d, "Nose", 0.0),
knocks_out: !inner && !matches!(d.get("layerConceals"), Some(Value::Boolean(false))),
}
}
fn parse_glow(d: &Descriptor, inner: bool) -> Glow {
let paint = if d.get("Grad").is_some() { FxPaint::Gradient(gradient_from(d)) } else { FxPaint::Color(color(d, "Clr ")) };
Glow {
common: common(d, BlendMode::Screen, 0.75),
paint,
technique: if enum_of(d, "GlwT") == Some(b"PrBL") { GlowTechnique::Precise } else { GlowTechnique::Softer },
spread: pct(d, "Ckmt", 0.0),
size: f(d, "blur", 5.0),
contour: contour(d, "TrnS"),
anti_alias: bool_of(d, "AntA"),
range: pct(d, "Inpr", 0.5),
jitter: pct(d, "ShdN", 0.0),
noise: pct(d, "Nose", 0.0),
source: if inner && enum_of(d, "glwS") == Some(b"SrcC") { GlowSource::Center } else { GlowSource::Edge },
}
}
fn parse_stroke(d: &Descriptor) -> StrokeFx {
let paint = match enum_of(d, "PntT") {
Some(b"GrFl") => FxPaint::Gradient(gradient_from(d)),
Some(b"Ptrn") => {
let p = get_desc(d, "Ptrn");
let text = |k: &str| match p.and_then(|p| p.get(k)) {
Some(Value::Text(t)) => t.to_string_lossy(),
_ => String::new(),
};
FxPaint::Pattern { name: text("Nm "), id: text("Idnt"), scale: pct(d, "Scl ", 1.0) }
}
_ => FxPaint::Color(color(d, "Clr ")),
};
StrokeFx {
common: common(d, BlendMode::Normal, 1.0),
size: f(d, "Sz ", 3.0),
position: match enum_of(d, "Styl") {
Some(b"InsF") => StrokePosition::Inside,
Some(b"CtrF") => StrokePosition::Center,
_ => StrokePosition::Outside,
},
paint,
overprint: bool_of(d, "overprint"),
}
}
fn parse_bevel(d: &Descriptor) -> Bevel {
Bevel {
enabled: !matches!(d.get("enab"), Some(Value::Boolean(false))),
style: match enum_of(d, "bvlS") {
Some(b"OtrB") => BevelStyle::OuterBevel,
Some(b"Embs") => BevelStyle::Emboss,
Some(b"PlEb") => BevelStyle::PillowEmboss,
Some(b"strokeEmboss") => BevelStyle::StrokeEmboss,
_ => BevelStyle::InnerBevel,
},
technique: match enum_of(d, "bvlT") {
Some(b"PrBL") => BevelTechnique::ChiselHard,
Some(b"Slmt") => BevelTechnique::ChiselSoft,
_ => BevelTechnique::Smooth,
},
depth: pct(d, "srgR", 1.0),
up: enum_of(d, "bvlD") != Some(b"Out "),
size: f(d, "blur", 5.0),
soften: f(d, "Sftn", 0.0),
angle: f(d, "lagl", 120.0),
altitude: f(d, "Lald", 30.0),
use_global_light: !matches!(d.get("uglg"), Some(Value::Boolean(false))),
gloss_contour: contour(d, "TrnS"),
gloss_anti_alias: bool_of(d, "antialiasGloss"),
highlight: FxCommon { enabled: true, blend: blend_of(d, "hglM", BlendMode::Screen), opacity: pct(d, "hglO", 0.75) },
highlight_color: get_desc(d, "hglC").and_then(color_from_desc).unwrap_or(Color::WHITE),
shadow: FxCommon { enabled: true, blend: blend_of(d, "sdwM", BlendMode::Multiply), opacity: pct(d, "sdwO", 0.75) },
shadow_color: color(d, "sdwC"),
contour: bool_of(d, "useShape").then(|| BevelContour { contour: contour(d, "MpgS"), range: pct(d, "Inpr", 0.5), anti_alias: bool_of(d, "AntA") }),
texture: bool_of(d, "useTexture").then(|| {
let (name, id) = pattern_ref(d);
let (_, link, phase) = pattern_placement(d);
BevelTexture { name, id, scale: pct(d, "Scl ", 1.0), depth: pct(d, "textureDepth", 1.0), invert: bool_of(d, "InvT"), link, phase }
}),
}
}
fn parse_one(key: &str, d: &Descriptor) -> Option<Effect> {
if matches!(d.get("present"), Some(Value::Boolean(false))) {
return None;
}
Some(match key {
"DrSh" => Effect::DropShadow(parse_shadow(d, false)),
"IrSh" => Effect::InnerShadow(parse_shadow(d, true)),
"OrGl" => Effect::OuterGlow(parse_glow(d, false)),
"IrGl" => Effect::InnerGlow(parse_glow(d, true)),
"FrFX" => Effect::Stroke(parse_stroke(d)),
"SoFi" => Effect::ColorOverlay { common: common(d, BlendMode::Normal, 1.0), color: color(d, "Clr ") },
"GrFl" => {
Effect::GradientOverlay { common: common(d, BlendMode::Normal, 1.0), gradient: gradient_from(d), dither: bool_of(d, "Dthr"), interpolation: None }
}
"patternFill" => {
let (name, id) = pattern_ref(d);
let (angle, link, phase) = pattern_placement(d);
Effect::PatternOverlay { common: common(d, BlendMode::Normal, 1.0), name, id, scale: pct(d, "Scl ", 1.0), angle, link, phase }
}
"ChFX" => Effect::Satin(Satin {
common: common(d, BlendMode::Multiply, 0.5),
color: color(d, "Clr "),
angle: f(d, "lagl", 19.0),
distance: f(d, "Dstn", 11.0),
size: f(d, "blur", 14.0),
contour: contour(d, "MpgS"),
anti_alias: bool_of(d, "AntA"),
invert: bool_of(d, "Invr"),
}),
"ebbl" => Effect::BevelEmboss(parse_bevel(d)),
_ => return None,
})
}
/// Single-instance key and multi-instance list key for each kind.
const KINDS: [(&str, &str); 10] = [
("DrSh", "dropShadowMulti"),
("IrSh", "innerShadowMulti"),
("OrGl", ""),
("IrGl", ""),
("ebbl", ""),
("ChFX", ""),
("SoFi", "solidFillMulti"),
("GrFl", "gradientFillMulti"),
("patternFill", ""),
("FrFX", "frameFXMulti"),
];
/// Parses `lfx2` block data: `(master switch, effects)`.
pub fn parse_lfx2(data: &[u8]) -> Option<(bool, Vec<Effect>)> {
let (vd, _) = VersionedDescriptor::parse_prefix(data.get(4..)?).ok()?;
Some(effects_from_descriptor(&vd.descriptor))
}
/// Keys of an effects descriptor (`lfx2`, a style library's `Lefx`) that
/// [`effects_from_descriptor`] reads, or that carry no effect of their own.
pub fn is_known_effects_key(key: &[u8]) -> bool {
KINDS.iter().any(|(single, multi)| single.as_bytes() == key || (!multi.is_empty() && multi.as_bytes() == key))
|| matches!(key, b"masterFXSwitch" | b"Scl " | b"numModifyingFX")
}
/// The effects of an effects descriptor (the body of `lfx2`, a style library's `Lefx`):
/// `(master switch, effects)`.
pub fn effects_from_descriptor(d: &Descriptor) -> (bool, Vec<Effect>) {
let master = !matches!(d.get("masterFXSwitch"), Some(Value::Boolean(false)));
let mut out = Vec::new();
for (single, multi) in KINDS {
let from_multi = (!multi.is_empty()).then(|| d.get(multi)).flatten();
match from_multi {
Some(Value::List(items)) => {
for it in items {
if let Value::Descriptor(x) = it
&& let Some(e) = parse_one(single, x)
{
out.push(e);
}
}
}
_ => {
if let Some(x) = get_desc(d, single)
&& let Some(e) = parse_one(single, x)
{
out.push(e);
}
}
}
}
(master, out)
}
fn unit(u: &[u8; 4], v: f32) -> Value {
Value::UnitFloat { unit: *u, value: f64::from(v) }
}
fn with_common(d: Descriptor, c: &FxCommon) -> Descriptor {
d.with("enab", Value::Boolean(c.enabled))
.with("present", Value::Boolean(true))
.with("showInDialog", Value::Boolean(true))
.with("Md ", blend_value(c.blend))
.with("Opct", unit(b"#Prc", c.opacity * 100.0))
}
fn gradient_keys(mut d: Descriptor, g: &Gradient) -> Descriptor {
d = d
.with("Grad", Value::Descriptor(gradient_desc(&g.stops, &g.opacity_stops)))
.with("Angl", unit(b"#Ang", g.angle))
.with("Type", gradient_style_value(g.style))
.with("Rvrs", Value::Boolean(g.reverse))
.with("Algn", Value::Boolean(g.align))
.with("Scl ", unit(b"#Prc", g.scale * 100.0));
d.with("Ofst", Value::Descriptor(Descriptor::new("Pnt ").with("Hrzn", unit(b"#Prc", g.offset.0 * 100.0)).with("Vrtc", unit(b"#Prc", g.offset.1 * 100.0))))
}
fn write_one(e: &Effect) -> (&'static str, Descriptor) {
match e {
Effect::DropShadow(s) | Effect::InnerShadow(s) => {
let inner = matches!(e, Effect::InnerShadow(_));
let mut d = with_common(Descriptor::new(if inner { "IrSh" } else { "DrSh" }), &s.common)
.with("Clr ", Value::Descriptor(color_to_desc(&s.color)))
.with("uglg", Value::Boolean(s.use_global_light))
.with("lagl", unit(b"#Ang", s.angle))
.with("Dstn", unit(b"#Pxl", s.distance))
.with("Ckmt", unit(b"#Pxl", s.spread * 100.0))
.with("blur", unit(b"#Pxl", s.size))
.with("Nose", unit(b"#Prc", s.noise * 100.0))
.with("AntA", Value::Boolean(s.anti_alias))
.with("TrnS", contour_value(&s.contour));
if !inner {
d = d.with("layerConceals", Value::Boolean(s.knocks_out));
}
(if inner { "IrSh" } else { "DrSh" }, d)
}
Effect::OuterGlow(g) | Effect::InnerGlow(g) => {
let inner = matches!(e, Effect::InnerGlow(_));
let mut d = with_common(Descriptor::new(if inner { "IrGl" } else { "OrGl" }), &g.common);
d = match &g.paint {
FxPaint::Gradient(gr) => gradient_keys(d, gr),
FxPaint::Color(c) => d.with("Clr ", Value::Descriptor(color_to_desc(c))),
FxPaint::Pattern { .. } => d.with("Clr ", Value::Descriptor(color_to_desc(&Color::WHITE))),
};
d = d
.with(
"GlwT",
Value::Enumerated { type_id: Id::new("BETE"), value: Id::new(if g.technique == GlowTechnique::Precise { "PrBL" } else { "SfBL" }) },
)
.with("Ckmt", unit(b"#Pxl", g.spread * 100.0))
.with("blur", unit(b"#Pxl", g.size))
.with("Nose", unit(b"#Prc", g.noise * 100.0))
.with("ShdN", unit(b"#Prc", g.jitter * 100.0))
.with("AntA", Value::Boolean(g.anti_alias))
.with("TrnS", contour_value(&g.contour))
.with("Inpr", unit(b"#Prc", g.range * 100.0));
if inner {
d = d
.with("glwS", Value::Enumerated { type_id: Id::new("IGSr"), value: Id::new(if g.source == GlowSource::Center { "SrcC" } else { "SrcE" }) });
}
(if inner { "IrGl" } else { "OrGl" }, d)
}
Effect::Stroke(s) => {
let styl = match s.position {
StrokePosition::Outside => "OutF",
StrokePosition::Inside => "InsF",
StrokePosition::Center => "CtrF",
};
let mut d = with_common(Descriptor::new("FrFX"), &s.common)
.with("Styl", Value::Enumerated { type_id: Id::new("FStl"), value: Id::new(styl) })
.with("Sz ", unit(b"#Pxl", s.size))
.with("overprint", Value::Boolean(s.overprint));
d = match &s.paint {
FxPaint::Color(c) => {
d.with("PntT", Value::Enumerated { type_id: Id::new("FrFl"), value: Id::new("SClr") }).with("Clr ", Value::Descriptor(color_to_desc(c)))
}
FxPaint::Gradient(g) => gradient_keys(d.with("PntT", Value::Enumerated { type_id: Id::new("FrFl"), value: Id::new("GrFl") }), g),
FxPaint::Pattern { name, id, scale } => d
.with("PntT", Value::Enumerated { type_id: Id::new("FrFl"), value: Id::new("Ptrn") })
.with(
"Ptrn",
Value::Descriptor(
Descriptor::new("Ptrn")
.with("Nm ", Value::Text(UnicodeString::new_nul(name)))
.with("Idnt", Value::Text(UnicodeString::new_nul(id))),
),
)
.with("Scl ", unit(b"#Prc", scale * 100.0)),
};
("FrFX", d)
}
Effect::ColorOverlay { common: c, color } => ("SoFi", with_common(Descriptor::new("SoFi"), c).with("Clr ", Value::Descriptor(color_to_desc(color)))),
Effect::GradientOverlay { common: c, gradient, dither, .. } => {
("GrFl", gradient_keys(with_common(Descriptor::new("GrFl"), c), gradient).with("Dthr", Value::Boolean(*dither)))
}
Effect::PatternOverlay { common: c, name, id, scale, angle, link, phase } => (
"patternFill",
with_pattern_placement(
with_common(Descriptor::new("patternFill"), c)
.with("Ptrn", Value::Descriptor(pattern_ref_desc(name, id)))
.with("Scl ", unit(b"#Prc", scale * 100.0)),
*angle,
*link,
*phase,
),
),
Effect::Satin(s) => (
"ChFX",
with_common(Descriptor::new("ChFX"), &s.common)
.with("Clr ", Value::Descriptor(color_to_desc(&s.color)))
.with("AntA", Value::Boolean(s.anti_alias))
.with("Invr", Value::Boolean(s.invert))
.with("lagl", unit(b"#Ang", s.angle))
.with("Dstn", unit(b"#Pxl", s.distance))
.with("blur", unit(b"#Pxl", s.size))
.with("MpgS", contour_value(&s.contour)),
),
Effect::BevelEmboss(b) => {
let style = match b.style {
BevelStyle::OuterBevel => "OtrB",
BevelStyle::InnerBevel => "InrB",
BevelStyle::Emboss => "Embs",
BevelStyle::PillowEmboss => "PlEb",
BevelStyle::StrokeEmboss => "strokeEmboss",
};
let tech = match b.technique {
BevelTechnique::Smooth => "SfBL",
BevelTechnique::ChiselHard => "PrBL",
BevelTechnique::ChiselSoft => "Slmt",
};
let d = Descriptor::new("ebbl")
.with("enab", Value::Boolean(b.enabled))
.with("present", Value::Boolean(true))
.with("showInDialog", Value::Boolean(true))
.with("hglM", blend_value(b.highlight.blend))
.with("hglC", Value::Descriptor(color_to_desc(&b.highlight_color)))
.with("hglO", unit(b"#Prc", b.highlight.opacity * 100.0))
.with("sdwM", blend_value(b.shadow.blend))
.with("sdwC", Value::Descriptor(color_to_desc(&b.shadow_color)))
.with("sdwO", unit(b"#Prc", b.shadow.opacity * 100.0))
.with("bvlT", Value::Enumerated { type_id: Id::new("bvlT"), value: Id::new(tech) })
.with("bvlS", Value::Enumerated { type_id: Id::new("BESl"), value: Id::new(style) })
.with("uglg", Value::Boolean(b.use_global_light))
.with("lagl", unit(b"#Ang", b.angle))
.with("Lald", unit(b"#Ang", b.altitude))
.with("srgR", unit(b"#Prc", b.depth * 100.0))
.with("blur", unit(b"#Pxl", b.size))
.with("bvlD", Value::Enumerated { type_id: Id::new("BESs"), value: Id::new(if b.up { "In " } else { "Out " }) })
.with("TrnS", contour_value(&b.gloss_contour))
.with("antialiasGloss", Value::Boolean(b.gloss_anti_alias))
.with("Sftn", unit(b"#Pxl", b.soften));
let d = match &b.contour {
Some(c) => d
.with("useShape", Value::Boolean(true))
.with("MpgS", contour_value(&c.contour))
.with("AntA", Value::Boolean(c.anti_alias))
.with("Inpr", unit(b"#Prc", c.range * 100.0)),
None => d.with("useShape", Value::Boolean(false)),
};
let d = match &b.texture {
Some(t) => with_pattern_placement(
d.with("useTexture", Value::Boolean(true))
.with("InvT", Value::Boolean(t.invert))
.with("Scl ", unit(b"#Prc", t.scale * 100.0))
.with("textureDepth", unit(b"#Prc", t.depth * 100.0))
.with("Ptrn", Value::Descriptor(pattern_ref_desc(&t.name, &t.id))),
0.0,
t.link,
t.phase,
),
None => d.with("useTexture", Value::Boolean(false)),
};
("ebbl", d)
}
}
}
/// Serializes effects as `lfx2` block data.
pub fn write_lfx2(master: bool, items: &[Effect]) -> Vec<u8> {
let mut out = 0u32.to_be_bytes().to_vec();
out.extend(VersionedDescriptor::new(effects_descriptor(master, items)).to_bytes());
out
}
/// Effects as an effects descriptor (the body of `lfx2`, a style library's `Lefx`).
pub fn effects_descriptor(master: bool, items: &[Effect]) -> Descriptor {
let mut d = Descriptor::new("null").with("Scl ", unit(b"#Prc", 100.0)).with("masterFXSwitch", Value::Boolean(master));
for (single, multi) in KINDS {
let of_kind: Vec<Descriptor> = items.iter().map(write_one).filter(|(k, _)| *k == single).map(|(_, d)| d).collect();
match of_kind.len() {
0 => {}
1 => d = d.with(single, Value::Descriptor(of_kind.into_iter().next().unwrap_or_default())),
_ if !multi.is_empty() => d = d.with(multi, Value::List(of_kind.into_iter().map(Value::Descriptor).collect())),
// Kinds without a multi key keep only the first instance.
_ => d = d.with(single, Value::Descriptor(of_kind.into_iter().next().unwrap_or_default())),
}
}
d
}
fn fixed(v: u32) -> f32 {
v as i32 as f32 / 65536.0
}
/// Parses legacy `lrFX` effects (Photoshop 5 layout): drop/inner shadows,
/// outer/inner glows and solid fill. Bevels are skipped.
pub fn parse_lrfx(data: &[u8]) -> Option<(bool, Vec<Effect>)> {
let be16 = |at: usize| data.get(at..at + 2).map(|b| u16::from_be_bytes([b[0], b[1]]));
let be32 = |at: usize| data.get(at..at + 4).map(|b| u32::from_be_bytes([b[0], b[1], b[2], b[3]]));
let color_at = |at: usize| -> Option<Color> {
// space (2) + 4 × u16 components (RGB uses the first three).
let c = |k: usize| be16(at + 2 + k * 2).map(|v| f32::from(v >> 8) / 255.0);
Some(Color::rgb(c(0)?, c(1)?, c(2)?))
};
let blend_at = |at: usize| -> BlendMode {
data.get(at + 4..at + 8).and_then(|k| photocraft_color::BlendMode::from_psd_key([k[0], k[1], k[2], k[3]])).unwrap_or(BlendMode::Normal)
};
let count = be16(2)?;
let mut at = 4;
let mut out = Vec::new();
for _ in 0..count {
let sig = data.get(at..at + 4)?;
if sig != b"8BIM" {
return None;
}
let key: [u8; 4] = data.get(at + 4..at + 8)?.try_into().ok()?;
let size = be32(at + 8)? as usize;
let b = at + 12; // start of this effect's data (after the size field)
match &key {
b"dsdw" | b"isdw" => {
// Blur, angle and distance are 16.16 fixed point in practice.
let blur = fixed(be32(b + 4)?);
let angle = fixed(be32(b + 12)?);
let distance = fixed(be32(b + 16)?);
let color = color_at(b + 20)?;
let blend = blend_at(b + 30);
let enabled = *data.get(b + 38)? != 0;
let use_global = *data.get(b + 39)? != 0;
let opacity = f32::from(*data.get(b + 40)?) / 255.0;
let s = Shadow {
common: FxCommon { enabled, blend, opacity },
color,
angle,
use_global_light: use_global,
distance,
spread: 0.0,
size: blur,
contour: Contour::Linear,
anti_alias: false,
noise: 0.0,
knocks_out: &key == b"dsdw",
};
out.push(if &key == b"dsdw" { Effect::DropShadow(s) } else { Effect::InnerShadow(s) });
}
b"oglw" | b"iglw" => {
let blur = fixed(be32(b + 4)?);
let color = color_at(b + 12)?;
let blend = blend_at(b + 22);
let enabled = *data.get(b + 30)? != 0;
let opacity = f32::from(*data.get(b + 31)?) / 255.0;
let g = Glow {
common: FxCommon { enabled, blend, opacity },
paint: FxPaint::Color(color),
technique: GlowTechnique::Softer,
spread: 0.0,
size: blur,
contour: Contour::Linear,
anti_alias: false,
range: 0.5,
jitter: 0.0,
noise: 0.0,
source: GlowSource::Edge,
};
out.push(if &key == b"oglw" { Effect::OuterGlow(g) } else { Effect::InnerGlow(g) });
}
b"sofi" => {
let blend = blend_at(b + 4);
let color = color_at(b + 12)?;
let opacity = f32::from(*data.get(b + 22)?) / 255.0;
let enabled = *data.get(b + 23)? != 0;
out.push(Effect::ColorOverlay { common: FxCommon { enabled, blend, opacity }, color });
}
_ => {}
}
at = b + size;
}
Some((true, out))
}
#[cfg(test)]
#[allow(clippy::unreachable)] // clippy.toml exempts unwrap/expect/panic in tests, not unreachable!
mod tests;