diff --git a/crates/aep/src/container/effects.rs b/crates/aep/src/container/effects.rs index 29940a712..8fe084f0f 100644 --- a/crates/aep/src/container/effects.rs +++ b/crates/aep/src/container/effects.rs @@ -382,17 +382,18 @@ impl<'f, 'a> EffectParam<'f, 'a> { self.recorded_value() } - /// The value in the value record whatever the record's state (`tdsb`): what - /// [`EffectParam::stored_value`] reads, also from a record in the default state (`tdsb` 3). - /// 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 or without a record. + /// 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 p.is_animated() { + if !matches!(p.flags(), Some(1 | 3)) || p.is_animated() { return None; } let v = p.static_value()?; diff --git a/crates/aep/src/import/effects.rs b/crates/aep/src/import/effects.rs index 5ab0c1514..d2924f2bd 100644 --- a/crates/aep/src/import/effects.rs +++ b/crates/aep/src/import/effects.rs @@ -133,6 +133,8 @@ struct ParamOut<'a> { col: &'a mut Collector, } +/// Imports one parameter of `fx` into its EffectCraft property: its keyframes, the value in its +/// record or the `pard` default, with a loss for what cannot be read or mapped. fn import_param(fx: &AepEffect<'_, '_>, map: &EffectMap, p: &EffectParam<'_, '_>, cx: &LayerCtx<'_>, shown: &str, out: ParamOut<'_>) { let ParamOut { group, refs, col } = out; if carries_nothing(p) { diff --git a/crates/aep/tests/it/import/synthetic.rs b/crates/aep/tests/it/import/synthetic.rs index af066ab7c..18709f9e5 100644 --- a/crates/aep/tests/it/import/synthetic.rs +++ b/crates/aep/tests/it/import/synthetic.rs @@ -339,6 +339,7 @@ fn effect_values(p: &Project) -> Vec<(String, effectcraft_keyframe::Value)> { out } +/// A default-state record (`tdsb` 3) reads like a written one, except through `stored_value`. #[test] fn a_default_state_effect_record_holds_the_value() { // After Effects writes the values of some parameters as records in the default state (`tdsb` @@ -368,6 +369,7 @@ fn a_default_state_effect_record_holds_the_value() { assert_eq!(seen, 1); } +/// Default-state records import with their values and no loss, like written records. #[test] fn default_state_effect_records_import_like_written_ones() { // Before, such a record was reported as an unreadable value and the default used. @@ -383,4 +385,21 @@ fn default_state_effect_records_import_like_written_ones() { let (p, _) = run(&with_record("ADBE Glo2-0011", 3, 0.75), true); let mid: Vec<_> = effect_values(&p).into_iter().filter(|(id, _)| id == "abMidpoint").collect(); assert_eq!(mid, vec![("abMidpoint".to_string(), effectcraft_keyframe::Value::Scalar(75.0))]); + // Levels' Clip To Output Black (`-0008`) recorded as After Effects' item 1, On: EffectCraft's + // On is item 1 of Off, On, Off for 32 bpc Color (the default is item 2). + let (p, _) = run(&with_record("ADBE Easy Levels2-0008", 3, 1.0), true); + let clip: Vec<_> = effect_values(&p).into_iter().filter(|(id, _)| id == "clipToOutputBlack").collect(); + assert_eq!(clip, vec![("clipToOutputBlack".to_string(), effectcraft_keyframe::Value::Enum(1))]); +} + +/// A record in a state other than 1 or 3 is not read and is reported. +#[test] +fn an_effect_record_in_another_state_is_reported_as_damaged() { + // Only the written (1) and the default (3) state hold a value; any other state is not read. + for tdsb in [0, 2, 5] { + let (_, r) = run(&with_record("ADBE Glo2-0011", tdsb, 0.75), true); + let r = r.unwrap(); + let damaged = losses(&r, LossCategory::DamagedData); + assert!(damaged.iter().any(|l| l.contains("unreadable value")), "tdsb {tdsb}: {:?}", r.report.losses); + } } diff --git a/docs/aep-format.md b/docs/aep-format.md index 5897453c6..08737bc8e 100644 --- a/docs/aep-format.md +++ b/docs/aep-format.md @@ -437,7 +437,7 @@ The expression engine is project-wide (`LIST ExEn/Utf8`, `javascript-1.0`). API: | **float slider valid minimum, slider range** | `pard` | 104; 112 / 116 | f32; f32 x 2 | the smallest valid value (Exposure -100, Offset -2, Gamma Correction 0.1 as f32) and the range the slider covers (Exposure -4..4, Offset -0.5..0.5, Gamma 0.1..10); the slider range is repeated as `tdum` / `tduM` (f64) in each value record (391-394). @120 holds the default again as f32 (Gamma 1.0, Blurriness 25), u16 @124 the display decimals (**H**: Exposure 2, Offset 4) | 389, 391-394 | V | | **definitions of a second instance** | top-level `LIST EfdG` | | | one `EfDf` per effect TYPE (`tdmn` = match name, then a copy of ONE instance's `sspc`, including that instance's value records); the `EfDC` count before them is a little-endian u32. When two layers carry the same effect type, only one instance keeps a full `parT`; **the other has an empty `LIST parT` (4 bytes)** but its own `tdgp` (input layer `-0000` with its own `tdpi`, the button record, and its value records). Its definitions come from the `EfdG` copy by match name, its values from its own records, never from the copy (383: the copy holds S100's Start [0.1, 0.2]; L's Ramp reads the 368 defaults). Which instance keeps the full `parT` is **?** (in 383 S100 is both first in the file and newest). The `EfDf` order follows the stack order (365 / 366); `Pefl/pjef` does not | 365, 366, 383 | V | | popup items | `parT/pdnm/Utf8` | | text | items separated by `\|` | 077 (`Item 1\|Item 2\|Item 3`), 214 | V | -| stored value | `sspc/tdgp/-NNNN` | | `tdbs` | present only for a parameter that differs from its default; `tdsb` 1, `cdat` as in 3.4. **Absent record = the `pard` default.** Some parameters are recorded in the default state (`tdsb` 3) and the record still holds the value After Effects uses: every one of the 25 such records in the corpus and the samples differs from its `pard` default and reads back as recorded (Lumetri Color's hidden controls High Dynamic Range, HDR Specular, the second Look, Legacy Tone And Saturation, Face Detection, Auto Color, Auto Intensity and SemanticAutoTone Progress, 1200; Cryptomatte's Selection Mode on, 1400). No effect record in the default state holds a value that does not count. The reader takes the value whatever the state (`EffectParam::recorded_value`, `EffectParam::value`); `EffectParam::stored_value` keeps to written records. That other effects record values so is H (no sample shows one) | see below; 1200, 1400 | V (1200, 1400), H (other effects) | +| stored value | `sspc/tdgp/-NNNN` | | `tdbs` | present only for a parameter that differs from its default; `tdsb` 1, `cdat` as in 3.4. **Absent record = the `pard` default.** Some parameters are recorded in the default state (`tdsb` 3) and the record still holds the value After Effects uses: every one of the 25 such records in the corpus and the samples differs from its `pard` default and reads back as recorded (Lumetri Color's hidden controls High Dynamic Range, HDR Specular, the second Look, Legacy Tone And Saturation, Face Detection, Auto Color, Auto Intensity and SemanticAutoTone Progress, 1200; Cryptomatte's Selection Mode on, 1400). No effect record in the default state holds a value that does not count. The reader takes the value in either state (`EffectParam::recorded_value`, `EffectParam::value`); `EffectParam::stored_value` keeps to written records, and a record in any other state (no sample has one) is not read: the importer reports it as damaged data. That other effects record values so is H (no sample shows one) | see below; 1200, 1400 | V (1200, 1400), H (other effects) | Stored values by type (`tdsb` 1, or 3 as above):