Files
effectcraft/crates/aep/src/container/effects.rs
T
vangel76andClaude Opus 5.5 e25025545a AEP import: an effect record is read only in the written or the default state
A record in another tdsb state is reported as damaged data again, as before the
default-state change. The synthetic test checks Levels' recorded popup directly.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 19:28:55 +01:00

676 lines
31 KiB
Rust

//! 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`.
use std::collections::{HashMap, HashSet};
use crate::container::Chunk;
use crate::container::bytes::{array, be_f32, be_f64, be_i32, be_u16, be_u32, cstr, text, until_nul};
use crate::container::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,
/// Neither 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 (`<effect 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<Vec<String>>,
/// True when the definition is the project-level copy (see [`DefinitionSource::Project`]).
pub borrowed_definition: bool,
pard: &'f [u8],
stored: Option<Property<'f, 'a>>,
}
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<String> {
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<String> {
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<bool> {
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<String> {
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<u32> {
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 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<EffectParam<'f, 'a>> {
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<EffectParam<'f, 'a>> {
let mut out: Vec<EffectParam<'f, 'a>> = 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<String, Option<Property<'f, 'a>>> = 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<String> = 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()
{
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<String> = HashSet::new();
for df in defs.children().iter().filter(|c| c.is_list(b"EfDf")) {
let mut name: Option<String> = 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).
pub fn effect_definition_index(&self) -> EffectDefinitions<'f, 'a> {
EffectDefinitions { by_name: self.effect_definitions().into_iter().collect() }
}
/// 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<EffectParam<'f, 'a>> {
self.effect_definition_index().params(effect)
}
}
/// The project-level effect definitions by match name ([`Project::effect_definition_index`]).
#[derive(Debug, Clone, Default)]
pub struct EffectDefinitions<'f, 'a> {
by_name: HashMap<String, &'f Chunk<'a>>,
}
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 {
DefinitionSource::Missing
}
}
/// The parameters of `effect`: from its own `parT`, or, when that is empty, from the
/// project-level definition of its match name ([`EffectParam::borrowed_definition`]). Values
/// always come from the instance's own records (absent = the `pard` default); the project
/// copy's values belong to another instance and are never read.
pub fn params(&self, effect: &Effect<'f, 'a>) -> Vec<EffectParam<'f, 'a>> {
if effect.has_own_definitions() {
return effect.params();
}
match self.by_name.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 (`<match>-NNNN` in the instance's `tdgp`), which
/// exists only for parameters that differ from the default.
pub fn stored(&self) -> Option<Property<'f, 'a>> {
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<KeyList<'f>> {
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<ParamValue> {
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<ParamValue> {
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<ParamValue> {
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<bool> {
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<LayerRef> {
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<f64> {
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<f64> {
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<u16> {
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<u32> {
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<ParamValue> {
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<Effect<'f, 'a>> {
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<String> = ["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::<Vec<_>>(), ["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<String> = (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::<Vec<_>>(), ["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);
}
}