//! Batch 4 (cases 356-383): Tint and Gradient Ramp parameters, their defaults, values, keys and //! scales, the effect stack order, and the second instance of an effect type whose `parT` is //! empty. Every expected value is the one the case was made with (its MANIFEST row). use crate::container::helpers::{assert_near, base_layer, first_comp, open}; use craft_aep::{DefinitionSource, EffectParam, Interpolation, KeyframeKind, ParamKind, ParamValue, Project}; /// Key time 2 s in the property timescale 25600. const T2: i32 = 51200; /// Runs `f` on the project view and the parameters of the single effect of `L`. fn with_params(name: &str, f: impl FnOnce(&Project<'_, '_>, &[EffectParam<'_, '_>]) -> R) -> Option { Some({ let bytes = crate::common::try_fixture(name)?; let file = open(&bytes); let project = Project::new(&file).unwrap(); let layer = base_layer(&first_comp(&project)); let effects = layer.effects(); assert_eq!(effects.len(), 1, "{name}"); // The first instance of a type in the combined file (the case that adds it at its defaults) // keeps its own definitions; the later cases read the project copy. let first = ["356-fx-tint-default.aep", "368-fx-ramp-default.aep", "389-fx-exposure-default.aep"].contains(&name); let want = if first { DefinitionSource::Instance } else { DefinitionSource::Project }; assert_eq!(project.definition_source(&effects[0]), want, "{name}"); let params = project.effect_params(&effects[0]); f(&project, ¶ms) }) } fn argb(v: Option) -> [f64; 4] { match v { Some(ParamValue::Argb(c)) => c, other => panic!("{other:?} is not a colour"), } } fn point(v: Option) -> [f64; 2] { match v { Some(ParamValue::Point(p)) => p, other => panic!("{other:?} is not a point"), } } fn number(v: Option) -> f64 { match v { Some(ParamValue::Number(n)) => n, other => panic!("{other:?} is not a number"), } } /// The colour as the case set it: float32 precision (0.2 becomes 0.20000000298). const F32: f64 = 1e-6; // ---------------------------------------------------------------------------- Tint #[test] fn tint_parameters_and_defaults() { // 356: Tint with every parameter at its default. let bytes = fixture!("356-fx-tint-default.aep"); let file = open(&bytes); let project = Project::new(&file).unwrap(); let fx = base_layer(&first_comp(&project)).effects(); let e = &fx[0]; assert_eq!((e.match_name(), e.name().as_deref()), ("ADBE Tint", Some("Tint"))); assert_eq!(e.declared_param_count(), Some(6)); let p = project.effect_params(e); let kinds: Vec<_> = p.iter().map(|p| (p.kind, p.name.as_str())).collect(); assert_eq!( kinds, [ (ParamKind::Layer, ""), (ParamKind::Color, "Map Black To"), (ParamKind::Color, "Map White To"), (ParamKind::FixedSlider, "Amount to Tint"), (ParamKind::Button, ""), (ParamKind::NoData, ""), ] ); assert_eq!(p[4].match_name, "ADBE Tint-0004"); // The defaults have alpha 0 (what After Effects reads back: [0,0,0,0] / [1,1,1,0]). assert_eq!(p[1].value(), Some(ParamValue::Argb([0.0, 0.0, 0.0, 0.0]))); assert_eq!(p[2].value(), Some(ParamValue::Argb([0.0, 1.0, 1.0, 1.0]))); assert!(p[1].stored().is_none() && p[2].stored().is_none() && p[3].stored().is_none()); // Amount to Tint: 100 percent, stored as a plain percent (valid max 100). assert_eq!(p[3].value(), Some(ParamValue::Number(100.0))); assert_eq!(p[3].valid_max(), Some(100.0)); // Swap Colors: a button whose record always exists and holds no value; its label is in pdnm. assert!(p[4].stored().is_some()); assert_eq!(p[4].value(), None); assert_eq!(p[4].is_default(), None); assert_eq!(p[4].items.as_deref(), Some(&["Swap Colors".to_string()][..])); assert!(p.iter().all(|p| !p.borrowed_definition && !p.is_animated())); } #[test] fn tint_colour_and_amount_values() { // 357: Map Black To [0.2, 0.4, 0.6], stored with alpha 255. skip!(with_params("357-fx-tint-black-0.2-0.4-0.6.aep", |_, p| { assert_near(&argb(p[1].stored_value()), &[1.0, 0.2, 0.4, 0.6], F32); assert_eq!(p[1].is_default(), Some(false)); assert_eq!(p[2].value(), Some(ParamValue::Argb([0.0, 1.0, 1.0, 1.0]))); })); // 361: Amount to Tint 0 writes a record with 0. skip!(with_params("361-fx-tint-amount-0.aep", |_, p| { assert_eq!(p[3].stored_value(), Some(ParamValue::Number(0.0))); assert_eq!(p[3].is_default(), Some(false)); })); } #[test] fn tint_keyframes() { // 363: Amount to Tint keyed 20 at 0 s, 80 at 2 s, linear: plain 48-byte records. skip!(with_params("363-fx-tint-amount-2key.aep", |_, p| { let amount = &p[3]; assert!(amount.is_animated()); assert_eq!(amount.stored_value(), None); let keys: Vec<_> = amount.keys().unwrap().keyframes().collect(); assert_eq!(keys.len(), 2); assert_eq!(keys.iter().map(|k| k.time_units().unwrap()).collect::>(), [0, T2]); assert_eq!(keys.iter().map(|k| k.scalar_value().unwrap()).collect::>(), [20.0, 80.0]); assert!(keys.iter().all(|k| k.kind() == KeyframeKind::Plain { components: 1 } && k.interpolation_out() == Some(Interpolation::Linear))); })); // 364: Map White To keyed [1,1,0] -> [0,0,1]: 152-byte colour records, ARGB 0..255. skip!(with_params("364-fx-tint-white-2key.aep", |_, p| { let white = &p[2]; assert!(white.is_animated()); let keys: Vec<_> = white.keys().unwrap().keyframes().collect(); assert_eq!(keys.len(), 2); assert!(keys.iter().all(|k| k.kind() == KeyframeKind::Spatial { components: 4 } && k.raw().len() == 152)); assert_eq!(keys.iter().map(|k| k.time_units().unwrap()).collect::>(), [0, T2]); assert_near(&keys[0].value().unwrap(), &[255.0, 255.0, 255.0, 0.0], 1e-9); assert_near(&keys[1].value().unwrap(), &[255.0, 0.0, 0.0, 255.0], 1e-9); })); } #[test] fn effect_stack_order_is_the_parade_order() { // 366: Gaussian Blur moved above the Tint. let bytes = fixture!("366-fx-blur-then-tint.aep"); let file = open(&bytes); let project = Project::new(&file).unwrap(); let fx = base_layer(&first_comp(&project)).effects(); assert_eq!(fx.iter().map(|e| e.match_name()).collect::>(), ["ADBE Gaussian Blur 2", "ADBE Tint"]); // Both types first appear in earlier cases of the combined file (214, 356), so both read the // project copy, which holds one copy per type. assert!(fx.iter().all(|e| project.definition_source(e) == DefinitionSource::Project)); let defs: Vec = project.effect_definitions().into_iter().map(|(m, _)| m).collect(); for m in ["ADBE Gaussian Blur 2", "ADBE Tint"] { assert_eq!(defs.iter().filter(|d| *d == m).count(), 1, "{m}"); } } // ---------------------------------------------------------------------------- Gradient Ramp #[test] fn ramp_parameters_and_defaults() { // 368: Gradient Ramp with every parameter at its default. skip!(with_params("368-fx-ramp-default.aep", |_, p| { let kinds: Vec<_> = p.iter().map(|p| (p.kind, p.name.as_str())).collect(); assert_eq!( kinds, [ (ParamKind::Layer, ""), (ParamKind::Point, "Start of Ramp"), (ParamKind::Color, "Start Color"), (ParamKind::Point, "End of Ramp"), (ParamKind::Color, "End Color"), (ParamKind::Popup, "Ramp Shape"), (ParamKind::FixedSlider, "Ramp Scatter"), (ParamKind::FixedSlider, "Blend With Original"), (ParamKind::Button, ""), (ParamKind::NoData, ""), ] ); // Start [960,0] and End [960,1080] on the 1920x1080 layer: fractions of the layer size. assert_eq!(p[1].value(), Some(ParamValue::Point([0.5, 0.0]))); assert_eq!(p[3].value(), Some(ParamValue::Point([0.5, 1.0]))); assert_eq!(p[1].point_px(1920.0, 1080.0), Some([960.0, 0.0])); assert_eq!(p[3].point_px(1920.0, 1080.0), Some([960.0, 1080.0])); // Black to white with alpha 255. assert_eq!(p[2].value(), Some(ParamValue::Argb([1.0, 0.0, 0.0, 0.0]))); assert_eq!(p[4].value(), Some(ParamValue::Argb([1.0, 1.0, 1.0, 1.0]))); // Linear Ramp is item 1 of 2. assert_eq!(p[5].value(), Some(ParamValue::Index(1))); assert_eq!(p[5].popup_item_count(), Some(2)); assert_eq!(p[5].popup_selected_item(), Some("Linear Ramp")); // Scatter 0 (pixels, valid max 512); Blend 0 (a 0..1 fraction, valid max 1). assert_eq!((p[6].value(), p[6].valid_max()), (Some(ParamValue::Number(0.0)), Some(512.0))); assert_eq!((p[7].value(), p[7].valid_max()), (Some(ParamValue::Number(0.0)), Some(1.0))); assert!(p[8].stored().is_some() && p[8].value().is_none()); assert!((1..=7).all(|i| p[i].stored().is_none())); })); } #[test] fn ramp_values() { // 369: Start of Ramp [100, 200] -> [100/1920, 200/1080]. skip!(with_params("369-fx-ramp-start-100-200.aep", |_, p| { assert_near(&point(p[1].stored_value()), &[100.0 / 1920.0, 200.0 / 1080.0], 1e-12); assert_near(&p[1].point_px(1920.0, 1080.0).unwrap(), &[100.0, 200.0], 1e-9); })); // 371: Start Color red. skip!(with_params("371-fx-ramp-startcolor-red.aep", |_, p| { assert_eq!(p[2].stored_value(), Some(ParamValue::Argb([1.0, 1.0, 0.0, 0.0]))); })); // 374: End of Ramp [2000, -100], outside the layer. skip!(with_params("374-fx-ramp-end-2000-m100.aep", |_, p| { assert_near(&p[3].point_px(1920.0, 1080.0).unwrap(), &[2000.0, -100.0], 1e-9); })); // 377: Ramp Shape 2 = Radial Ramp. skip!(with_params("377-fx-ramp-shape-radial.aep", |_, p| { assert_eq!(p[5].stored_value(), Some(ParamValue::Index(2))); assert_eq!(p[5].popup_selected_item(), Some("Radial Ramp")); })); // 378: Ramp Scatter 12 (plain pixels). skip!(with_params("378-fx-ramp-scatter-12.aep", |_, p| assert_eq!(number(p[6].stored_value()), 12.0))); // 380: Blend With Original 25 % is stored as the fraction 0.25. skip!(with_params("380-fx-ramp-blend-25pct.aep", |_, p| { assert_eq!(number(p[7].stored_value()), 0.25); assert_eq!(p[7].valid_max(), Some(1.0)); })); } #[test] fn ramp_point_keyframes() { // 382: Start of Ramp keyed [100,100] at 0 s, [1800,900] at 2 s, linear: 104-byte spatial records. skip!(with_params("382-fx-ramp-start-2key.aep", |_, p| { let start = &p[1]; assert!(start.is_animated()); let keys: Vec<_> = start.keys().unwrap().keyframes().collect(); assert_eq!(keys.len(), 2); assert!(keys.iter().all(|k| k.kind() == KeyframeKind::Spatial { components: 2 } && k.raw().len() == 104)); assert_eq!(keys.iter().map(|k| k.time_units().unwrap()).collect::>(), [0, T2]); assert_near(&keys[0].value().unwrap(), &[100.0 / 1920.0, 100.0 / 1080.0], 1e-12); assert_near(&keys[1].value().unwrap(), &[1800.0 / 1920.0, 900.0 / 1080.0], 1e-12); assert!(keys.iter().all(|k| k.interpolation_in() == Some(Interpolation::Linear))); let s = keys[0].spatial().unwrap(); assert_eq!((s.auto_bezier_flag, s.in_tangent.clone(), s.out_tangent.clone()), (1, vec![0.0; 2], vec![0.0; 2])); })); } #[test] fn second_instance_takes_its_definitions_from_the_project_and_its_values_from_itself() { // 383: L keeps its default Gradient Ramp; the new 100x100 solid S100 above it has a Ramp with // Start of Ramp [10, 20]. One of the two instances has an empty parT. let bytes = fixture!("383-fx-ramp-on-100x100-start-10-20.aep"); let file = open(&bytes); let project = Project::new(&file).unwrap(); let comp = first_comp(&project); let layers: Vec<_> = comp.layers().collect(); assert_eq!(layers.len(), 2); let l = base_layer(&comp); let small = layers.iter().find(|x| x.id() != l.id()).unwrap(); let small_fx = small.effects(); let l_fx = l.effects(); assert_eq!((small_fx.len(), l_fx.len()), (1, 1)); // In the combined file an earlier case holds the type's full definitions, so here both // instances take them from the project copy. assert_eq!(project.definition_source(&small_fx[0]), DefinitionSource::Project); assert_eq!(project.definition_source(&l_fx[0]), DefinitionSource::Project); assert!(l_fx[0].params().is_empty()); assert_eq!(l_fx[0].declared_param_count(), None); // S100: [10,20] px on its own 100x100 size; End of Ramp default [50,100]. let sp = project.effect_params(&small_fx[0]); assert_near(&point(sp[1].stored_value()), &[0.1, 0.2], 1e-12); assert_near(&sp[1].point_px(100.0, 100.0).unwrap(), &[10.0, 20.0], 1e-9); assert_eq!(sp[3].point_px(100.0, 100.0), Some([50.0, 100.0])); // L: the 368 defaults, not the values of the project copy (which is S100's instance). let lp = project.effect_params(&l_fx[0]); assert_eq!(lp.len(), 10); assert!(lp.iter().all(|p| p.borrowed_definition)); assert_eq!(lp[1].name, "Start of Ramp"); assert!(lp[1].stored().is_none()); assert_eq!(lp[1].value(), Some(ParamValue::Point([0.5, 0.0]))); assert_eq!(lp[3].value(), Some(ParamValue::Point([0.5, 1.0]))); assert_eq!(lp[2].value(), Some(ParamValue::Argb([1.0, 0.0, 0.0, 0.0]))); assert_eq!(lp[4].value(), Some(ParamValue::Argb([1.0, 1.0, 1.0, 1.0]))); assert_eq!((number(lp[6].value()), number(lp[7].value())), (0.0, 0.0)); // A borrowed popup has no reliable pard default (a selection writes into that slot). assert_eq!(lp[5].pard_default(), None); assert_eq!(lp[5].items.as_deref().map(<[String]>::len), Some(2)); // L's own records: the input layer (its own id) and the button. assert_eq!(lp[0].stored_value(), Some(ParamValue::Layer(l.id().unwrap()))); assert!(lp[8].stored().is_some()); // One definition for the type, although there are two instances. let defs = project.effect_definitions(); assert_eq!(defs.iter().filter(|(m, _)| m == "ADBE Ramp").count(), 1); } #[test] fn files_without_effects_have_no_definitions() { // The definitions are per file: a file has some exactly when one of its layers has an effect. let mut without = 0; for name in crate::common::FILES.iter().filter(|n| n.ends_with(".aep")) { let bytes = skip!(crate::common::try_file(name)); let file = open(&bytes); let project = Project::new(&file).unwrap(); let has_effects = project.all_items().iter().filter_map(|i| i.comp()).any(|c| c.layers().any(|l| !l.effects().is_empty())); assert_eq!(project.effect_definitions().is_empty(), !has_effects, "{name}"); without += usize::from(!has_effects); } assert!(without > 0); }