From 6a67b4dac1f989d3ae4be3ef01656404b7144f11 Mon Sep 17 00:00:00 2001 From: vangel76 Date: Thu, 5 Nov 2026 17:48:29 +0100 Subject: [PATCH] AEP import: Lumetri Color's master RGB curve, Hue vs Sat and colour wheels After Effects 2026 presets made by hand (1200 lumetri-58 ... -62) decode the data the scripted samples could not set. - RGB curve (-0039): the first of the four curves is the master curve and arrives as curves/rgbCurves/master. A moved red, green or blue curve is reported: no sample tells which slot is which channel. - Hue vs Sat (-0042): y is the offset from neutral, 1 doubling the saturation (in linear light), so EffectCraft's y is 0.5 + y / 2. The other four hue / saturation curves stay reported when moved. - Color Wheels (-0047): four triples of hue (degrees), brightness slider (0.5 neutral) and distance. Shadows, Midtones and Highlights become EffectCraft's wheel colours (HSL of that hue, saturation = distance, lightness 0.5); the brightness sliders and the fourth wheel are reported. - Damaged curve or wheel data is reported as damaged. - Legacy Tone And Saturation is reported when a record says on: its pard default turns from off to on when the project is saved again (the 25.x / 24.x / 26 copies of 1200), so comparing with it missed it. - The Input LUT and Look presets confirm -0095 / -0096 hold the names; the LUT files are After Effects' own and are not embedded. Evidence: the 62 read-backs still equal the imported values in the new 63-composition 1200 file. The version copies store the same Lumetri definitions, ranges and values and give the same report. Renders: master curve within 0.024 (After Effects uses a natural spline, EffectCraft's Lumetri a monotone one), Hue vs Sat mean 0.008, wheels mean 0.042. Part of #3111 Co-Authored-By: Claude Opus 5.5 --- crates/aep/src/import/lumetri.rs | 190 +++++++++++++++++++++++--- crates/aep/tests/it/import/lumetri.rs | 129 +++++++++++++++-- docs/aep-format.md | 52 ++++--- docs/aep-import.md | 26 +++- 4 files changed, 348 insertions(+), 49 deletions(-) diff --git a/crates/aep/src/import/lumetri.rs b/crates/aep/src/import/lumetri.rs index 22f28b1aa..9369155f4 100644 --- a/crates/aep/src/import/lumetri.rs +++ b/crates/aep/src/import/lumetri.rs @@ -16,11 +16,18 @@ //! After Effects; EffectCraft's Lumetri has none. A section that is off does nothing in After //! Effects (1200 `lumetri-03-sec-basic-correction`, `-17-sec-vignette` render as `-00-defaults`), //! so its values are not imported (the effect looks the same) and are listed in the report. -//! - **Arbitrary data** (`ParamKind::ArbitraryData`, every instance stores it): the hue / -//! saturation curves (a curve type, a point count, points as `f64` x, y with y an offset from -//! neutral) are imported when neutral and reported otherwise (no sample moves a point); the -//! HSL Secondary key ([`HslKey`]) is imported; the RGB curves, the colour wheels, Split Toning -//! and the correction wheels are reported when they are not the default (not sampled). +//! - **Arbitrary data** (`ParamKind::ArbitraryData`, every instance stores it). Curves are a +//! curve type, a point count and points as `f64` x, y ([`curve_points`]). +//! - The RGB curve (`-0039`) holds four curves; the first is the master (white) curve (1200 +//! `lumetri-60-preset-curves-rgb`), imported. The other three are reported when moved: no +//! sample tells which is which channel. +//! - Hue vs Sat (`-0042`): y is an offset from neutral, 1 doubling the saturation (`-61`), so +//! EffectCraft's y is `0.5 + y / 2`. The other four hue / saturation curves have no sample +//! of their scale and are reported when moved. +//! - Color Wheels (`-0047`): Shadows, Midtones, Highlights and a fourth wheel, each hue, +//! brightness slider, distance ([`import_wheels`], `-62`). +//! - The HSL Secondary key ([`HslKey`]) is imported; Split Toning and the correction wheels are +//! reported when they are not the default (not sampled). //! - **LUTs and looks.** The Input LUT and the Look are files After Effects names (the Look's name //! is UTF-16 text in `-0096`, the popups only index a list of the installed ones); they are not //! in the project, so a set one is reported by name and EffectCraft's popup stays None. @@ -82,8 +89,9 @@ impl Section { enum Data { /// The four RGB curves (4 x 520 bytes). RgbCurves, - /// One hue / saturation curve (520 bytes); the EffectCraft parameter it belongs to. - HueSatCurve, + /// One hue / saturation curve (520 bytes); the EffectCraft parameter when the scale of its y + /// is known (Hue vs Sat). + HueSatCurve(Option<&'static str>), /// Color Wheels (4 x 3 `f64`, each wheel 0, 0.5, 0 by default). Wheels, /// HSL Secondary's correction wheels (8 x 3 `f64`, the same default). @@ -105,6 +113,11 @@ enum Role { Switch, /// No counterpart: reported when not at the default. Missing, + /// A checkbox without counterpart whose `pard` default is not reliable: reported when a + /// record says on. Legacy Tone And Saturation (`-0097`) is off in a fresh instance, but its + /// `pard` default reads off in 1200 as saved by the script and on after the project was + /// opened and saved again (`versions/1200-lumetri-26`, `-25x`, `-24x`). + MissingWhenOn, /// An eyedropper colour: reported when set. Eyedropper, /// The Input LUT (`lut` true) or the Look: a popup, a name or an asset. @@ -187,19 +200,19 @@ const ROWS: &[Row] = &[ row(39, S::RgbCurves, Some(K::ArbitraryData), Role::Data(Data::RgbCurves)), row(104, S::HueSat(0), Some(K::Checkbox), Role::Switch), row(113, S::HueSat(0), Some(K::Color), Role::Eyedropper), - row(42, S::HueSat(0), Some(K::ArbitraryData), Role::Data(Data::HueSatCurve)), + row(42, S::HueSat(0), Some(K::ArbitraryData), Role::Data(Data::HueSatCurve(Some("curves/hueSaturationCurves/hueVsSat")))), row(105, S::HueSat(1), Some(K::Checkbox), Role::Switch), row(114, S::HueSat(1), Some(K::Color), Role::Eyedropper), - row(106, S::HueSat(1), Some(K::ArbitraryData), Role::Data(Data::HueSatCurve)), + row(106, S::HueSat(1), Some(K::ArbitraryData), Role::Data(Data::HueSatCurve(None))), row(107, S::HueSat(2), Some(K::Checkbox), Role::Switch), row(115, S::HueSat(2), Some(K::Color), Role::Eyedropper), - row(108, S::HueSat(2), Some(K::ArbitraryData), Role::Data(Data::HueSatCurve)), + row(108, S::HueSat(2), Some(K::ArbitraryData), Role::Data(Data::HueSatCurve(None))), row(109, S::HueSat(3), Some(K::Checkbox), Role::Switch), row(116, S::HueSat(3), Some(K::Color), Role::Eyedropper), - row(110, S::HueSat(3), Some(K::ArbitraryData), Role::Data(Data::HueSatCurve)), + row(110, S::HueSat(3), Some(K::ArbitraryData), Role::Data(Data::HueSatCurve(None))), row(111, S::HueSat(4), Some(K::Checkbox), Role::Switch), row(117, S::HueSat(4), Some(K::Color), Role::Eyedropper), - row(112, S::HueSat(4), Some(K::ArbitraryData), Role::Data(Data::HueSatCurve)), + row(112, S::HueSat(4), Some(K::ArbitraryData), Role::Data(Data::HueSatCurve(None))), // Color Wheels: 46 the switch, 102 Face Detection (on by default), 66 HDR White, 47 the wheels. row(46, S::ColorWheels, Some(K::Checkbox), Role::Switch), row(102, S::ColorWheels, Some(K::Checkbox), Role::Missing), @@ -235,7 +248,7 @@ const ROWS: &[Row] = &[ row(60, S::SpeedGradeCustom, Some(K::ArbitraryData), Role::Data(Data::Other)), // Outside the sections: Legacy Tone And Saturation, the embedded LUTs, Auto Tone, Auto Color, // the LUT and Look assets. - row(97, S::Always, Some(K::Checkbox), Role::Missing), + row(97, S::Always, Some(K::Checkbox), Role::MissingWhenOn), row(98, S::Always, Some(K::ArbitraryData), Role::Data(Data::Other)), row(99, S::Always, Some(K::ArbitraryData), Role::Data(Data::Other)), row(100, S::Always, Some(K::ArbitraryData), Role::Data(Data::Other)), @@ -355,7 +368,11 @@ fn utf16_text(d: &[u8]) -> Option { /// A Lumetri curve (520 bytes): `i32` curve type at 0 (3 for the hue curves, which wrap; 2 for /// Luma vs Sat and Sat vs Sat; 0 for an RGB curve), the `u32` point count at 4, then the points -/// as little-endian `f64` pairs x, y from byte 8 (at most 32). `None` when the count does not fit. +/// as little-endian `f64` pairs x, y from byte 8 (at most 32), sorted by x. Bytes after the +/// counted points are stale (a fresh hue curve has count 0 and old points behind it). An RGB +/// curve's x and y are 0..1 (2 points 0,0 and 1,1 by default); a hue / saturation curve's x is +/// the hue (0 red, 1/3 green) or the luma / saturation, 0..1, and y its offset from neutral. +/// `None` when the count does not fit. fn curve_points(d: &[u8]) -> Option> { let n = usize::try_from(le_u32(d, 4)?).ok()?; if n > d.len().saturating_sub(8) / 16 { @@ -418,7 +435,7 @@ fn data_default(data: Data, d: Option<&[u8]>) -> bool { Data::RgbCurves => { d.len() == 4 * 520 && d.as_chunks::<520>().0.iter().all(|c| curve_points(c).is_some_and(|pts| pts.iter().all(|(x, y)| (x - y).abs() < 1e-9))) } - Data::HueSatCurve => curve_points(d).is_some_and(|pts| pts.iter().all(|(_, y)| *y == 0.0)), + Data::HueSatCurve(_) => curve_points(d).is_some_and(|pts| pts.iter().all(|(_, y)| *y == 0.0)), Data::Wheels => wheels_default(d, 4), Data::CorrectionWheels => wheels_default(d, 8), Data::SplitToning | Data::Other => is_empty_data(Some(d)), @@ -552,6 +569,11 @@ pub(crate) fn import_lumetri(fx: &AepEffect<'_, '_>, params: &[EffectParam<'_, ' col.lose(LossCategory::Effects, format!("{here}: parameter {label} is off and has no counterpart")); } } + Role::MissingWhenOn => { + if p.is_animated() || matches!(p.recorded_value(), Some(ParamValue::Bool(true))) { + col.lose(LossCategory::Effects, format!("{here}: parameter {label} is on and has no counterpart, not imported")); + } + } Role::Missing => { if is_set(p) { col.lose( @@ -575,8 +597,20 @@ pub(crate) fn import_lumetri(fx: &AepEffect<'_, '_>, params: &[EffectParam<'_, ' } if !data_default(d, data()) { let why = match d { - Data::HueSatCurve => "the curve's points are moved; no sample tells their scale", - Data::RgbCurves | Data::Wheels | Data::CorrectionWheels | Data::SplitToning => "the data is not decoded (not sampled)", + Data::RgbCurves => { + import_rgb_curves(data(), (&here, &label), &mut *group, &mut *col); + continue; + } + Data::HueSatCurve(Some(target)) => { + import_hue_sat_curve(data(), target, (&here, &label), &mut *group, &mut *col); + continue; + } + Data::Wheels => { + import_wheels(data(), (&here, &label), &mut *group, &mut *col); + continue; + } + Data::HueSatCurve(None) => "the curve's points are moved; no sample tells their scale", + Data::CorrectionWheels | Data::SplitToning => "the data is not decoded (not sampled)", Data::HslKey | Data::Other => "no counterpart", }; col.lose(LossCategory::Effects, format!("{here}: parameter {label} is not imported ({why})")); @@ -640,3 +674,125 @@ fn import_hsl_key(d: Option<&[u8]>, here: &str, group: &mut effectcraft_project: set(&format!("{id}Softness"), Value::Scalar((t * 100.0).clamp(0.0, 100.0))); } } + +/// A parameter of `group` set to `v` (ids the spec does not have are skipped). +fn set_prop(group: &mut effectcraft_project::PropGroup, id: &str, v: Value) { + if let Some(prop) = group.prop_mut(id) { + prop.value = v; + } +} + +/// Curve points as EffectCraft's `x,y x,y ...` text, x and y clamped to 0..1; `None` when a value +/// is not finite. +fn points_text(pts: impl IntoIterator) -> Option { + let mut out: Vec = Vec::new(); + for (x, y) in pts { + if !x.is_finite() || !y.is_finite() { + return None; + } + out.push(format!("{},{}", x.clamp(0.0, 1.0), y.clamp(0.0, 1.0))); + } + Some(out.join(" ")) +} + +/// The RGB curve (`-0039`, four curves of 520 bytes). The first is the master curve (1200 +/// `lumetri-60-preset-curves-rgb`: its midpoint raised to 0.787 lifts R, G and B alike) and +/// becomes `curves/rgbCurves/master`. The other three are the channel curves, but no sample moves +/// one, so which is red, green or blue is not known: a moved one is reported. +fn import_rgb_curves(d: Option<&[u8]>, (here, label): (&str, &str), group: &mut effectcraft_project::PropGroup, col: &mut crate::import::report::Collector) { + let Some(d) = d else { return }; + let curves: Option>> = if d.len() == 4 * 520 { d.as_chunks::<520>().0.iter().map(|c| curve_points(c)).collect() } else { None }; + let Some(curves) = curves else { + col.lose(LossCategory::DamagedData, format!("{here}: the data of {label} is unreadable, the curves are not imported")); + return; + }; + let identity = |pts: &[(f64, f64)]| pts.iter().all(|(x, y)| (x - y).abs() < 1e-9); + if let Some(master) = curves.first().filter(|c| !identity(c)) { + match points_text(master.iter().copied()) { + Some(text) => set_prop(group, "curves/rgbCurves/master", Value::Str(text)), + None => col.lose(LossCategory::DamagedData, format!("{here}: the master curve of {label} has unreadable points, not imported")), + } + } + let moved = curves.iter().skip(1).filter(|c| !identity(c)).count(); + if moved > 0 { + col.lose( + LossCategory::Effects, + format!("{here}: {moved} channel curve(s) of {label} are not imported (no sample tells which curve is red, green or blue)"), + ); + } +} + +/// Hue vs Sat (`-0042`): After Effects' y is an offset from neutral where 1 doubles the +/// saturation (1200 `lumetri-61-preset-curves-huesat`: a point at hue 0.0155, y 0.97 scales pure +/// red's saturation by about 1.96 in linear light; pins at -0.03 and -0.02 scale green, blue and +/// cyan by 0.97). EffectCraft's y = 1 doubles it and 0.5 is neutral, so y becomes `0.5 + y / 2`. +/// x is the hue in both (0 red, 1/3 green). +fn import_hue_sat_curve( + d: Option<&[u8]>, + target: &str, + (here, label): (&str, &str), + group: &mut effectcraft_project::PropGroup, + col: &mut crate::import::report::Collector, +) { + let Some(d) = d else { return }; + match curve_points(d).and_then(|pts| points_text(pts.into_iter().map(|(x, y)| (x, 0.5 + y / 2.0)))) { + Some(text) => set_prop(group, target, Value::Str(text)), + None => col.lose(LossCategory::DamagedData, format!("{here}: the data of {label} is unreadable, the curve is not imported")), + } +} + +/// The colour EffectCraft's wheel takes for After Effects' hue (degrees, 0 red, 120 green) and +/// distance from the centre (0..1): the HSL colour of that hue with saturation = distance and +/// lightness 0.5 (grey 0.5 at distance 0). EffectCraft's wheel adds that colour's offset from its +/// grey, so the direction is After Effects' and the strength grows with the distance. +fn wheel_colour(hue: f64, distance: f64) -> [f64; 4] { + let c = distance.clamp(0.0, 1.0); + let h = hue.rem_euclid(360.0) / 60.0; + let x = c * (1.0 - (h.rem_euclid(2.0) - 1.0).abs()); + let (r, g, b) = match h { + h if h < 1.0 => (c, x, 0.0), + h if h < 2.0 => (x, c, 0.0), + h if h < 3.0 => (0.0, c, x), + h if h < 4.0 => (0.0, x, c), + h if h < 5.0 => (x, 0.0, c), + _ => (c, 0.0, x), + }; + let m = 0.5 - c / 2.0; + [r + m, g + m, b + m, 1.0] +} + +/// Color Wheels (`-0047`, 96 bytes): four wheels of three little-endian `f64`, the hue in degrees, +/// the brightness slider (0..1, 0.5 neutral) and the distance from the centre (0..1); `0, 0.5, 0` +/// each by default. The first three are Shadows, Midtones and Highlights (1200 +/// `lumetri-62-preset-wheels`: Shadows 229 degrees (blue), slider 0.37, distance 0.55; Midtones +/// 0.1 degrees (red), 0.5, 0.39; Highlights 93 degrees (yellow-green), 0.73, 0.32); the fourth is +/// not known. Hue and distance become EffectCraft's wheel colour ([`wheel_colour`]); the brightness +/// sliders and a set fourth wheel have no counterpart and are reported. +fn import_wheels(d: Option<&[u8]>, (here, label): (&str, &str), group: &mut effectcraft_project::PropGroup, col: &mut crate::import::report::Collector) { + let Some(d) = d else { return }; + let wheels: Option> = if d.len() == 4 * 24 { + (0..4).map(|w| Some([le_f64(d, w * 24)?, le_f64(d, w * 24 + 8)?, le_f64(d, w * 24 + 16)?]).filter(|v| v.iter().all(|x| x.is_finite()))).collect() + } else { + None + }; + let Some(wheels) = wheels else { + col.lose(LossCategory::DamagedData, format!("{here}: the data of {label} is unreadable, the colour wheels are not imported")); + return; + }; + let mut lost: Vec = Vec::new(); + for (w, (id, name)) in wheels.iter().zip([("shadowsWheel", "Shadows"), ("midtonesWheel", "Midtones"), ("highlightsWheel", "Highlights")]) { + let [hue, brightness, distance] = *w; + if distance != 0.0 { + set_prop(group, &format!("colorWheels/{id}"), Value::Color(wheel_colour(hue, distance))); + } + if (brightness - 0.5).abs() > 1e-9 { + lost.push(format!("{name} brightness {brightness:.3} (0.5 is neutral)")); + } + } + if wheels.get(3).is_some_and(|w| *w != [0.0, 0.5, 0.0]) { + lost.push("the fourth wheel (its role is not known)".to_string()); + } + if !lost.is_empty() { + col.lose(LossCategory::Effects, format!("{here}: {label}: {} not imported (no counterpart)", lost.join(", "))); + } +} diff --git a/crates/aep/tests/it/import/lumetri.rs b/crates/aep/tests/it/import/lumetri.rs index 526f43648..a3ec9138b 100644 --- a/crates/aep/tests/it/import/lumetri.rs +++ b/crates/aep/tests/it/import/lumetri.rs @@ -172,6 +172,7 @@ fn defs() -> Vec<(u16, Def)> { (39, Def::Data { name: "RGB Curve" }), (104, Def::Check { default: true }), (42, Def::Data { name: "Hue vs Sat" }), + (106, Def::Data { name: "Hue vs Hue" }), (47, Def::Data { name: "Wheels" }), (68, Def::Check { default: true }), (73, Def::Data { name: "HSL Sliders" }), @@ -195,6 +196,7 @@ fn default_data() -> Vec<(u16, Rec)> { (32, Rec::Data(vec![0; 32])), (39, Rec::Data(rgb_curves())), (42, Rec::Data(curve(3, &[]))), + (106, Rec::Data(curve(3, &[]))), (47, Rec::Data(wheels(4))), (73, Rec::Data(hex(HSL_FRESH))), (85, Rec::Data(wheels(8))), @@ -372,20 +374,112 @@ fn a_look_is_reported_by_name() { } #[test] -fn hue_saturation_curves_with_neutral_points_arrive_moved_ones_are_reported() { +fn hue_vs_sat_arrives_with_its_y_scaled_other_moved_curves_are_reported() { // 1200 `-11-sec-hue-saturation-curves`: the selector put three neutral points on the curve. let neutral = curve(3, &[(0.742_483, 0.0), (0.798_038, 0.0), (0.853_594, 0.0)]); let (g, r) = run(&[(42, Rec::Data(neutral))]); assert_eq!(losses(&r), Vec::::new()); assert_eq!(val(&g, "curves/hueSaturationCurves/hueVsSat"), Value::Str(String::new())); - let (_, r) = run(&[(42, Rec::Data(curve(3, &[(0.5, 0.25)])))]); + // 1200 `-61-preset-curves-huesat`: red raised to 0.97 (After Effects nearly doubles its + // saturation), the neighbours pinned near 0. EffectCraft's y: 0.5 + y / 2. + let preset = curve(3, &[(0.015_463_917_525_773_196, 0.97), (0.262_886_597_938_144_34, -0.03), (0.764_604_810_996_563_5, -0.02)]); + let (g, r) = run(&[(42, Rec::Data(preset))]); + assert_eq!(losses(&r), Vec::::new()); + let Value::Str(text) = val(&g, "curves/hueSaturationCurves/hueVsSat") else { panic!("a point list") }; + let pts: Vec<(f64, f64)> = text + .split(' ') + .map(|p| { + let (x, y) = p.split_once(',').unwrap(); + (x.parse().unwrap(), y.parse().unwrap()) + }) + .collect(); + let want = [(0.015_464, 0.985), (0.262_887, 0.485), (0.764_605, 0.49)]; + assert_eq!(pts.len(), 3, "{text}"); + for ((x, y), (wx, wy)) in pts.iter().zip(want) { + assert!((x - wx).abs() < 1e-5 && (y - wy).abs() < 1e-9, "{text}"); + } + // Hue vs Hue moved: no sample tells its scale, reported and left neutral. + let (g, r) = run(&[(106, Rec::Data(curve(3, &[(0.5, 0.25)])))]); let l = losses(&r); - assert!(l.len() == 1 && l[0].contains("Hue vs Sat (ADBE Lumetri-0042) is not imported"), "{l:?}"); - // A point count past the data: not a default, reported, no panic. + assert!(l.len() == 1 && l[0].contains("Hue vs Hue (ADBE Lumetri-0106) is not imported"), "{l:?}"); + assert_eq!(val(&g, "curves/hueSaturationCurves/hueVsHue"), Value::Str(String::new())); + // A point count past the data: not a default, reported as damaged, no panic. let mut bad = curve(3, &[]); bad[4..8].copy_from_slice(&1000u32.to_le_bytes()); - let (_, r) = run(&[(42, Rec::Data(bad))]); - assert_eq!(losses(&r).len(), 1); + let (g, r) = run(&[(42, Rec::Data(bad))]); + let l = losses(&r); + assert!(l.len() == 1 && l[0].contains("Hue vs Sat (ADBE Lumetri-0042) is unreadable"), "{l:?}"); + assert_eq!(val(&g, "curves/hueSaturationCurves/hueVsSat"), Value::Str(String::new())); +} + +#[test] +fn the_master_rgb_curve_arrives_moved_channel_curves_are_reported() { + // 1200 `-60-preset-curves-rgb`: the first of the four curves, its midpoint raised. + let master = curve(0, &[(0.0, 0.0), (0.491_408_934_707_903_76, 0.786_885_245_901_639_3), (1.0, 1.0)]); + let identity = curve(0, &[(0.0, 0.0), (1.0, 1.0)]); + let mut d = master.clone(); + (0..3).for_each(|_| d.extend(&identity)); + let (g, r) = run(&[(39, Rec::Data(d))]); + assert_eq!(losses(&r), Vec::::new()); + assert_eq!(val(&g, "curves/rgbCurves/master"), Value::Str("0,0 0.49140893470790376,0.7868852459016393 1,1".into())); + assert_eq!(val(&g, "curves/rgbCurves/red"), Value::Str("0,0 1,1".into())); + // A moved channel curve: which channel it is is not known, reported. + let mut d = identity.clone(); + d.extend(&master); + (0..2).for_each(|_| d.extend(&identity)); + let (g, r) = run(&[(39, Rec::Data(d))]); + let l = losses(&r); + assert!(l.len() == 1 && l[0].contains("1 channel curve(s) of RGB Curve (ADBE Lumetri-0039) are not imported"), "{l:?}"); + assert_eq!(val(&g, "curves/rgbCurves/master"), Value::Str("0,0 1,1".into())); + // Damaged: the wrong length. + let (_, r) = run(&[(39, Rec::Data(master))]); + assert!(losses(&r).iter().any(|x| x.contains("RGB Curve (ADBE Lumetri-0039) is unreadable")), "{:?}", losses(&r)); +} + +#[test] +fn colour_wheels_arrive_as_colours_their_brightness_is_reported() { + // 1200 `-62-preset-wheels`: Shadows towards blue with the slider down, Midtones towards red, + // Highlights towards yellow-green with the slider up; the fourth wheel at its default. + let w: Vec = [ + 229.119_895_976_548_18, + 0.369_892_467_494_294_2, + 0.547_209_348_004_638, + 0.104_745_366_878_020_72, + 0.5, + 0.394_961_840_132_529_44, + 93.374_076_744_668_57, + 0.726_881_718_523_399_9, + 0.318_976_840_675_999_35, + 0.0, + 0.5, + 0.0, + ] + .into_iter() + .flat_map(f64::to_le_bytes) + .collect(); + let (g, r) = run(&[(47, Rec::Data(w.clone()))]); + let colour = |id: &str| match val(&g, id) { + Value::Color(c) => c, + other => panic!("{id}: {other:?}"), + }; + // The hue's direction: blue strongest in Shadows, red in Midtones, green in Highlights. + let s = colour("colorWheels/shadowsWheel"); + assert!(s[2] > 0.77 && s[0] < 0.23 && (s[0] + s[2] - 1.0).abs() < 1e-9, "{s:?}"); + let m = colour("colorWheels/midtonesWheel"); + assert!((m[0] - 0.697_480_920_066_264_7).abs() < 1e-9 && m[1] > m[2], "{m:?}"); + let h = colour("colorWheels/highlightsWheel"); + assert!(h[1] > h[0] && h[0] > h[2], "{h:?}"); + let l = losses(&r); + assert!(l.len() == 1 && l[0].contains("Shadows brightness 0.370") && l[0].contains("Highlights brightness 0.727"), "{l:?}"); + assert!(!l[0].contains("Midtones") && !l[0].contains("fourth"), "{l:?}"); + // The fourth wheel set: reported; damaged data: reported, the wheels stay grey. + let mut w4 = w; + w4[88..96].copy_from_slice(&0.3f64.to_le_bytes()); + let (_, r) = run(&[(47, Rec::Data(w4))]); + assert!(losses(&r).iter().any(|x| x.contains("the fourth wheel")), "{:?}", losses(&r)); + let (g, r) = run(&[(47, Rec::Data(vec![1; 95]))]); + assert!(losses(&r).iter().any(|x| x.contains("Wheels (ADBE Lumetri-0047) is unreadable")), "{:?}", losses(&r)); + assert_eq!(val(&g, "colorWheels/shadowsWheel"), Value::Color([0.5, 0.5, 0.5, 1.0])); } #[test] @@ -420,17 +514,30 @@ fn the_picked_hsl_key_becomes_hue_saturation_and_lightness_ranges() { fn undecoded_data_and_controls_without_counterpart_are_reported_when_set() { let mut split = vec![0u8; 32]; split[0] = 1; - let mut w = wheels(4); - w[8] = 1; - let (_, r) = run(&[(32, Rec::Data(split)), (47, Rec::Data(w)), (97, Rec::Num(1.0, 3)), (95, Rec::Data(utf16("my.cube")))]); + let (_, r) = run(&[(32, Rec::Data(split)), (97, Rec::Num(1.0, 3)), (95, Rec::Data(utf16("my.cube")))]); let l = losses(&r); - assert_eq!(l.len(), 4, "{l:?}"); + assert_eq!(l.len(), 3, "{l:?}"); assert!(l.iter().any(|d| d.contains("Split Toning (ADBE Lumetri-0032) is not imported")), "{l:?}"); - assert!(l.iter().any(|d| d.contains("Wheels (ADBE Lumetri-0047) is not imported")), "{l:?}"); assert!(l.iter().any(|d| d.contains("Legacy Tone And Saturation (ADBE Lumetri-0097) is on")), "{l:?}"); assert!(l.iter().any(|d| d.contains("the Input LUT 'my.cube' is not imported")), "{l:?}"); } +#[test] +fn legacy_tone_on_is_reported_whatever_the_pard_default_says() { + // After the project is opened and saved again, the `pard` default of Legacy Tone And + // Saturation reads on (versions/1200-lumetri-26 / -25x / -24x; off in 1200 as the script + // saved it): a record that says on is still reported, a missing record is not. + let mut d = defs(); + if let Some((_, Def::Check { default })) = d.iter_mut().find(|(s, _)| *s == 97) { + *default = true; + } + let (_, r) = run_with(&d, &[(97, Rec::Num(1.0, 3))]); + let l = losses(&r); + assert!(l.len() == 1 && l[0].contains("Legacy Tone And Saturation (ADBE Lumetri-0097) is on"), "{l:?}"); + let (_, r) = run_with(&d, &[]); + assert_eq!(losses(&r), Vec::::new()); +} + #[test] fn an_unknown_scale_or_parameter_is_reported_not_guessed() { let mut d = defs(); diff --git a/docs/aep-format.md b/docs/aep-format.md index 9a37b384f..639c0a013 100644 --- a/docs/aep-format.md +++ b/docs/aep-format.md @@ -534,9 +534,10 @@ suffix is a parameter id, not the `parT` position (Curves -0002 Channel before - -0005 Quality before -0004). API: `CurvesData`, `MeshData`, `curve_lut`. [V; details: corpus FORMAT S] -**Lumetri Color** (`ADBE Lumetri`, 1200: 58 compositions made by script in After Effects 2026, 32 bpc, -working space None; defaults, all set, one per section, valid minima and maxima, every popup item the -script can set, checkboxes toggled, keys, an expression): 131 `parT` entries. The `-NNNN` suffix is a +**Lumetri Color** (`ADBE Lumetri`, 1200: 63 compositions in After Effects 2026, 32 bpc, working space +None; by script: defaults, all set, one per section, valid minima and maxima, every popup item the script +can set, checkboxes toggled, keys, an expression; from presets made by hand, `-58` ... `-62`: an Input +LUT, a Look, the RGB curve, Hue vs Sat, the colour wheels): 131 `parT` entries. The `-NNNN` suffix is a parameter id, not the position (`-0127` ... `-0130` sit between `-0005` and `-0026`), so the importer keys by suffix. Float sliders (type 10) store the value After Effects shows; the valid ranges (`pard` 104 / 108) are wider than the slider's (112 / 116): Temperature / Tint -300..300 (slider -100..100), @@ -546,31 +547,50 @@ HDR Range 100..10000, Refine Blur 0..1000 (0..30), Vignette Amount -5..5 (-3..3) `-19-min` / `-20-max` equal the stored numbers. **Hidden controls are recorded in the default state** (`tdsb` 3) and still hold the value After Effects reads back: High Dynamic Range `-0062` (1.0), HDR Specular `-0064` (22), the second Look popup `-0128` (26.16 as set, read back 26), Legacy Tone And -Saturation `-0097`, Auto Color `-0124`, Face Detection `-0102`, the Auto Intensity `-0123` +Saturation `-0097` (its `pard` default reads off as the script saved the project and on once the +project was opened and saved again, `versions/1200-lumetri-26`, `-25x`, `-24x`; a fresh instance reads +back off), Auto Color `-0124`, Face Detection `-0102`, the Auto Intensity `-0123` (`EffectParam::recorded_value`). Each section has an Active checkbox (blank `pard` name; `-0003` Basic Correction, `-0023` Creative, `-0037` Curves, `-0118` RGB Curves, `-0104` / `-0105` / `-0107` / `-0109` / `-0111` the hue / saturation curves, `-0046` Color Wheels, `-0068` HSL Secondary, `-0050` Vignette, on by default); off, the section does nothing (`-03-sec-basic-correction`, `-17-sec-vignette` render as `-00-defaults`). Arbitrary data (type 11, a record and `aRbs` in every instance; empty is `fe fe`): `-0001` an XML copy of the grade written only when the Look changes (not kept current: `-01-set-all` -holds defaults for controls set after the Look); `-0096` the Look's name, UTF-16LE (`SL BLUE ICE`; the -popup `-0025` itself has no record, `-0128` can disagree with it: `-07-sec-creative` names item 20 -while `-0128` holds 23), `-0095` likewise for the Input LUT (H, by analogy: no sample sets a LUT); -`-0129` the Look colour space's 16-byte id (the GUID `LookColorSpace` of `-0001`, byte-swapped; zero -for None; the Color Space popup `-0130` writes no record of its own, `-22-pop38-1` ... `-53-pop38-33`); +holds defaults for controls set after the Look; it names the LUT file, `Fuji F125 Kodak 2393 (by +Adobe).cube`); `-0096` the Look's name, UTF-16LE (`SL BLUE ICE`, `Fuji F125 Kodak 2393 (by Adobe)` in +`-59-preset-look-lut`; the popup `-0025` itself has no record, `-0128` can disagree with it: +`-07-sec-creative` names item 20 while `-0128` holds 23), `-0095` likewise for the Input LUT +(`ALEXA_Default_LogC2Rec709`, `-58-preset-input-lut`; the popups `-0005` / `-0127` keep no record, +`-0004`, `-0024` and the assets `-0125` / `-0126` stay `fe fe`: the LUTs are After Effects' own files, +named, not embedded); `-0129` the Look colour space's 16-byte id (the GUID `LookColorSpace` of `-0001` +in Windows layout: the first 4, 2 and 2 bytes little-endian; zero for None; the Color Space popup +`-0130` writes no record of its own, `-22-pop38-1` ... `-53-pop38-33`); `-0032` Split Toning (32 zero bytes); `-0039` the four RGB curves and `-0042`, `-0106`, `-0108`, `-0110`, `-0112` the hue / saturation curves, 520 bytes each: `i32` type (3 for the hue curves, 2 for Luma vs Sat and Sat vs Sat, 0 for an RGB curve), `u32` point count, then little-endian `f64` pairs x, y -(RGB: (0, 0), (1, 1) by default; hue / saturation: no points, and the selector colours `-0113` ... -`-0117` add three points at y = 0, the hue picked +- 0.0556; no sample moves a point, so y's scale is -H); `-0047` Color Wheels (4 x `f64` triples, 0, 0.5, 0 each) and `-0085` the HSL correction wheels (8 -triples, the same); `-0073` the HSL key, 40 bytes: a flag byte (1 once a colour was picked), then hue, +sorted by x (bytes after the counted points are stale). RGB: x and y 0..1, (0, 0), (1, 1) by default; +the first of the four is the master curve (`-60-preset-curves-rgb`: (0.491, 0.787) lifts R, G and B +alike, and the render follows a natural cubic spline through the points within 0.003); the order of +the other three is H (the `-0001` XML names `ToneCurvesLuma`, `Red`, `Green`, `Blue`; no sample moves +one). Hue / saturation: x the hue (0 red, 1/3 green) or the luma / saturation 0..1, y the offset from +neutral 0; a fresh curve has no points, the selector colours `-0113` ... `-0117` add three at y = 0, +the hue picked +- 0.0556. Hue vs Sat's y scales the saturation by 1 + y in linear light +(`-61-preset-curves-huesat`: (0.0155, 0.97) takes pure red's linear saturation to 1.96 times, (0.263, +-0.03) and (0.765, -0.02) green, cyan and blue to 0.97); the other four curves' scale is H. `-0047` +Color Wheels, 4 `f64` triples: hue in degrees (0 red, 120 green), brightness slider 0..1 (0.5 +neutral), distance from the centre 0..1; 0, 0.5, 0 each by default; Shadows, Midtones, Highlights, then +a fourth wheel of unknown role (`-62-preset-wheels`: Shadows 229.1, 0.370, 0.547 towards blue with the +slider down; Midtones 0.1, 0.5, 0.395 towards red; Highlights 93.4, 0.727, 0.319 towards yellow-green +with the slider up; the fourth at its default); `-0085` the HSL correction wheels (8 triples, the same +default; layout H); `-0073` the HSL key, 40 bytes: a flag byte (1 once a colour was picked), then hue, saturation, lightness, each centre, range, tolerance (`f32` LE, fractions) and an enabled byte (fresh: 0.5, 0, 0, on; picking RGB 0.02 0.55 0.85 gives hue centre 0.5605 = that colour's hue and lightness centre 0.435 = its HSL lightness, range 0.05, tolerance 0.1: `-01-set-all`; whether range is the half -or full width is H); `-0122` the Auto Tone analysis (UTF-16 JSON). Curves, LUTs and wheels are not -scriptable: the presets for them are still to be made. [V for the sliders, checkboxes, popups and the -`tdsb` 3 values; H where marked] +or full width is H); `-0122` the Auto Tone analysis (UTF-16 JSON). The project saved as 25.x and 24.x +copies (`versions/1200-lumetri-25x`, `-24x`: the 58 scripted compositions, saved by After Effects +2026) stores every Lumetri definition, valid range and value as the 26 save does. [V for the sliders, +checkboxes, popups, the `tdsb` 3 values, the master RGB curve, Hue vs Sat, the colour wheels and the +LUT names; H where marked] ### 3.8 Masks and bezier paths (`tdgp[ADBE Mask Parade]`) diff --git a/docs/aep-import.md b/docs/aep-import.md index 07c089782..f09f0ba1a 100644 --- a/docs/aep-import.md +++ b/docs/aep-import.md @@ -1,6 +1,6 @@ # After Effects project import -> **Last reviewed:** 2026-11-05 · **Last updated:** 2026-11-05 · **Change:** Lumetri Color is imported (1200: every slider, checkbox and popup with a counterpart, by match name, also from records in the default state; switched-off sections, LUTs, looks, undecoded curves and wheels are reported); before that: Curves and Mesh Warp are imported (961-965, 985; FORMAT S of the corpus); before that: a shape item's eye switch is imported (bit 0 of its group flags, 986 / 987: a switched-off Fill under Trim Paths is no longer drawn, render equal to After Effects), and a layer style's (switch group `tdsb` 0, 988); before that: a negative time stretch (Time Reverse Layer) is imported: in and out point in order, the layer plays backwards (981-983: render equal to After Effects at 7 points); before that: After Effects' None on Compound Blur, Displacement Map and Set Matte arrives as an explicit None (`NO_LAYER`), not as the owning layer; before that: layer styles are imported (the nine styles and Blending Options, 951-979; Global Light becomes the composition's); before that: the 27 effects of round 3 are in the table (`effectcraft_aep::effect_map`); before that: a layer of a clip, sequence or sound ends at its source's end (After Effects ends it there when it opens a project, read-back test f1), unless it is remapped or looped or runs less than a frame past; before that: keys of shape colours are read (the reader took their 152-byte spatial records for plain ones: `tdb4` byte 7 is 0 there, 854); before that: the effect table moved to `effectcraft_aep::effect_map`; point defaults in percent and a popup default slot of 0 (After Effects' save of a project it opened, e1 / e2 re-saves) are read; before that: the justification of right-to-left paragraphs is read relative to the direction (t7); before that: 24 more layer blend modes (906, 907); before that: 27 more effects are imported (batch 6: 903-905), with layer parameters; before that: a Rec. 709 working space also sets the output space (After Effects renders in the working space, 902); before that: a mask whose path was never set arrives as the source rectangle (900, 901); before that: Offset Paths' Copy Offset arrives as EffectCraft's (After Effects' value minus 1, 828); before that: project settings, image sequences, conformed clips and effect instance names are imported, next to the mattes, shapes, text settings and animators, effects, markers, time remapping, cameras and lights); before that: parallel and ambient lights (701, 702) and keyed Orientation (703) are imported; before that: masks on text and shape layers (705, 706: AE vs EffectCraft render 12 and 0 of 5184 points, glyph edges) and Merge Paths, Offset Paths, Repeater (707-710, 731; merge renders equal but for edges; Repeater copy opacity and Offset Paths' outline differ in EffectCraft's renderer) are imported; before that: keyframes: the ease of Anchor Point, effect point, effect colour and mask path keys is imported (597-599, 700, 730: within 0.5 px of After Effects' read-backs); before that: effects: Directional Blur, Radial Blur, CC Radial Blur, Venetian Blinds, Channel Blur, Black & White, Tritone, Luma Key, Simple Choker, Matte Choker and Transform are imported (711-723; render checks: CC Radial Blur and Channel Blur equal, Directional Blur within 5/255; Venetian Blinds, Radial Blur, Black & White (8/255), Tritone (19/255), Luma Key, Matte Choker and Transform differ in EffectCraft's renderer, see STATUS); before that: text: character and paragraph settings (stroke, scales, baseline shift, kerning, ligatures, tsume, indents, spacing, justified text, composer, direction, vertical text; 538-578) and text animators with range selectors (579-594, 114) are imported; render checks 594 (keyed range selector at 0 / 0.5 / 1 / 1.5 / 2.5 s), 595, 596 equal but for glyph edges; before that: cameras, spot and point lights, X / Y Rotation and Orientation, Timewarp and expressions on effect parameters are imported (render checks 486, 535-537 equal but for edges; 488 differs by 5.25 %, cause in EffectCraft's renderer not isolated); before that: text: superscript and subscript are imported (526-528); before that: effects: Invert, Fast Box Blur, Gaussian Blur, Drop Shadow, Glow, Fill, Hue/Saturation, Levels and five expression controls are imported, popups are mapped by item name, checkbox defaults come from the file; before that: shape layers are imported (groups, paths, paints, trim paths, round corners; 451-484, 519 added); before that: markers and Time Remapping are imported (509, 510 added); before that: track mattes are imported (402-408); before that: text: automatic leading is verified (400); effects: Exposure is imported (Channels, all twelve sliders, Bypass, keys), next to Tint and Gradient Ramp; before that: text: U+0003 is a paragraph break (386, 387); before that: a stored text Anchor Point is pixels (385, now V); before that: effects: Tint and Gradient Ramp are imported (all parameters and keys), other effects stay reported; `.aepx` is accepted; text layers are imported (Source Text with style runs, paragraphs and box text; fonts by PostScript name, missing fonts listed); layer styles are reported; before that: layer masks · **Target:** Adobe After Effects 2026 +> **Last reviewed:** 2026-11-05 · **Last updated:** 2026-11-05 · **Change:** Lumetri Color's master RGB curve, Hue vs Sat curve and colour wheels are imported (1200 presets made by hand); before that: Lumetri Color is imported (1200: every slider, checkbox and popup with a counterpart, by match name, also from records in the default state; switched-off sections, LUTs, looks, undecoded curves and wheels are reported); before that: Curves and Mesh Warp are imported (961-965, 985; FORMAT S of the corpus); before that: a shape item's eye switch is imported (bit 0 of its group flags, 986 / 987: a switched-off Fill under Trim Paths is no longer drawn, render equal to After Effects), and a layer style's (switch group `tdsb` 0, 988); before that: a negative time stretch (Time Reverse Layer) is imported: in and out point in order, the layer plays backwards (981-983: render equal to After Effects at 7 points); before that: After Effects' None on Compound Blur, Displacement Map and Set Matte arrives as an explicit None (`NO_LAYER`), not as the owning layer; before that: layer styles are imported (the nine styles and Blending Options, 951-979; Global Light becomes the composition's); before that: the 27 effects of round 3 are in the table (`effectcraft_aep::effect_map`); before that: a layer of a clip, sequence or sound ends at its source's end (After Effects ends it there when it opens a project, read-back test f1), unless it is remapped or looped or runs less than a frame past; before that: keys of shape colours are read (the reader took their 152-byte spatial records for plain ones: `tdb4` byte 7 is 0 there, 854); before that: the effect table moved to `effectcraft_aep::effect_map`; point defaults in percent and a popup default slot of 0 (After Effects' save of a project it opened, e1 / e2 re-saves) are read; before that: the justification of right-to-left paragraphs is read relative to the direction (t7); before that: 24 more layer blend modes (906, 907); before that: 27 more effects are imported (batch 6: 903-905), with layer parameters; before that: a Rec. 709 working space also sets the output space (After Effects renders in the working space, 902); before that: a mask whose path was never set arrives as the source rectangle (900, 901); before that: Offset Paths' Copy Offset arrives as EffectCraft's (After Effects' value minus 1, 828); before that: project settings, image sequences, conformed clips and effect instance names are imported, next to the mattes, shapes, text settings and animators, effects, markers, time remapping, cameras and lights); before that: parallel and ambient lights (701, 702) and keyed Orientation (703) are imported; before that: masks on text and shape layers (705, 706: AE vs EffectCraft render 12 and 0 of 5184 points, glyph edges) and Merge Paths, Offset Paths, Repeater (707-710, 731; merge renders equal but for edges; Repeater copy opacity and Offset Paths' outline differ in EffectCraft's renderer) are imported; before that: keyframes: the ease of Anchor Point, effect point, effect colour and mask path keys is imported (597-599, 700, 730: within 0.5 px of After Effects' read-backs); before that: effects: Directional Blur, Radial Blur, CC Radial Blur, Venetian Blinds, Channel Blur, Black & White, Tritone, Luma Key, Simple Choker, Matte Choker and Transform are imported (711-723; render checks: CC Radial Blur and Channel Blur equal, Directional Blur within 5/255; Venetian Blinds, Radial Blur, Black & White (8/255), Tritone (19/255), Luma Key, Matte Choker and Transform differ in EffectCraft's renderer, see STATUS); before that: text: character and paragraph settings (stroke, scales, baseline shift, kerning, ligatures, tsume, indents, spacing, justified text, composer, direction, vertical text; 538-578) and text animators with range selectors (579-594, 114) are imported; render checks 594 (keyed range selector at 0 / 0.5 / 1 / 1.5 / 2.5 s), 595, 596 equal but for glyph edges; before that: cameras, spot and point lights, X / Y Rotation and Orientation, Timewarp and expressions on effect parameters are imported (render checks 486, 535-537 equal but for edges; 488 differs by 5.25 %, cause in EffectCraft's renderer not isolated); before that: text: superscript and subscript are imported (526-528); before that: effects: Invert, Fast Box Blur, Gaussian Blur, Drop Shadow, Glow, Fill, Hue/Saturation, Levels and five expression controls are imported, popups are mapped by item name, checkbox defaults come from the file; before that: shape layers are imported (groups, paths, paints, trim paths, round corners; 451-484, 519 added); before that: markers and Time Remapping are imported (509, 510 added); before that: track mattes are imported (402-408); before that: text: automatic leading is verified (400); effects: Exposure is imported (Channels, all twelve sliders, Bypass, keys), next to Tint and Gradient Ramp; before that: text: U+0003 is a paragraph break (386, 387); before that: a stored text Anchor Point is pixels (385, now V); before that: effects: Tint and Gradient Ramp are imported (all parameters and keys), other effects stay reported; `.aepx` is accepted; text layers are imported (Source Text with style runs, paragraphs and box text; fonts by PostScript name, missing fonts listed); layer styles are reported; before that: layer masks · **Target:** Adobe After Effects 2026 File ▸ Import ▸ After Effects Project… (`file.importAep`) reads an `.aep` or its XML form `.aepx` and adds its items to the open project, in a folder named after the file (as After Effects does when it imports a project). @@ -179,7 +179,7 @@ parameter gets the file's value: | `ADBE Easy Levels2` 1 Channel, 2 Histogram data, 8 / 9 Clip To Output Black / White | `channel`; `inBlack`, `inWhite`, `gamma`, `outBlack`, `outWhite` from the data; `clipToOutputBlack`, `clipToOutputWhite` | popups by name (After Effects' items On, Off, Off for 32 bpc Color are in another order here); the RGB values are the `f32` of the arbitrary data; the per-channel values are reported when they are not at their defaults (not sampled) | 511-516 | | Batch 6 (903 defaults, 904 / 905 set values): `ADBE Brightness & Contrast 2`, `ADBE Turbulent Displace`, `ADBE Fractal Noise`, `ADBE Laser` (Beam), `CC Radial Fast Blur`, `ADBE Noise2`, `ADBE Pro Levels2` (Levels (Individual Controls)), `ADBE Corner Pin`, `ADBE Grid`, `ADBE Sharpen`, `ADBE Linear Color Key2`, `ADBE Radial Wipe`, `ADBE Remove Color Matting`, `CC Bend It`, `CC Power Pin`, `ADBE 4ColorGradient`, `ADBE Change Color`, `ADBE Extract`, `ADBE Noise HLS2`, `ADBE Scatter`, `CC Light Sweep`, `ADBE Compound Blur`, `ADBE Displacement Map`, `ADBE Set Matte3`, `ADBE Gaussian Blur` (legacy), `ADBE OFMotionBlur` (Pixel Motion Blur), `ADBE Layer Control` | the EffectCraft effect of the same name, every parameter EffectCraft has | sliders as stored (each checked against its valid maximum); Beam Length, Time and Softness and CC Power Pin Perspective are 0..1 fractions, percentages here; points as layer pixels; popups by item name (Set Matte's `Red Channel` ... is `Red` ...); twirl-down group markers and the Levels histogram carry nothing; **layer parameters** (below) | 903, 904, 905; 220, 320, 321 | | `ADBE Slider Control`, `ADBE Angle Control`, `ADBE Checkbox Control`, `ADBE Color Control`, `ADBE Point Control` (parameter 1) | `ec.control.slider` `slider`, `ec.control.angle` `angle`, `ec.control.checkbox` `checkbox`, `ec.control.color` `color`, `ec.control.point` `point` | float (valid max 1 000 000) as stored; degrees; checkbox; colour; point in layer pixels | 071, 073, 217-219, 221, 304, 310 | -| `ADBE Lumetri` by match-name suffix (1200), not by index: -0062 High Dynamic Range; -0063 HDR White; -0007 / -0008 Temperature / Tint; -0020 Saturation; -0011 Exposure; -0012 ... -0016 Contrast, Highlights, Shadows, Whites, Blacks; -0064 HDR Specular; -0026 Look Intensity; -0028 Faded Film; -0029 Sharpen; -0030 Vibrance; -0031 Saturation; -0033 Tint Balance; -0074 Show Mask; -0075 mask style; -0076 Invert Mask; -0081 Denoise; -0082 Blur; -0086 ... -0090 the HSL correction's Temperature, Tint, Contrast, Sharpen, Saturation; -0051 ... -0054 Vignette Amount, Midpoint, Roundness, Feather; -0073 the HSL key data | `highDynamicRange`, `basicCorrection/tone/hdrWhite`, `basicCorrection/whiteBalance/temperature` / `tint`, `basicCorrection/saturation`, `basicCorrection/tone/exposure`, `.../contrast`, `highlights`, `shadows`, `whites`, `blacks`, `hdrSpecular`, `creative/lookIntensity`, `creative/adjustments/fadedFilm`, `sharpen`, `vibrance`, `creativeSaturation`, `tintBalance`, `hslSecondary/key/showMask`, `maskStyle`, `invertMask`, `hslSecondary/refine/denoise`, `blur`, `hslSecondary/correction/*`, `vignette/*`; `hslSecondary/key/hue*`, `saturation*`, `lightness*` | sliders: the stored number x 1 (valid maxima 1000, 300, 300, 7, 150, 200, 150, 100, 100, 300, 150, 100, 1000, 5, 100 as in `pard`); checkboxes and the popup as elsewhere, **also from records in the default state** (`tdsb` 3); the key: hue centre x 360 degrees, range x 360 as the half width, tolerance x 360 as the softness, saturation and lightness centre +- range x 100 percent, tolerance x 100, once a colour was picked | 1200: all 62 read-backs of mapped controls in sections that act equal after import (min, max, sections, popups); the other 61 lie in sections that do nothing and are reported; keys and the expression on HDR White (`-56-keys`, `-57-expr`) | +| `ADBE Lumetri` by match-name suffix (1200), not by index: -0062 High Dynamic Range; -0063 HDR White; -0007 / -0008 Temperature / Tint; -0020 Saturation; -0011 Exposure; -0012 ... -0016 Contrast, Highlights, Shadows, Whites, Blacks; -0064 HDR Specular; -0026 Look Intensity; -0028 Faded Film; -0029 Sharpen; -0030 Vibrance; -0031 Saturation; -0033 Tint Balance; -0074 Show Mask; -0075 mask style; -0076 Invert Mask; -0081 Denoise; -0082 Blur; -0086 ... -0090 the HSL correction's Temperature, Tint, Contrast, Sharpen, Saturation; -0051 ... -0054 Vignette Amount, Midpoint, Roundness, Feather; -0073 the HSL key data; -0039 the first of the four RGB curves; -0042 Hue vs Sat; -0047 the Shadows, Midtones and Highlights wheels | `highDynamicRange`, `basicCorrection/tone/hdrWhite`, `basicCorrection/whiteBalance/temperature` / `tint`, `basicCorrection/saturation`, `basicCorrection/tone/exposure`, `.../contrast`, `highlights`, `shadows`, `whites`, `blacks`, `hdrSpecular`, `creative/lookIntensity`, `creative/adjustments/fadedFilm`, `sharpen`, `vibrance`, `creativeSaturation`, `tintBalance`, `hslSecondary/key/showMask`, `maskStyle`, `invertMask`, `hslSecondary/refine/denoise`, `blur`, `hslSecondary/correction/*`, `vignette/*`; `hslSecondary/key/hue*`, `saturation*`, `lightness*`; `curves/rgbCurves/master`; `curves/hueSaturationCurves/hueVsSat`; `colorWheels/shadowsWheel`, `midtonesWheel`, `highlightsWheel` | sliders: the stored number x 1 (valid maxima 1000, 300, 300, 7, 150, 200, 150, 100, 100, 300, 150, 100, 1000, 5, 100 as in `pard`); checkboxes and the popup as elsewhere, **also from records in the default state** (`tdsb` 3); the key: hue centre x 360 degrees, range x 360 as the half width, tolerance x 360 as the softness, saturation and lightness centre +- range x 100 percent, tolerance x 100, once a colour was picked; the master curve's points as `x,y` (0..1); Hue vs Sat's points with y as `0.5 + y / 2` (After Effects' y 1 doubles the saturation, EffectCraft's y 1 too); a wheel's hue (degrees) and distance as the HSL colour of that hue with saturation = distance and lightness 0.5 | 1200: all 62 read-backs of mapped controls in sections that act equal after import (min, max, sections, popups); the other 61 lie in sections that do nothing and are reported; keys and the expression on HDR White (`-56-keys`, `-57-expr`); the presets `-60-preset-curves-rgb`, `-61-preset-curves-huesat`, `-62-preset-wheels` | - **Nothing to import:** the implicit input-layer parameter (index 0), the Swap Colors buttons (Tint 4, Ramp 8: they always have a record and hold no value), Exposure's twirl-down group @@ -242,8 +242,15 @@ parameter gets the file's value: The Input LUT and the Look are reported by name (LUT files are not in the project). Controls without a counterpart (the Auto tone's Intensity, RGB Curves' HDR Range, Color Wheels' HDR White and Face Detection, Legacy Tone And Saturation, Auto Color, SpeedGrade Custom Layer), the eyedropper - colours, and the RGB curves, colour wheels, Split Toning, HSL correction wheels and moved hue / - saturation curve points (not sampled) are reported when set. Render check at 0.5 s against After + colours, the red, green and blue curves (only the master curve is sampled, so which is which is + not known), the colour wheels' brightness sliders and their fourth wheel, Split Toning, the HSL + correction wheels and moved points of the other four hue / saturation curves (not sampled) are + reported when set. The master RGB curve arrives as its points; Hue vs Sat with y as `0.5 + y / 2` + (After Effects' y is the offset from neutral, 1 doubling the saturation; EffectCraft's y = 1 + doubles it); a colour wheel's hue and distance as the HSL colour of that hue with saturation = + distance, lightness 0.5 (EffectCraft's wheels have no brightness slider). Legacy Tone And + Saturation is reported when a record says on: its `pard` default turns from off to on when the + project is saved again. Render check at 0.5 s against After Effects' 32 bpc PNGs (stored as value x 6553.5 in 16 bits, so read as raw / 6553.5): every composition whose grade After Effects draws as identity (defaults, set-all with every section off, the switched-off sections, the popups, keys, expression) is within 1/255 in EffectCraft; the @@ -253,6 +260,15 @@ parameter gets the file's value: EffectCraft clips at 1 and keeps black at 0; mean 0.12), Creative adjustments (sec-adjustments: Faded Film lifts black to 0.07 in After Effects, 0.035 here) and the extremes (min / max: mean 0.063 / 0.454; max: After Effects' vignette at +5 and the tone sliders give mid grey, EffectCraft white). + The 32 bpc PNGs hold the pixel value with no transfer curve (the defaults' ramp reads x / 320 + exactly), so values above 1 survive up to about 10; the comparison clips After Effects at 1 as + EffectCraft's 8-bit frame does. The hand-made presets: the master RGB curve within 0.024 (mean + 0.009; After Effects' curve is a natural cubic spline through the points, within 0.003, while + EffectCraft's Lumetri interpolates monotone); Hue vs Sat mean 0.008, max 0.18 (After Effects + scales the saturation in linear light, red reaching 1.26; EffectCraft in display values, 8.3 % of + pixels > 2 levels); the colour wheels mean 0.042, max 0.13 (the hues go the same way: Shadows + lift blue at black by 0.13 in After Effects, 0.21 here; Highlights add green; the brightness + sliders are not imported); the Input LUT and the Look are not imported (mean 0.088 / 0.076). - **Colour.** After Effects stores the colour its picker shows as ARGB doubles 0..255 in the project's working-space encoding; EffectCraft's parameter colours are straight RGBA 0..1 that the render pipeline hands to the effect as stored (`crates/render/src/color.rs`: authored colours are @@ -412,5 +428,5 @@ crate yet. ## Not done yet -Lumetri Color's section switches (a switched-off section is reported and its values left out), its LUTs and looks (files After Effects names: reported by name), its RGB curves, colour wheels, Split Toning and HSL correction wheels (not sampled: reported when set) and moved hue / saturation curve points (the scale of y is not sampled); effects outside the table (EXtractoR: its channel choice is arbitrary data; Stroke: its path is a mask reference), environment lights, the ease of bezier Orientation keys, wiggly and expression selectors and the other text animator properties (Character Value, Character Alignment other than its default, the Hue / Saturation / Brightness / Opacity variants, 3D rotations, variable font axes), manual kerning, No Break, the stroke line join, the Japanese leading model, Source Text keyframes, layer-style gradients, Pattern Overlay and the switch state 3, Mesh Warp handles moved on their own, +Lumetri Color's section switches (a switched-off section is reported and its values left out), its LUTs and looks (files After Effects names: reported by name), its red, green and blue curves (no sample tells which is which), the colour wheels' brightness sliders and the fourth wheel, Split Toning and the HSL correction wheels (not sampled: reported when set) and moved points of the four hue / saturation curves other than Hue vs Sat (the scale of y is not sampled); effects outside the table (EXtractoR: its channel choice is arbitrary data; Stroke: its path is a mask reference), environment lights, the ease of bezier Orientation keys, wiggly and expression selectors and the other text animator properties (Character Value, Character Alignment other than its default, the Hue / Saturation / Brightness / Opacity variants, 3D rotations, variable font axes), manual kerning, No Break, the stroke line join, the Japanese leading model, Source Text keyframes, layer-style gradients, Pattern Overlay and the switch state 3, Mesh Warp handles moved on their own, separated dimensions and footage interpretation need verified layouts in the reader first.