//! Photoshop import as a composition (File ▸ Import ▸ File… with Import As: Composition or //! Composition – Retain Layer Sizes), and Layer ▸ Create ▸ Convert to Editable Text. //! //! Every pixel layer becomes a footage item that names its layer (`Footage::layer`), placed in //! a composition the size of the document. Groups become precomps (pass-through groups collapse //! so their blend modes act on the layers below, like Photoshop); type layers become editable //! text layers; layer masks are baked into the layer's alpha and vector masks become masks; //! layer effects map to Layer Styles; adjustment layers become adjustment layers with the //! matching effect (best effort); clipped layers preserve the transparency underneath. Smart //! objects whose file is embedded become footage of that file (an embedded Photoshop document's //! merged image, or the embedded image) placed with the smart object's transform. use std::sync::Arc; use effectcraft_color::BlendMode; use effectcraft_color::Label; use effectcraft_keyframe::{Gradient, Justify, ShapePath, TextDoc, Value}; use effectcraft_project::build::{self, Ids}; use effectcraft_project::styles::{self, STYLE_BLEND_MODES}; use effectcraft_project::{AlphaMode, Comp, Footage, FootageKind, ItemId, ItemKind, Layer, LayerSource, MaskMode, Project, PropGroup, Solid, SourceLayer}; use effectcraft_psd::{Adjustment, DValue, Descriptor, Node, Psd}; use effectcraft_time::{FrameRate, Tick}; /// Photoshop blend mode key → compositor mode (`pass` = pass through, handled by collapsing). pub fn blend_mode(key: &str) -> BlendMode { match key { "diss" => BlendMode::Dissolve, "dark" => BlendMode::Darken, "mul " => BlendMode::Multiply, "idiv" => BlendMode::ColorBurn, "lbrn" => BlendMode::LinearBurn, "dkCl" => BlendMode::DarkerColor, "lite" => BlendMode::Lighten, "scrn" => BlendMode::Screen, "div " => BlendMode::ColorDodge, "lddg" => BlendMode::LinearDodge, "lgCl" => BlendMode::LighterColor, "over" => BlendMode::Overlay, "sLit" => BlendMode::SoftLight, "hLit" => BlendMode::HardLight, "vLit" => BlendMode::VividLight, "lLit" => BlendMode::LinearLight, "pLit" => BlendMode::PinLight, "hMix" => BlendMode::HardMix, "diff" => BlendMode::Difference, "smud" => BlendMode::Exclusion, "fsub" => BlendMode::Subtract, "fdiv" => BlendMode::Divide, "hue " => BlendMode::Hue, "sat " => BlendMode::Saturation, "colr" => BlendMode::Color, "lum " => BlendMode::Luminosity, _ => BlendMode::Normal, } } /// Descriptor blend mode enum (`BlnM`) → compositor mode. fn desc_blend(v: Option<&str>) -> Option { Some(match v? { "Nrml" => BlendMode::Normal, "Dslv" => BlendMode::Dissolve, "Drkn" => BlendMode::Darken, "Mltp" => BlendMode::Multiply, "CBrn" => BlendMode::ColorBurn, "linearBurn" => BlendMode::LinearBurn, "darkerColor" => BlendMode::DarkerColor, "Lghn" => BlendMode::Lighten, "Scrn" => BlendMode::Screen, "CDdg" => BlendMode::ColorDodge, "linearDodge" => BlendMode::LinearDodge, "lighterColor" => BlendMode::LighterColor, "Ovrl" => BlendMode::Overlay, "SftL" => BlendMode::SoftLight, "HrdL" => BlendMode::HardLight, "vividLight" => BlendMode::VividLight, "linearLight" => BlendMode::LinearLight, "pinLight" => BlendMode::PinLight, "hardMix" => BlendMode::HardMix, "Dfrn" => BlendMode::Difference, "Xclu" => BlendMode::Exclusion, "blendSubtraction" => BlendMode::Subtract, "blendDivide" => BlendMode::Divide, "H " => BlendMode::Hue, "Strt" => BlendMode::Saturation, "Clr " => BlendMode::Color, "Lmns" => BlendMode::Luminosity, _ => return None, }) } /// What the import created. pub struct PsdImport { pub comp: ItemId, pub folder: ItemId, pub items: Vec, pub warnings: Vec, } struct Ctx<'a> { psd: &'a Psd, path: &'a str, retain: bool, folder: ItemId, rate: FrameRate, duration: Tick, items: Vec, warnings: Vec, } /// Import `psd` (read from `path`) as a composition named `name`. pub fn import(proj: &mut Project, psd: &Psd, path: &str, name: &str, retain: bool, rate: FrameRate, duration: Tick) -> PsdImport { let folder = proj.add_item(&format!("{name} Layers"), Label::Yellow, None, ItemKind::Folder); let mut cx = Ctx { psd, path, retain, folder, rate, duration, items: vec![], warnings: vec![] }; let comp = build_comp(proj, &mut cx, name, &psd.tree(), None); PsdImport { comp, folder, items: cx.items, warnings: cx.warnings } } fn new_comp(cx: &Ctx) -> Comp { Comp::new(cx.psd.width, cx.psd.height, cx.rate, cx.duration) } fn build_comp(proj: &mut Project, cx: &mut Ctx, name: &str, nodes: &[Node], parent: Option) -> ItemId { let cid = proj.add_item(name, Label::Sandstone, parent, ItemKind::Comp(Arc::new(new_comp(cx)))); cx.items.push(cid); let mut comp = new_comp(cx); for n in nodes { if let Some(l) = build_layer(proj, cx, &comp, n) { comp.layers.push(l); } } styles::sync_comp(&mut comp); if let Some(item) = proj.item_mut(cid) { item.kind = ItemKind::Comp(Arc::new(comp)); } cid } fn footage_item(proj: &mut Project, cx: &mut Ctx, index: usize, name: &str) -> (ItemId, (u32, u32)) { let l = &cx.psd.layers[index]; let retain = cx.retain; let (w, h) = if retain && !l.rect.is_empty() { (l.rect.width(), l.rect.height()) } else { (cx.psd.width, cx.psd.height) }; let file = std::path::Path::new(cx.path).file_name().map(|n| n.to_string_lossy().to_string()).unwrap_or_default(); let f = Footage { path: cx.path.to_string(), kind: FootageKind::Still, width: w, height: h, pixel_aspect: 1.0, frame_rate: cx.rate, native_rate: None, duration: Tick::ZERO, has_video: true, has_audio: false, alpha: AlphaMode::Straight, premul_color: [0.0; 3], loop_count: 1, codec: "PSD".into(), missing: false, sequence: vec![], color_profile: None, layer: Some(SourceLayer { index: index as u32, name: name.to_string(), layer_size: retain && !l.rect.is_empty(), embedded: None, placed: false }), ..Default::default() }; let id = proj.add_item(&format!("{name}/{file}"), Label::Lavender, Some(cx.folder), ItemKind::Footage(f)); cx.items.push(id); (id, (w, h)) } /// Size of an embedded file's image: the smart object's recorded size, else the embedded /// document's header (Photoshop, PNG). fn embedded_size(so: &effectcraft_psd::SmartObject, data: &[u8]) -> Option<(u32, u32)> { if let Some([w, h]) = so.size.filter(|s| s[0] >= 1.0 && s[1] >= 1.0) { return Some((w.round() as u32, h.round() as u32)); } if effectcraft_psd::is_psd(data) && data.len() >= 22 { let h = u32::from_be_bytes([data[14], data[15], data[16], data[17]]); let w = u32::from_be_bytes([data[18], data[19], data[20], data[21]]); return Some((w, h)); } if data.starts_with(b"\x89PNG") && data.len() >= 24 { let w = u32::from_be_bytes([data[16], data[17], data[18], data[19]]); let h = u32::from_be_bytes([data[20], data[21], data[22], data[23]]); return Some((w, h)); } None } /// A smart object layer as footage of its embedded file, placed by the smart object's /// transform. `None` when the file is not embedded (linked externally) or unreadable. fn smart_object_layer(proj: &mut Project, cx: &mut Ctx, comp: &Comp, index: usize, name: &str) -> Option { let pl = &cx.psd.layers[index]; let so = pl.smart_object.clone()?; let data = cx.psd.linked_data(&so.uuid)?; let (cw, ch) = embedded_size(&so, data).filter(|s| s.0 > 0 && s.1 > 0)?; let file = cx.psd.linked.iter().find(|f| f.uuid == so.uuid).map(|f| f.name.clone()).unwrap_or_default(); // Perspective quads and warps are baked over the placed bounds, at up to 4× the document // resolution when the embedded content has that much detail. let bake = so.needs_bake(); let (w, h, k, bounds) = if bake { let bb = so.placed_bounds(cw as f64, ch as f64); let area = ((bb[2] - bb[0]) * (bb[3] - bb[1])).max(1.0); let k = ((cw as f64 * ch as f64) / area).sqrt().clamp(1.0, 4.0); let (w, h) = so.placed_size(cw as f64, ch as f64, k); (w, h, k, bb) } else { (cw, ch, 1.0, [0.0; 4]) }; let f = Footage { path: cx.path.to_string(), kind: FootageKind::Still, width: w, height: h, pixel_aspect: 1.0, frame_rate: cx.rate, has_video: true, alpha: AlphaMode::Straight, loop_count: 1, codec: if effectcraft_psd::is_psd(data) { "PSD".into() } else { "PSD (embedded image)".into() }, layer: Some(SourceLayer { index: index as u32, name: name.to_string(), layer_size: true, embedded: Some(so.uuid.clone()), placed: bake }), ..Default::default() }; let label = if file.is_empty() { name.to_string() } else { file }; let id = proj.add_item(&format!("{label} (Smart Object)"), Label::Lavender, Some(cx.folder), ItemKind::Footage(f)); cx.items.push(id); let mut l = build::layer(proj, comp, name, LayerSource::Footage { item: id }, (w, h), None); if bake { // The baked pixels cover the placed bounds: centred there, scaled back to 1×. let c = [bounds[0] + w as f64 / k / 2.0, bounds[1] + h as f64 / k / 2.0]; set_vec2(&mut l, "transform/position", c); set_vec2(&mut l, "transform/scale", [100.0 / k, 100.0 / k]); return Some(l); } let (c, sx, sy, rot) = so.placement(w as f64, h as f64); set_vec2(&mut l, "transform/position", c); set_vec2(&mut l, "transform/scale", [sx * 100.0, sy * 100.0]); if rot.abs() > 1e-9 { set_scalar(&mut l, "transform/rotation", rot); } Some(l) } fn build_layer(proj: &mut Project, cx: &mut Ctx, comp: &Comp, n: &Node) -> Option { let (index, children) = match n { Node::Layer(i) => (*i, None), Node::Group { layer, children } => (*layer, Some(children)), }; let pl = cx.psd.layers[index].clone(); let name = if pl.name.is_empty() { format!("Layer {}", index + 1) } else { pl.name.clone() }; let mut layer = if let Some(children) = children { let sub = build_comp(proj, cx, &name, children, Some(cx.folder)); let mut l = build::layer(proj, comp, &name, LayerSource::Comp { item: sub }, (comp.width, comp.height), None); // Pass-through groups composite their layers straight into the parent. l.switches.collapse = pl.blend_key() == "pass"; l } else if let Some(t) = &pl.text { text_layer(proj, comp, &name, t) } else if let Some(adj) = &pl.adjustment { let mut l = adjustment_layer(proj, comp, &name); add_adjustment_effect(proj, &mut l, adj, comp, &mut cx.warnings, &name); l } else if let Some(l) = pl.smart_object.as_ref().and_then(|_| smart_object_layer(proj, cx, comp, index, &name)) { l } else if pl.has_pixels() || pl.fill.is_some() { let (item, size) = footage_item(proj, cx, index, &name); let mut l = build::layer(proj, comp, &name, LayerSource::Footage { item }, size, None); if cx.retain && !pl.rect.is_empty() { set_vec2(&mut l, "transform/position", [pl.rect.left as f64 + size.0 as f64 / 2.0, pl.rect.top as f64 + size.1 as f64 / 2.0]); } l } else { // Empty layers have nothing to show. return None; }; layer.name = comp.unique_layer_name(&name); layer.blend_mode = blend_mode(pl.blend_key()); layer.switches.video = !pl.hidden; layer.preserve_transparency = pl.clipping; set_scalar(&mut layer, "transform/opacity", pl.opacity as f64 / 255.0 * 100.0); // Vector mask → a mask in layer space. let origin = layer_origin(&pl, cx.retain, &layer); if let Some(vm) = pl.vector_mask.as_ref().filter(|v| !v.disabled) && let Some(masks) = layer.props.sub_mut("masks") { let mut ids = Ids(&mut proj.next_id); for (k, sp) in vm.subpaths.iter().enumerate() { let n = sp.knots.len(); let mut path = ShapePath { vertices: vec![], in_tangents: vec![], out_tangents: vec![], closed: sp.closed, feather: Vec::new() }; for kn in &sp.knots { let v = [kn[1][0] - origin[0], kn[1][1] - origin[1]]; path.vertices.push(v); path.in_tangents.push([kn[0][0] - kn[1][0], kn[0][1] - kn[1][1]]); path.out_tangents.push([kn[2][0] - kn[1][0], kn[2][1] - kn[1][1]]); } if n == 0 { continue; } let mode = if vm.inverted { MaskMode::Subtract } else { MaskMode::Add }; masks.children.push(build::mask(&mut ids, &format!("Mask {}", k + 1), path, mode, [255, 255, 0]).into()); } } // Layer effects → Layer Styles; Fill Opacity → Advanced Blending. let gl = comp.global_light.clone(); if let Some(fx) = &pl.effects && fx.bool("masterFXSwitch") != Some(false) && layer.can_have_styles() { apply_effects(proj, &mut layer, fx, &gl); } if pl.fill_opacity < 255 && layer.can_have_styles() { let mut ids = Ids(&mut proj.next_id); if layer.props.sub(styles::GROUP).is_none() { let g = styles::layer_styles(&mut ids, &gl); let at = layer.props.children.iter().position(|c| c.match_id() == "transform").map(|i| i + 1).unwrap_or(layer.props.children.len()); layer.props.children.insert(at, g.into()); } if let Some(p) = layer.props.prop_mut("layerStyles/blendingOptions/advancedBlending/fillOpacity") { p.value = Value::Scalar(pl.fill_opacity as f64 / 255.0 * 100.0); } } Some(layer) } /// Where layer space's origin sits in document pixels. fn layer_origin(pl: &effectcraft_psd::Layer, retain: bool, layer: &Layer) -> [f64; 2] { if let Some(t) = &pl.text { return [t.transform[4], t.transform[5]]; } if matches!(layer.source, LayerSource::Footage { .. }) && retain && !pl.rect.is_empty() { return [pl.rect.left as f64, pl.rect.top as f64]; } [0.0, 0.0] } fn set_scalar(l: &mut Layer, path: &str, v: f64) { if let Some(p) = l.props.prop_mut(path) { p.value = Value::Scalar(v); } } fn set_vec2(l: &mut Layer, path: &str, v: [f64; 2]) { if let Some(p) = l.props.prop_mut(path) { p.value = match p.value { Value::Vec3(o) => Value::Vec3([v[0], v[1], o[2]]), _ => Value::Vec2(v), }; } } /// Split a PostScript font name into family and style (`Inter-Bold` → `Inter`, `Bold`). fn font_names(ps: &str) -> (String, String) { match ps.split_once('-') { Some((fam, style)) => (fam.to_string(), style.to_string()), None => (ps.to_string(), "Regular".to_string()), } } fn text_doc(t: &effectcraft_psd::TextInfo) -> TextDoc { let mut d = TextDoc::default(); d.set_text(&t.text); let [xx, xy, yx, yy, _, _] = t.transform; let scale = (xx * yy - xy * yx).abs().sqrt().max(1e-6); if let Some(s) = t.style.size { d.size = s * scale; } if let Some(f) = &t.style.font { let (fam, style) = font_names(f); d.font = fam; d.style = style; } if let Some(c) = t.style.color { d.fill = [c[0] as f32, c[1] as f32, c[2] as f32, 1.0]; } d.justify = match t.style.justification { Some(1) => Justify::Right, Some(2) => Justify::Center, Some(3) => Justify::JustifyLastLeft, Some(4) => Justify::JustifyLastRight, Some(5) => Justify::JustifyLastCenter, Some(6) => Justify::JustifyAll, _ => Justify::Left, }; if let Some(tr) = t.style.tracking { d.tracking = tr; } if let Some(l) = t.style.leading { d.leading = Some(l * scale); } d.faux_bold = t.style.faux_bold; d.faux_italic = t.style.faux_italic; if t.style.box_text && let Some([l, top, r, b]) = t.bounds { d.box_size = Some([(r - l) * scale, (b - top) * scale]); d.box_pos = [l * scale, top * scale]; } d } fn text_layer(proj: &mut Project, comp: &Comp, name: &str, t: &effectcraft_psd::TextInfo) -> Layer { let mut l = build::layer(proj, comp, name, LayerSource::Text, (0, 0), None); if let Some(p) = l.props.prop_mut("text/sourceText") { p.value = Value::Text(Box::new(text_doc(t))); } set_vec2(&mut l, "transform/position", [t.transform[4], t.transform[5]]); let [xx, xy, _, _, _, _] = t.transform; let rot = xy.atan2(xx).to_degrees(); if rot.abs() > 1e-6 { set_scalar(&mut l, "transform/rotation", rot); } l } fn adjustment_layer(proj: &mut Project, comp: &Comp, name: &str) -> Layer { let folder = proj.folder_named("Solids").unwrap_or_else(|| proj.add_item("Solids", Label::Yellow, None, ItemKind::Folder)); let sid = proj.add_item( name, Label::Red, Some(folder), ItemKind::Solid(Solid { color: [1.0, 1.0, 1.0], width: comp.width, height: comp.height, pixel_aspect: 1.0 }), ); let mut l = build::layer(proj, comp, name, LayerSource::Solid { item: sid }, (comp.width, comp.height), None); l.switches.adjustment = true; l.label = Label::Purple; l } fn add_effect(proj: &mut Project, l: &mut Layer, effect: &str, comp: &Comp, params: &[(&str, Value)]) { let Some(spec) = effectcraft_effects::lookup(effect) else { return }; let mut ids = Ids(&mut proj.next_id); let mut g = effectcraft_effects::instantiate(spec, &mut ids, spec.name, [comp.width as f64, comp.height as f64]); for (k, v) in params { // Paths reach parameters in twirl-down groups (`master/exposure`). if let Some(p) = g.prop_mut(k) { p.value = v.clone(); } } if let Some(fx) = l.props.sub_mut("effects") { fx.children.push(g.into()); } } fn add_adjustment_effect(proj: &mut Project, l: &mut Layer, adj: &Adjustment, comp: &Comp, warnings: &mut Vec, name: &str) { match adj { Adjustment::Invert => add_effect(proj, l, "Invert", comp, &[]), Adjustment::BrightnessContrast { brightness, contrast, legacy } => add_effect( proj, l, "Brightness & Contrast", comp, &[("brightness", Value::Scalar(*brightness)), ("contrast", Value::Scalar(*contrast)), ("useLegacy", Value::Bool(*legacy))], ), Adjustment::HueSaturation { hue, saturation, lightness, colorize } => { let mut p = vec![("hue", Value::Scalar(*hue)), ("saturation", Value::Scalar(*saturation)), ("lightness", Value::Scalar(*lightness))]; if *colorize { p = vec![ ("colorize", Value::Bool(true)), ("colorizeHue", Value::Scalar(*hue)), ("colorizeSaturation", Value::Scalar(*saturation)), ("colorizeLightness", Value::Scalar(*lightness)), ]; } add_effect(proj, l, "Hue/Saturation", comp, &p) } Adjustment::Levels { in_black, in_white, gamma, out_black, out_white } => add_effect( proj, l, "Levels", comp, &[ ("inBlack", Value::Scalar(in_black / 255.0)), ("inWhite", Value::Scalar(in_white / 255.0)), ("gamma", Value::Scalar(*gamma)), ("outBlack", Value::Scalar(out_black / 255.0)), ("outWhite", Value::Scalar(out_white / 255.0)), ], ), Adjustment::Exposure { exposure, offset, gamma } => add_effect( proj, l, "Exposure", comp, &[("master/exposure", Value::Scalar(*exposure)), ("master/offset", Value::Scalar(*offset)), ("master/gamma", Value::Scalar(*gamma))], ), Adjustment::Other(k) => warnings.push(format!("{name}: adjustment `{k}` is not converted (layer kept as an empty adjustment layer)")), } } // ---------------------------------------------------------------- layer effects → styles fn style_mode(m: BlendMode) -> Value { Value::Enum(STYLE_BLEND_MODES.iter().position(|x| x.1 == m).unwrap_or(0) as u32) } fn set(g: &mut PropGroup, k: &str, v: Value) { if let Some(p) = g.get_mut(k) { p.value = v; } } fn color4(c: [f64; 3]) -> Value { Value::Color([c[0], c[1], c[2], 1.0]) } /// Copy the common style settings (`Md `, `Clr `, `Opct`, angle, distance, size…). fn common(g: &mut PropGroup, d: &Descriptor) { if let Some(m) = desc_blend(d.enum_value("Md ")) { set(g, "blendMode", style_mode(m)); } if let Some(c) = d.color("Clr ") { set(g, "color", color4(c)); } for (k, prop) in [("Opct", "opacity"), ("Nose", "noise"), ("Dstn", "distance"), ("blur", "size"), ("lagl", "angle"), ("ShdN", "jitter"), ("Inpr", "range")] { if let Some(v) = d.num(k) { set(g, prop, Value::Scalar(v)); } } if let Some(b) = d.bool("uglg") { set(g, "useGlobalLight", Value::Bool(b)); } } fn gradient_of(d: &Descriptor) -> Option { let g = d.obj("Grad")?; let list = |k: &str| match g.get(k) { Some(DValue::List(v)) => v.clone(), _ => vec![], }; let colors: Vec<(f64, [f32; 4])> = list("Clrs") .iter() .filter_map(|c| match c { DValue::Descriptor(s) => { let col = s.color("Clr ")?; Some((s.num("Lctn").unwrap_or(0.0) / 4096.0, [col[0] as f32, col[1] as f32, col[2] as f32, 1.0])) } _ => None, }) .collect(); let opacities: Vec<(f64, f32)> = list("Trns") .iter() .filter_map(|c| match c { DValue::Descriptor(s) => Some((s.num("Lctn").unwrap_or(0.0) / 4096.0, (s.num("Opct").unwrap_or(100.0) / 100.0) as f32)), _ => None, }) .collect(); (!colors.is_empty()).then(|| Gradient { colors, opacities: if opacities.is_empty() { vec![(0.0, 1.0), (1.0, 1.0)] } else { opacities } }) } /// The effect descriptor for `key`, or the first enabled one of its `…Multi` list. fn effect<'a>(fx: &'a Descriptor, key: &str, multi: &str) -> Option<&'a Descriptor> { fx.obj(key).or_else(|| match fx.get(multi) { Some(DValue::List(v)) => v.iter().find_map(|x| match x { DValue::Descriptor(d) if d.bool("enab") != Some(false) => Some(d), _ => None, }), _ => None, }) } fn apply_effects(proj: &mut Project, layer: &mut Layer, fx: &Descriptor, gl: &styles::GlobalLight) { const MAP: &[(&str, &str, &str)] = &[ ("DrSh", "dropShadowMulti", "dropShadow"), ("IrSh", "innerShadowMulti", "innerShadow"), ("OrGl", "outerGlowMulti", "outerGlow"), ("IrGl", "innerGlowMulti", "innerGlow"), ("ebbl", "bevelEmbossMulti", "bevelEmboss"), ("ChFX", "satinMulti", "satin"), ("SoFi", "solidFillMulti", "colorOverlay"), ("GrFl", "gradientFillMulti", "gradientOverlay"), ("FrFX", "frameFXMulti", "stroke"), ]; for (key, multi, id) in MAP { let Some(d) = effect(fx, key, multi) else { continue }; let enabled = d.bool("enab").unwrap_or(true); let mut ids = Ids(&mut proj.next_id); let Some(uid) = styles::add_style(layer, &mut ids, id, gl, enabled) else { continue }; let Some(g) = layer.props.find_group_mut(uid) else { continue }; common(g, d); match *id { "dropShadow" => { if let Some(v) = d.num("Ckmt") { set(g, "spread", Value::Scalar(v)); } if let Some(b) = d.bool("layerConceals") { set(g, "knocksOut", Value::Bool(b)); } } "innerShadow" | "innerGlow" => { if let Some(v) = d.num("Ckmt") { set(g, "choke", Value::Scalar(v)); } if d.enum_value("glwS") == Some("SrcC") { set(g, "source", Value::Enum(0)); } } "outerGlow" => { if let Some(v) = d.num("Ckmt") { set(g, "spread", Value::Scalar(v)); } } "bevelEmboss" => { let style = match d.enum_value("bvlS") { Some("OtrB") => 0, Some("Embs") => 2, Some("PlEb") => 3, Some("strokeEmboss") => 4, _ => 1, }; set(g, "style", Value::Enum(style)); let tech = match d.enum_value("bvlT") { Some("PrBL") => 1, Some("Slmt") => 2, _ => 0, }; set(g, "technique", Value::Enum(tech)); if let Some(v) = d.num("srgR") { set(g, "depth", Value::Scalar(v)); } set(g, "direction", Value::Enum(u32::from(d.enum_value("bvlD") == Some("Out ")))); if let Some(v) = d.num("Sftn") { set(g, "soften", Value::Scalar(v)); } if let Some(v) = d.num("Lald") { set(g, "altitude", Value::Scalar(v)); } for (k, p) in [("hglM", "highlightMode"), ("sdwM", "shadowMode")] { if let Some(m) = desc_blend(d.enum_value(k)) { set(g, p, style_mode(m)); } } for (k, p) in [("hglC", "highlightColor"), ("sdwC", "shadowColor")] { if let Some(c) = d.color(k) { set(g, p, color4(c)); } } for (k, p) in [("hglO", "highlightOpacity"), ("sdwO", "shadowOpacity")] { if let Some(v) = d.num(k) { set(g, p, Value::Scalar(v)); } } } "satin" => { if let Some(b) = d.bool("Invr") { set(g, "invert", Value::Bool(b)); } } "gradientOverlay" => { if let Some(gr) = gradient_of(d) { set(g, "colors", Value::Gradient(gr)); } if let Some(a) = d.num("Angl") { set(g, "angle", Value::Scalar(a)); } let st = match d.enum_value("Type") { Some("Rdl ") => 1, Some("Angl") => 2, Some("Rflc") => 3, Some("Dmnd") => 4, _ => 0, }; set(g, "style", Value::Enum(st)); if let Some(b) = d.bool("Rvrs") { set(g, "reverse", Value::Bool(b)); } if let Some(b) = d.bool("Algn") { set(g, "alignWithLayer", Value::Bool(b)); } if let Some(v) = d.num("Scl ") { set(g, "scale", Value::Scalar(v)); } } "stroke" => { if let Some(v) = d.num("Sz ") { set(g, "size", Value::Scalar(v)); } let pos = match d.enum_value("Styl") { Some("InsF") => 1, Some("CtrF") => 2, _ => 0, }; set(g, "position", Value::Enum(pos)); } _ => {} } } } /// Layer ▸ Create ▸ Convert to Editable Text: a footage layer showing a Photoshop type layer /// becomes a text layer (`None` when the layer's source has no text data). pub fn editable_text(proj: &mut Project, comp: &Comp, layer: &Layer, psd: &Psd, index: usize) -> Option { let t = psd.layers.get(index)?.text.as_ref()?; let mut l = text_layer(proj, comp, &layer.name, t); l.name = layer.name.clone(); l.blend_mode = layer.blend_mode; l.switches.video = layer.switches.video; l.in_point = layer.in_point; l.out_point = layer.out_point; l.start_time = layer.start_time; if let (Some(o), Some(p)) = (layer.props.prop("transform/opacity"), l.props.prop_mut("transform/opacity")) { *p = effectcraft_project::Property { uid: p.uid, ..o.clone() }; } Some(l) } #[cfg(test)] mod tests { use super::*; #[test] fn blend_keys() { assert_eq!(blend_mode("mul "), BlendMode::Multiply); assert_eq!(blend_mode("smud"), BlendMode::Exclusion); assert_eq!(blend_mode("pass"), BlendMode::Normal); assert_eq!(desc_blend(Some("linearDodge")), Some(BlendMode::LinearDodge)); assert_eq!(font_names("Inter-Bold"), ("Inter".into(), "Bold".into())); } }