//! Effect instances: match name, instance name, the parameter list from `parT`/`pard` and the //! values stored with the instance. //! //! An effect on a layer is a `LIST sspc` under `ADBE Effect Parade`. It holds `fnam` (the //! instance name), `LIST parT` (the parameter definitions: `parn`, then a `tdmn` and a 148-byte //! `pard` per parameter, a `pdnm` after a popup), `LIST tdgp` (the values) and `pgui`. Only //! parameters that differ from their `pard` default have a value record in the `tdgp` (the Layer //! Control and button records are the exceptions: they always exist). //! //! When several layers carry an effect of the same type, only one instance has a full `parT`; the //! others have an empty `LIST parT` (case 383). Their parameter definitions are the ones in the //! project-level `LIST EfdG` (one `EfDf` per effect type: a `tdmn` with the match name, then a copy //! of one instance's `sspc`), see [`Project::effect_params`]. That copy also carries the values of //! the instance it was copied from, so values are only ever read from the instance's own `tdgp`. //! //! Some files have no `EfdG` at all (seen in 17 projects, all from inside After Effects-made //! Motion Graphics templates). In those, one instance per effect type kept its full `parT` (which //! one is not known) and it is the only place the definitions are written, so the other instances //! of that type borrow them from it (#3170). use std::collections::{HashMap, HashSet}; use crate::Chunk; use crate::bytes::{array, be_f32, be_f64, be_i32, be_u16, be_u32, cstr, text, until_nul}; use crate::typed::{KeyList, Layer, Project, PropGroup, PropNode, Property}; /// The parameter type, the `u32` at byte 12 of `pard`. The numbering matches the plug-in /// parameter types of the After Effects effect interface. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum ParamKind { /// `0`: a layer reference (also the implicit first parameter, the input layer). Layer, /// `1`: an integer slider (Fast Box Blur's Iterations, 412-414): the default is an `i32` at /// `pard` byte 56 (3), the valid maximum an `i32` at byte 128 (50), the value record a plain /// `f64` (1.0). IntSlider, /// `2`: a fixed-point (16.16) slider. FixedSlider, /// `3`: an angle (16.16 degrees). Angle, /// `4` Checkbox, /// `5` Color, /// `6` Point, /// `7`: a popup menu (the items are in the `pdnm` chunk). Popup, /// `9`: no data (the group-end entry `ADBE Effect Built In Params`). NoData, /// `10`: a floating-point slider. FloatSlider, /// `11`: arbitrary data (Hue/Saturation's Channel Range, Levels' Histogram, 443, 511): the /// value record is a 4-byte `cdat`, the data itself the `aRbp` of the `LIST aRbs` after it, /// see [`Effect::arbitrary_data`]. ArbitraryData, /// `12`: a path / mask reference. Path, /// `13`: the start of a twirl-down group (Exposure's Master, Red, Green and Blue, 389). It /// holds no value; its value record always exists with `cdat` 0. GroupStart, /// `14`: the end of a twirl-down group (its name repeats the group's name, 389). It holds no /// value; its value record always exists with `cdat` 0. GroupEnd, /// `15`: a button (Tint's and Gradient Ramp's Swap Colors). It holds no value; its value /// record always exists with `cdat` 0 (356, 368). Button, /// Any other value. Other(u32), } impl ParamKind { fn from_u32(v: u32) -> Self { match v { 0 => Self::Layer, 1 => Self::IntSlider, 2 => Self::FixedSlider, 3 => Self::Angle, 4 => Self::Checkbox, 5 => Self::Color, 6 => Self::Point, 7 => Self::Popup, 9 => Self::NoData, 10 => Self::FloatSlider, 11 => Self::ArbitraryData, 12 => Self::Path, 13 => Self::GroupStart, 14 => Self::GroupEnd, 15 => Self::Button, other => Self::Other(other), } } } /// A decoded parameter value. #[derive(Debug, Clone, Copy, PartialEq)] pub enum ParamValue { /// A slider or angle value as stored: float sliders, fixed sliders and angles keep a plain /// `f64` in their value record (an angle in degrees; a fixed slider in its own scale, see /// [`EffectParam::valid_max`]); defaults read from `pard` are 16.16 decoded for fixed sliders /// and angles. Number(f64), /// A checkbox: stored as 1.0 when on; an absent record means off. Bool(bool), /// A point control as stored: a fraction of the layer size (`[111, 222]` px on a 1920x1080 /// layer is `[111/1920, 222/1080]`). Point([f64; 2]), /// A layer control: the id of the chosen layer (the `tdpi` chunk next to the value record). /// In the implicit first parameter it is the id of the layer that owns the effect. Layer(u32), /// A colour as ARGB fractions 0..1 (stored as doubles 0..255 in the order alpha, red, green, /// blue: a colour `[0.2, 0.6, 1]` is `[255, 51, 153, 255]`, with float rounding). Argb([f64; 4]), /// A popup's item index, 1-based (Gradient Ramp's Radial Ramp is 2, 377): the default in /// `pard`, or the stored selection. Index(u32), } /// What a Layer Control points at, given the id of the layer that owns the effect. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum LayerRef { /// The layer itself (the default, "Layer 1" on the first layer): the stored `tdpi` equals the /// owning layer's id. This, /// None: the stored `tdpi` is 0. Nothing, /// Another layer, by id. Layer(u32), } /// An effect instance on a layer. #[derive(Debug, Clone)] pub struct Effect<'f, 'a> { match_name: String, chunk: &'f Chunk<'a>, } /// Where the parameter definitions of an effect instance come from. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum DefinitionSource { /// The instance's own `parT`. Instance, /// The project-level `EfdG/EfDf` copy for the effect's match name (the instance's own `parT` /// is empty, 383). Project, /// The own `parT` of another instance of the same match name in the file: the instance's /// own `parT` is empty and the file has no project-level copy of the type (a project without /// `EfdG`, #3170). OtherInstance, /// None of these has any: the effect has no readable parameter definitions. Missing, } /// One parameter of an effect. #[derive(Debug, Clone)] pub struct EffectParam<'f, 'a> { /// The position in `parT` (0 is the implicit input layer). pub index: usize, /// The `tdmn` match name (`-NNNN`, the last one is /// `ADBE Effect Built In Params`). pub match_name: String, pub kind: ParamKind, /// The parameter name (`Blurriness`, `Slider`, ...; empty for the implicit parameters). pub name: String, /// The popup items (`pdnm`, split at `|`; for a button its label, e.g. `Swap Colors`). pub items: Option>, /// True when the definition is borrowed: the project-level copy ([`DefinitionSource::Project`]) /// or another instance's ([`DefinitionSource::OtherInstance`]). pub borrowed_definition: bool, pard: &'f [u8], stored: Option>, } impl<'f, 'a> Effect<'f, 'a> { /// The effect's match name (`ADBE Slider Control`, `ADBE Gaussian Blur 2`, ...). pub fn match_name(&self) -> &str { &self.match_name } /// The underlying `LIST sspc` chunk. pub fn chunk(&self) -> &'f Chunk<'a> { self.chunk } /// The instance name shown in the effect controls (`fnam`). pub fn name(&self) -> Option { self.chunk.child(b"fnam")?.children().iter().find(|c| c.is(b"Utf8"))?.bytes().map(text) } /// The name the user gave the instance: the `tdsn` of the instance's value group when it is /// not the placeholder `-_0_/-` (610: `My Tint`; `fnam` keeps the effect's own name `Tint`). /// `None` while the instance shows the effect's name. pub fn user_name(&self) -> Option { let values = self.chunk.child_list(b"tdgp")?; let name = values.child(b"tdsn")?.children().iter().find(|c| c.is(b"Utf8"))?.bytes().map(text)?; (name != "-_0_/-" && !name.is_empty()).then_some(name) } /// The instance's fx switch: the `tdsb` of its value group is 1 when on and 0 when switched /// off (`fx-switch` cases: one instance on, one off, both in one save). `None` for another /// value or a missing group. pub fn enabled(&self) -> Option { match be_u32(self.chunk.child_list(b"tdgp")?.child(b"tdsb")?.bytes()?, 0)? { 1 => Some(true), 0 => Some(false), _ => None, } } /// The name After Effects shows for the instance: [`Effect::user_name`], else [`Effect::name`]. pub fn display_name(&self) -> Option { self.user_name().or_else(|| self.name()) } /// The number of parameters declared by `parn` (the implicit input layer and the built-in /// entry included). pub fn declared_param_count(&self) -> Option { be_u32(self.chunk.child_list(b"parT")?.child(b"parn")?.bytes()?, 0) } /// True when the instance's own `parT` holds parameter definitions (a `pard`). A second /// instance of an effect type has an empty `parT` (383). pub fn has_own_definitions(&self) -> bool { self.chunk.child_list(b"parT").is_some_and(has_pard) } /// The match names of the value records in the instance's own `tdgp` (properties only, not /// groups), in file order, each once. pub fn value_record_names(&self) -> Vec { let mut seen: HashSet = HashSet::new(); self.chunk .child_list(b"tdgp") .and_then(PropGroup::from_chunk) .map(|g| g.entries()) .unwrap_or_default() .into_iter() .filter(|e| matches!(e.node, PropNode::Property(_))) .map(|e| e.match_name) .filter(|n| seen.insert(n.clone())) .collect() } /// The parameters in the order of the instance's own `parT`. Empty for an instance whose /// `parT` is empty; [`Project::effect_params`] falls back to the project's definitions. pub fn params(&self) -> Vec> { match self.chunk.child_list(b"parT") { Some(part) => self.params_from(part, false), None => Vec::new(), } } /// The parameters described by `part` (a `LIST parT`) with the values of this instance. fn params_from(&self, part: &'f Chunk<'a>, borrowed: bool) -> Vec> { let mut out: Vec> = Vec::new(); // The stored values by match name: the first entry of a name, as `PropGroup::get` finds it // (a group of that name means no stored value). Indexed once: a lookup per parameter // would scan the whole group for each of them. let mut values: HashMap>> = HashMap::new(); for e in self.chunk.child_list(b"tdgp").and_then(PropGroup::from_chunk).map(|g| g.entries()).unwrap_or_default() { let stored = if let PropNode::Property(p) = e.node { Some(p) } else { None }; values.entry(e.match_name).or_insert(stored); } let mut pending: Option = None; for c in part.children() { if c.is(b"tdmn") { pending = c.bytes().map(|b| text(until_nul(b))); } else if c.is(b"pard") { let Some(pard) = c.bytes() else { continue }; let match_name = pending.take().unwrap_or_default(); let stored = values.get(&match_name).copied().flatten(); out.push(EffectParam { index: out.len(), match_name, kind: ParamKind::from_u32(be_u32(pard, 12).unwrap_or(u32::MAX)), name: cstr(pard, 16, 32).unwrap_or_default(), items: None, borrowed_definition: borrowed, pard, stored, }); } else if c.is(b"pdnm") && let Some(last) = out.last_mut() && let Some(u) = c.children().iter().find(|u| u.is(b"Utf8")) && let Some(b) = u.bytes() // An empty `pdnm` names nothing (some writers store one: the effect // supplies its items). && !b.is_empty() { last.items = Some(text(b).split('|').map(str::to_string).collect()); } } out } } fn has_pard(part: &Chunk<'_>) -> bool { part.children().iter().any(|c| c.is(b"pard")) } impl<'f, 'a> Project<'f, 'a> { /// The project-level effect definitions (`LIST EfdG`): one entry per effect type, the match /// name and the `LIST parT` of the copied instance, in file order (the stack order of the /// types, 365/366). The `EfDC` count before them is little-endian and is not needed to read /// them. A type whose copy has no definitions is left out. pub fn effect_definitions(&self) -> Vec<(String, &'f Chunk<'a>)> { let mut out: Vec<(String, &'f Chunk<'a>)> = Vec::new(); let Some(defs) = self.effect_defs else { return out }; let mut seen: HashSet = HashSet::new(); for df in defs.children().iter().filter(|c| c.is_list(b"EfDf")) { let mut name: Option = None; for c in df.children() { if c.is(b"tdmn") { name = c.bytes().map(|b| text(until_nul(b))); } else if c.is_list(b"sspc") && let Some(n) = name.take() && let Some(part) = c.child_list(b"parT").filter(|p| has_pard(p)) && seen.insert(n.clone()) { out.push((n, part)); } } } out } /// [`Self::effect_definitions`] indexed by match name, for looking up many effects: build it /// once per file, not once per effect (a file can hold many definitions and many instances). /// For a type without a project-level copy it also keeps the own `parT` of the first instance /// in the file (items, layers and effects in file order) that has one. pub fn effect_definition_index(&self) -> EffectDefinitions<'f, 'a> { let by_name: HashMap> = self.effect_definitions().into_iter().collect(); let mut instances: HashMap> = HashMap::new(); for item in self.all_items() { let Some(comp) = item.comp() else { continue }; for layer in comp.layers() { for fx in layer.effects() { if by_name.contains_key(&fx.match_name) || instances.contains_key(&fx.match_name) { continue; } if let Some(part) = fx.chunk.child_list(b"parT").filter(|p| has_pard(p)) { instances.insert(fx.match_name, part); } } } } EffectDefinitions { by_name, instances } } /// Where the parameter definitions of `effect` come from. Builds the index each call: use /// [`Self::effect_definition_index`] for more than one effect. pub fn definition_source(&self, effect: &Effect<'f, 'a>) -> DefinitionSource { self.effect_definition_index().source(effect) } /// The parameters of `effect` ([`EffectDefinitions::params`]). Builds the index each call: /// use [`Self::effect_definition_index`] for more than one effect. pub fn effect_params(&self, effect: &Effect<'f, 'a>) -> Vec> { self.effect_definition_index().params(effect) } } /// The borrowable effect definitions by match name ([`Project::effect_definition_index`]). #[derive(Debug, Clone, Default)] pub struct EffectDefinitions<'f, 'a> { /// The project-level copies (`EfdG`). by_name: HashMap>, /// For a type without a project-level copy: the own `parT` of the first instance that has one. instances: HashMap>, } impl<'f, 'a> EffectDefinitions<'f, 'a> { /// Where the parameter definitions of `effect` come from. pub fn source(&self, effect: &Effect<'f, 'a>) -> DefinitionSource { if effect.has_own_definitions() { DefinitionSource::Instance } else if self.by_name.contains_key(effect.match_name()) { DefinitionSource::Project } else if self.instances.contains_key(effect.match_name()) { DefinitionSource::OtherInstance } else { DefinitionSource::Missing } } /// The parameters of `effect`: from its own `parT`, or, when that is empty, from the /// project-level definition of its match name, else from another instance of it that has /// its own ([`EffectParam::borrowed_definition`]). Values always come from the instance's own /// records (absent = the `pard` default); the values next to borrowed definitions belong to /// another instance and are never read. pub fn params(&self, effect: &Effect<'f, 'a>) -> Vec> { if effect.has_own_definitions() { return effect.params(); } match self.by_name.get(effect.match_name()).or_else(|| self.instances.get(effect.match_name())) { Some(part) => effect.params_from(part, true), None => Vec::new(), } } } impl<'f, 'a> EffectParam<'f, 'a> { /// The raw 148-byte `pard` record (it contains stale memory after the name). pub fn pard(&self) -> &'f [u8] { self.pard } /// The value record stored with the instance (`-NNNN` in the instance's `tdgp`), which /// exists only for parameters that differ from the default. pub fn stored(&self) -> Option> { self.stored } /// True when the parameter's value record has keyframes ([`EffectParam::keys`]); then /// [`EffectParam::stored_value`] is `None` and [`EffectParam::value`] is not the value. pub fn is_animated(&self) -> bool { self.stored.is_some_and(|p| p.is_animated()) } /// The keyframes of an animated parameter: plain 48-byte records for sliders (363), 152-byte /// colour records (364) and 104-byte spatial records for points (382), with the values in the /// stored units of [`EffectParam::stored_value`]. pub fn keys(&self) -> Option> { self.stored?.keys() } /// The value in the value record: float and fixed sliders, angles, checkboxes, points, /// colours, popups and layer controls. A record counts when its `tdsb` is 1 and it has no /// keyframes; a layer control reads the `tdpi` chunk; a button has no value. pub fn stored_value(&self) -> Option { let p = self.stored?; if self.kind != ParamKind::Layer && p.flags() != Some(1) { return None; } self.recorded_value() } /// The value in the value record when the record is written (`tdsb` 1) or in the default /// state (`tdsb` 3): what [`EffectParam::stored_value`] reads, also from a default-state /// record. Lumetri Color writes the values of its hidden parameters so (High Dynamic Range, /// HDR Specular, Look: 1200 `lumetri-01-set-all`, `-02-sec-top`, `-06-sec-light`; After /// Effects reads back the recorded values). `None` with keyframes, without a record or for a /// record in any other state (no sample has one; the importer reports it as damaged). pub fn recorded_value(&self) -> Option { let p = self.stored?; if self.kind == ParamKind::Layer { return be_u32(p.chunk().child(b"tdpi")?.bytes()?, 0).map(ParamValue::Layer); } if !matches!(p.flags(), Some(1 | 3)) || p.is_animated() { return None; } let v = p.static_value()?; match self.kind { ParamKind::FloatSlider | ParamKind::FixedSlider | ParamKind::Angle | ParamKind::IntSlider => v.first().copied().map(ParamValue::Number), ParamKind::Checkbox => v.first().map(|x| ParamValue::Bool(*x != 0.0)), ParamKind::Point => { let [x, y] = *v.as_slice().first_chunk::<2>()?; Some(ParamValue::Point([x, y])) } ParamKind::Popup => { let index = v.first().copied()?; (0.0..=f64::from(u32::MAX)).contains(&index).then(|| ParamValue::Index(index.round() as u32)) } ParamKind::Color => { let [a, r, g, b] = *v.as_slice().first_chunk::<4>()?; Some(ParamValue::Argb([a / 255.0, r / 255.0, g / 255.0, b / 255.0])) } _ => None, } } /// The default stored in `pard` at byte 56: a float slider's `f64`, a fixed slider's or angle's /// 16.16 value, a colour's ARGB bytes, a popup's item index, a point's two 16.16 fractions of /// the layer size (Point Control 0.5, 0.5: the layer centre, also on a 100x100 layer; /// Gradient Ramp Start 0.5, 0 and End 0.5, 1), an integer slider's `i32` (Iterations 3, 412), /// a checkbox's `u32` (1 = on: Repeat Edge Pixels of Fast Box Blur and Gaussian Blur, read back /// on, 412, 417; 0 = off: Shadow Only 420, Bypass Linear Light Conversion 389); other types: /// `None`. /// /// It is the default and not the current value: Gaussian Blur with Blurriness 40 still has 25 /// here and a value record with 40 (After Effects itself reads a fresh Gaussian Blur back as 25). /// Exception: selecting a popup item also writes the selection to this slot. So a popup whose /// definition was borrowed from the project copy ([`EffectParam::borrowed_definition`]) has no /// reliable default here (the slot holds the copied instance's selection): `None`. pub fn pard_default(&self) -> Option { match self.kind { ParamKind::Popup if self.borrowed_definition => None, ParamKind::FloatSlider => be_f64(self.pard, 56).map(ParamValue::Number), ParamKind::FixedSlider | ParamKind::Angle => be_i32(self.pard, 56).map(|v| ParamValue::Number(f64::from(v) / 65536.0)), ParamKind::IntSlider => be_i32(self.pard, 56).map(|v| ParamValue::Number(f64::from(v))), ParamKind::Checkbox => be_u32(self.pard, 56).map(|v| ParamValue::Bool(v != 0)), ParamKind::Popup => be_u32(self.pard, 56).map(ParamValue::Index), ParamKind::Point => { let p = [f64::from(be_i32(self.pard, 56)?) / 65536.0, f64::from(be_i32(self.pard, 60)?) / 65536.0]; // A fresh instance stores fractions of the layer (0.5, 0.5); After Effects' save of // a project it opened stores percent (50, 50: the re-saves of read-back tests e1 and // e2, where Gradient Ramp's Start and End and every other point default came back // as 50 / 0 and 50 / 100). Point // defaults lie inside the layer, so a component beyond 1 tells percent. let percent = p.iter().any(|c| c.abs() > 1.0 + 1e-9); Some(ParamValue::Point(if percent { p.map(|c| c / 100.0) } else { p })) } ParamKind::Color => { let [a, r, g, b] = array::<4>(self.pard, 56)?.map(|c| f64::from(c) / 255.0); Some(ParamValue::Argb([a, r, g, b])) } _ => None, } } /// True when the parameter is at its default: it has no value record (a record exists exactly /// when the value differs from the `pard` default; setting a value back to the default removes /// the record again), or the record equals the default. A record in the default state (`tdsb` /// 3) counts with its value ([`EffectParam::recorded_value`]). `None` when that cannot be /// decided (unknown parameter type, keyframes). pub fn is_default(&self) -> Option { match (self.stored, self.kind) { // A Layer Control always has its record, and a popup selection overwrites the default // in `pard`. Buttons and group markers hold no value at all. (_, ParamKind::Layer | ParamKind::Button | ParamKind::GroupStart | ParamKind::GroupEnd) => None, (None, _) => Some(true), (Some(_), ParamKind::Popup) => Some(false), (Some(_), _) => { let stored = self.recorded_value()?; // A checkbox's default is in `pard` too (on for Repeat Edge Pixels, 412). Some(self.pard_default().is_some_and(|d| d == stored)) } } } /// A point control in pixels of a layer of this size (the stored point is a fraction of the /// layer size; the default is the layer centre). `None` for other parameter types. pub fn point_px(&self, layer_width: f64, layer_height: f64) -> Option<[f64; 2]> { match self.value()? { ParamValue::Point([x, y]) if self.kind == ParamKind::Point => Some([x * layer_width, y * layer_height]), _ => None, } } /// For a Layer Control: what the stored layer id means for the layer that owns the effect. /// `None` for other types and for the implicit first parameter of an effect. pub fn layer_ref(&self, owner_layer_id: u32) -> Option { if self.kind != ParamKind::Layer || self.index == 0 { return None; } match self.stored_value()? { ParamValue::Layer(0) => Some(LayerRef::Nothing), ParamValue::Layer(id) if id == owner_layer_id => Some(LayerRef::This), ParamValue::Layer(id) => Some(LayerRef::Layer(id)), _ => None, } } /// The largest valid value of a slider, from `pard`: a fixed slider's 16.16 at byte 128 (Tint /// Amount to Tint 100, Gradient Ramp Ramp Scatter 512 and Blend With Original 1.0, Drop Shadow /// Opacity 255), a float slider's `f32` at byte 108 (Blurriness 30000, Exposure 100, Exposure's /// Gamma Correction 10). The stored scale of a /// slider differs per parameter (Amount 37.5 % is stored 37.5, Blend 25 % is stored 0.25), so /// this is what tells a reader the scale. `None` for other types. pub fn valid_max(&self) -> Option { match self.kind { ParamKind::FixedSlider => be_i32(self.pard, 128).map(|v| f64::from(v) / 65536.0), ParamKind::FloatSlider => be_f32(self.pard, 108).map(f64::from), ParamKind::IntSlider => be_i32(self.pard, 128).map(f64::from), _ => None, } } /// The smallest valid value of a float slider, the `f32` at byte 104 of `pard` (Exposure /// -100, Offset -2, Gamma Correction 0.1: 389). `None` for other types (a fixed slider's /// minimum is not decoded). pub fn valid_min(&self) -> Option { match self.kind { ParamKind::FloatSlider => be_f32(self.pard, 104).map(f64::from), _ => None, } } /// The range of a float slider's slider (what the control's drag covers, inside the valid /// range): the `f32` pair at bytes 112 and 116 of `pard` (Exposure -4..4, Offset -0.5..0.5, /// Gamma Correction 0.1..10: 389). Each value record repeats it as `tdum` / `tduM`. `None` /// for other types. pub fn slider_range(&self) -> Option<(f64, f64)> { match self.kind { ParamKind::FloatSlider => Some((f64::from(be_f32(self.pard, 112)?), f64::from(be_f32(self.pard, 116)?))), _ => None, } } /// For a popup: the number of items (`u16` at byte 60 of `pard`). pub fn popup_item_count(&self) -> Option { if self.kind != ParamKind::Popup { return None; } be_u16(self.pard, 60) } /// For a popup: the selected item index (1-based): the stored selection, else the default. pub fn popup_selected_index(&self) -> Option { match self.value()? { ParamValue::Index(i) if self.kind == ParamKind::Popup => Some(i), _ => None, } } /// For a popup: the text of the selected item. pub fn popup_selected_item(&self) -> Option<&str> { let index = usize::try_from(self.popup_selected_index()?).ok()?.checked_sub(1)?; self.items.as_ref()?.get(index).map(String::as_str) } /// The effective value: the value in the record when there is one, also in a record in the /// default state (`tdsb` 3, [`EffectParam::recorded_value`]), else the `pard` default (absent = /// default); a checkbox without a record is off. pub fn value(&self) -> Option { self.recorded_value().or_else(|| self.pard_default()).or(match self.kind { ParamKind::Checkbox if self.stored.is_none() => Some(ParamValue::Bool(false)), _ => None, }) } } impl<'f, 'a> Layer<'f, 'a> { /// The effects on the layer, in stack order (`ADBE Effect Parade`). pub fn effects(&self) -> Vec> { let Some(root) = self.properties() else { return Vec::new() }; let Some(PropNode::Group(parade)) = root.get("ADBE Effect Parade") else { return Vec::new() }; parade .entries() .into_iter() .filter_map(|e| match e.node { PropNode::Other(chunk) if chunk.is_list(b"sspc") => Some(Effect { match_name: e.match_name, chunk }), _ => None, }) .collect() } } #[cfg(test)] mod tests { use super::*; fn tdmn(name: &str) -> Chunk<'static> { let mut b = name.as_bytes().to_vec(); b.resize(40, 0); Chunk::data(*b"tdmn", b) } fn pard() -> Chunk<'static> { Chunk::data(*b"pard", vec![0u8; 48]) } fn tdbs() -> Chunk<'static> { Chunk::list(*b"tdbs", Vec::new()) } /// An effect instance whose `parT` names `defs` and whose `tdgp` stores `stored` (in order). fn instance(defs: &[String], stored: &[(String, Chunk<'static>)]) -> Chunk<'static> { let part = defs.iter().flat_map(|n| [tdmn(n), pard()]).collect(); let values = stored.iter().flat_map(|(n, c)| [tdmn(n), c.clone()]).collect(); Chunk::list(*b"sspc", vec![Chunk::list(*b"parT", part), Chunk::list(*b"tdgp", values)]) } #[test] fn a_parameter_takes_the_first_stored_entry_of_its_name() { let names: Vec = ["A", "B", "C"].map(String::from).to_vec(); // `A` is stored twice (the first wins), `B` names a group (no stored value), `C` is absent. let stored = vec![ ("A".to_string(), tdbs()), ("B".to_string(), Chunk::list(*b"tdgp", Vec::new())), ("A".to_string(), Chunk::list(*b"tdbs", vec![Chunk::data(*b"cdat", vec![0u8; 8])])), ]; let chunk = instance(&names, &stored); let fx = Effect { match_name: "X".into(), chunk: &chunk }; let params = fx.params(); assert_eq!(params.iter().map(|p| p.match_name.as_str()).collect::>(), ["A", "B", "C"]); let first = params.first().and_then(|p| p.stored).expect("A stored"); assert!(first.chunk.children().is_empty(), "the first A (the second has a cdat)"); assert!(params.get(1).is_some_and(|p| p.stored.is_none()), "a group of that name is no stored value"); assert!(params.get(2).is_some_and(|p| p.stored.is_none())); } #[test] fn many_parameters_are_read_in_linear_time() { // 100 000 parameters with 100 000 stored values: a lookup per parameter that scans the // group (as before) is 1e10 entry builds; one index is instant. let n = 100_000; let names: Vec = (0..n).map(|i| format!("P{i}")).collect(); let stored: Vec<(String, Chunk<'static>)> = names.iter().map(|m| (m.clone(), tdbs())).collect(); let chunk = instance(&names, &stored); let fx = Effect { match_name: "X".into(), chunk: &chunk }; let params = fx.params(); assert_eq!(params.len(), n); assert!(params.iter().all(|p| p.stored.is_some())); } #[test] fn many_project_definitions_are_indexed_once() { // 50 000 definitions, each twice (the first copy wins), then 1000 instances without // their own definitions looked up through one index. let n = 50_000; let mut defs = Vec::new(); for round in 0..2 { for i in 0..n { let part = Chunk::list(*b"parT", vec![tdmn(&format!("R{round}")), pard()]); defs.push(Chunk::list(*b"EfDf", vec![tdmn(&format!("FX{i}")), Chunk::list(*b"sspc", vec![part])])); } } let efdg = Chunk::list(*b"EfdG", defs); let fold = Chunk::list(*b"Fold", Vec::new()); let project = Project { fold: &fold, effect_defs: Some(&efdg) }; let all = project.effect_definitions(); assert_eq!(all.len(), n); let index = project.effect_definition_index(); let bare = Chunk::list(*b"sspc", vec![Chunk::list(*b"parT", Vec::new())]); for i in (0..n).step_by(50) { let fx = Effect { match_name: format!("FX{i}"), chunk: &bare }; assert_eq!(index.source(&fx), DefinitionSource::Project); let params = index.params(&fx); assert_eq!(params.iter().map(|p| p.match_name.as_str()).collect::>(), ["R0"], "the first copy's definitions"); assert!(params.iter().all(|p| p.borrowed_definition)); } let unknown = Effect { match_name: "nope".into(), chunk: &bare }; assert_eq!(index.source(&unknown), DefinitionSource::Missing); assert!(index.params(&unknown).is_empty()); assert_eq!(project.definition_source(&unknown), DefinitionSource::Missing); } }