AEP import: map CC Color Neutralizer, EXtractoR and IDentifier

The importer reported these three plug-ins as lost effects. They now
become EffectCraft's effects of the same name (parameter lists and data
layouts from our own After Effects 2026 project, docs/aep-format.md 3.7):

- CS Color Neutralizer -> ec.color.cccolorneutralizer: Pinning and Blend
  w. Original. After Effects' correction is the per-range Red / Green /
  Blue sliders (the Unbalance colours are pickers that keep no record),
  plus View and Black / White Point; EffectCraft's effect has none of
  them, so each is reported when stored.
- EXtractoR -> ec.3d.extractor: Black / White Point, UnMult, and the
  default Channel Info (R, G, B, (copy)) as EffectCraft's R, G, B, A
  (ExtractorChannels). Other channels are reported: only the default
  layout is sampled.
- IDentifier -> ec.3d.identifier: Display by item name, ID. Its Channel
  Info names only the channel type (Object ID, identifier_channel_type),
  not the channel After Effects reads; After Effects left the sampled EXR
  unchanged where EffectCraft draws the footage's object ID channel, so
  every instance is reported.

On the sample project no effect is lost any more (29 before); the
parameters above are reported instead.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
vangel76andClaude Opus 5.5 committed 2026-11-05 14:56:55 +01:00
1 parent d10991eff3
commit 77e91199e8
10 files changed
+347 -11

No files matched your search

+1 -1
View File
@@ -36,7 +36,7 @@ mod values;
mod write;
pub use aepx::{is_aepx, parse_any, read_aepx, read_aepx_with_limits};
pub use arbitrary::{CURVE_IDENTITY, CurvesData, HueSaturationData, LevelsData, MeshData, curve_lut};
pub use arbitrary::{CURVE_IDENTITY, CurvesData, ExtractorChannels, HueSaturationData, LevelsData, MeshData, curve_lut, identifier_channel_type};
pub use camera::{CameraPosition, DEFAULT_CAMERA_ZOOM};
pub use chunk::{AepFile, Body, Chunk, LIST, NESTED_IDS, Walk};
pub use effects::{DefinitionSource, Effect, EffectDefinitions, EffectParam, LayerRef, ParamKind, ParamValue};
+51
View File
@@ -307,3 +307,54 @@ impl MeshData {
Some(out)
}
}
/// EXtractoR's Channel Info data (`EXtractoR-0013`, 688 bytes, the plug-in's own little-endian
/// layout; read from the default instances of the importer block of 2026-11-05, where no channel
/// was changed): a `u32` 1 and 28 bytes, then four records of 164 bytes (Red, Green, Blue, Alpha),
/// each a `u32` kind (1 a named channel, 2 the input's channel: `(copy)` on Alpha) and the channel
/// name from byte 35, NUL-terminated. Only the default (`R`, `G`, `B`, `(copy)`) is sampled.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ExtractorChannels {
/// Per output channel (red, green, blue, alpha): the kind and the name.
pub channels: [(u32, String); 4],
}
impl ExtractorChannels {
/// The data's length.
pub const LEN: usize = 688;
const RECORD: usize = 164;
/// `None` when the data does not have this layout.
pub fn parse(data: &[u8]) -> Option<Self> {
if data.len() != Self::LEN || data.get(..4)? != 1u32.to_le_bytes() {
return None;
}
let mut channels: [(u32, String); 4] = Default::default();
for (k, slot) in channels.iter_mut().enumerate() {
let at = 32usize.checked_add(k.checked_mul(Self::RECORD)?)?;
let rec = data.get(at..at.checked_add(Self::RECORD)?)?;
let kind = u32::from_le_bytes(*rec.first_chunk::<4>()?);
let name = rec.get(35..)?;
let end = name.iter().position(|b| *b == 0)?;
let name = std::str::from_utf8(name.get(..end)?).ok()?;
*slot = (kind, name.to_string());
}
Some(Self { channels })
}
/// The channels of a fresh EXtractoR: Red, Green and Blue from the channels of those names,
/// Alpha copied from the input.
pub fn is_default(&self) -> bool {
let want = [(1, "R"), (1, "G"), (1, "B"), (2, "(copy)")];
self.channels.iter().zip(want).all(|((k, n), (wk, wn))| *k == wk && n == wn)
}
}
/// IDentifier's Channel Info data (`IDentifier-0005`, 148 bytes): the channel type's name,
/// NUL-terminated from byte 19 (`Object ID` in every sample of the importer block of 2026-11-05;
/// the bytes around it differ between instances with the same settings and are not read).
pub fn identifier_channel_type(data: &[u8]) -> Option<&str> {
let s = data.get(19..)?;
let end = s.iter().position(|b| *b == 0)?;
std::str::from_utf8(s.get(..end)?).ok()
}
+42
View File
@@ -138,6 +138,10 @@ pub enum Extra {
Curves,
/// Mesh Warp's Distortion Mesh (965, 985, `MeshData`).
MeshWarp,
/// EXtractoR's Channel Info (`ExtractorChannels`): the default channels only.
Extractor,
/// IDentifier's Channel Info (`identifier_channel_type`): Object ID only.
Identifier,
}
/// One mapped effect.
@@ -354,6 +358,8 @@ pub const SHUTTER_CONTROL: &[&str] = &["Manual", "Automatic"];
pub const ECHO_OPERATOR: &[&str] = &["Add", "Maximum", "Minimum", "Screen", "Composite In Back", "Composite In Front", "Blend"];
/// Color Stabilizer's Stabilize (`colstab-02`, `colstab-03`; the fresh default is Levels).
pub const STABILIZE: &[&str] = &["Brightness", "Levels", "Curves"];
/// IDentifier's Display (`identifier-04` ... `identifier-06`, `1300-thirdparty.aep`).
pub const IDENTIFIER_DISPLAY: &[&str] = &["Colors", "Luma Matte", "Alpha Matte", "Raw"];
/// The effects the importer converts.
pub const EFFECTS: &[EffectMap] = &[
@@ -1066,6 +1072,40 @@ pub const EFFECTS: &[EffectMap] = &[
pm(5, K::FixedSlider, "sampleSize", Conv::Plain { valid_max: 50.0 }),
],
),
// CC Color Neutralizer (importer block 2026-11-05, `ccneutral-*`): Pinning and Blend w.
// Original are float sliders in percent (valid 0..100). After Effects' correction is the Red /
// Green / Blue slider of each range (3-5, 9-11, 15-17; the Unbalance colours are pickers that
// set them and keep no record), then View (22) and Black / White Point (23, 24):
// EffectCraft's CC Color Neutralizer has none of them, so they are reported when stored.
fx(
"CS Color Neutralizer",
"ec.color.cccolorneutralizer",
&[pm(19, K::FloatSlider, "pinning", Conv::Plain { valid_max: 100.0 }), pm(20, K::FloatSlider, "blendWOriginal", Conv::Plain { valid_max: 100.0 })],
),
// EXtractoR (`extractor-*`): 1 Channel Info (arbitrary data, `Extra::Extractor`), 2 / 6 the
// Process group, Black and White Point float sliders (valid ±3.4e38, the float32 range),
// UnMult. EffectCraft's Clip has no counterpart and stays off.
EffectMap {
match_name: "EXtractoR",
effect: "ec.3d.extractor",
params: &[
pm(3, K::FloatSlider, "blackPoint", Conv::Plain { valid_max: F32_MAX }),
pm(4, K::FloatSlider, "whitePoint", Conv::Plain { valid_max: F32_MAX }),
pm(5, K::Checkbox, "unMult", Conv::Checkbox),
],
extra: Extra::Extractor,
},
// IDentifier (`identifier-*`): 1 Channel Info (arbitrary data, `Extra::Identifier`), 2 Display,
// 3 ID an integer slider (valid 0..2 147 483 647).
EffectMap {
match_name: "IDentifier",
effect: "ec.3d.identifier",
params: &[
pm(2, K::Popup, "display", Conv::Popup { default: 1, items: IDENTIFIER_DISPLAY }),
pm(3, K::IntSlider, "id", Conv::Plain { valid_max: 2_147_483_647.0 }),
],
extra: Extra::Identifier,
},
// Layer Control (076, 220, 320, 321): the default is the owning layer, None is unset.
fx("ADBE Layer Control", "ec.control.layer", &[pm(1, K::Layer, "layer", Conv::LayerRef { own: true })]),
fx("ADBE Slider Control", "ec.control.slider", &[pm(1, K::FloatSlider, "slider", Conv::Plain { valid_max: 1_000_000.0 })]),
@@ -1079,6 +1119,8 @@ pub const EFFECTS: &[EffectMap] = &[
pub const EXPOSURE: Conv = Conv::Plain { valid_max: 100.0 };
pub const OFFSET: Conv = Conv::Plain { valid_max: 2.0 };
pub const GAMMA: Conv = Conv::Plain { valid_max: 10.0 };
/// The largest float32, the valid maximum of EXtractoR's Black and White Point.
pub const F32_MAX: f64 = f32::MAX as f64;
/// The valid maximum of Glow's A & B Midpoint (16.16 of 0.99, 427).
pub const GLOW_MIDPOINT_MAX: f64 = 0.990_005_493_164_062_5;
+36 -2
View File
@@ -36,8 +36,8 @@
use crate::effect_map::{Conv, EFFECTS, EffectMap, Extra, HUE_SATURATION_MASTER, LEVELS_RGB, popup_key};
use crate::{
CurvesData, DefinitionSource, Effect as AepEffect, EffectDefinitions, EffectParam, HueSaturationData, Layer as AepLayer, LayerRef, LevelsData, MeshData,
ParamKind, ParamValue,
CurvesData, DefinitionSource, Effect as AepEffect, EffectDefinitions, EffectParam, ExtractorChannels, HueSaturationData, Layer as AepLayer, LayerRef,
LevelsData, MeshData, ParamKind, ParamValue,
};
use effectcraft_keyframe::Value;
use effectcraft_project::build::Ids;
@@ -465,6 +465,40 @@ fn import_extra(fx: &AepEffect<'_, '_>, extra: Extra, p: &EffectParam<'_, '_>, h
Some(m) => import_mesh(&m, here, size, group, col),
None => col.lose(LossCategory::DamagedData, format!("{here}: the Distortion Mesh is unreadable, defaults used")),
},
Extra::Extractor => match ExtractorChannels::parse(data) {
// The fresh channels: EffectCraft's own R, G, B, A (its A is the input's alpha, After
// Effects' `(copy)`).
Some(c) if c.is_default() => {
for (id, name) in [("red", "R"), ("green", "G"), ("blue", "B"), ("alpha", "A")] {
if let Some(prop) = group.prop_mut(id) {
prop.value = Value::Str(name.into());
}
}
}
Some(c) => {
let names: Vec<&str> = c.channels.iter().map(|(_, n)| n.as_str()).collect();
col.lose(
LossCategory::Effects,
format!("{here}: the channels {} are not imported (only the default channels are sampled), R, G, B, A used", names.join(", ")),
);
}
None => col.lose(LossCategory::Effects, format!("{here}: the Channel Info data is not in the sampled layout, R, G, B, A used")),
},
Extra::Identifier => match crate::identifier_channel_type(data) {
Some("Object ID") => {
if let Some(prop) = group.prop_mut("channelType") {
prop.value = Value::Enum(0);
}
// The data names the type, not the channel After Effects reads: with this Channel
// Info After Effects left the sampled EXR unchanged, where EffectCraft draws the
// footage's object ID channel.
col.lose(
LossCategory::Effects,
format!("{here}: the channel IDentifier reads (Channel Info) is not decoded; EffectCraft uses the footage's object ID channel"),
);
}
other => col.lose(LossCategory::Effects, format!("{here}: the channel type {other:?} is not imported (only Object ID is sampled), Object ID used")),
},
Extra::None => {}
}
}
+7 -7
View File
@@ -14,11 +14,11 @@ pub mod import;
pub use container::{
AepError, AepFile, AutoKern, BlendMode, Body, CURVE_IDENTITY, CameraPosition, Caps, Chunk, Comp, CompHeader, CueType, CurvesData, DEFAULT_CAMERA_ZOOM,
DefinitionSource, Effect, EffectDefinitions, EffectParam, ExpressionFlags, FontBaseline, Footage, GradientType, HueSaturationData, Interpolation, Item,
ItemKind, Justification, KernRun, KeyEase, KeyLayout, KeyList, Keyframe, KeyframeKind, LIST, Layer, LayerHeader, LayerRef, LayerType, LevelsData,
LightType, Limits, ListHeader, Marker, Mask, MaskAtom, MaskMode, MatteMode, MeshData, NESTED_IDS, Orientation, OrientationKey, ParagraphRun, ParamKind,
ParamValue, PathVertex, PolystarType, Project, ProjectSettings, PropEntry, PropGroup, PropNode, Property, Quality, Rational, Renderer, SequenceInfo,
ShapeDetail, ShapeGroup, ShapeItem, ShapePath, SpatialKey, Stroke, StrokeCap, StrokeJoin, StyleRun, TextBox, TextDocument, TextValue, TimeBase,
TimeDisplay, TimeRemap, TimeRemapKey, TrackMatte, Tracked, Transform, ValueSource, Walk, WorkingSpace, curve_lut, effect_map, is_aepx, parse, parse_any,
parse_text_dict, parse_with_limits, read_aepx, read_aepx_with_limits, style_map, write, write_to,
DefinitionSource, Effect, EffectDefinitions, EffectParam, ExpressionFlags, ExtractorChannels, FontBaseline, Footage, GradientType, HueSaturationData,
Interpolation, Item, ItemKind, Justification, KernRun, KeyEase, KeyLayout, KeyList, Keyframe, KeyframeKind, LIST, Layer, LayerHeader, LayerRef, LayerType,
LevelsData, LightType, Limits, ListHeader, Marker, Mask, MaskAtom, MaskMode, MatteMode, MeshData, NESTED_IDS, Orientation, OrientationKey, ParagraphRun,
ParamKind, ParamValue, PathVertex, PolystarType, Project, ProjectSettings, PropEntry, PropGroup, PropNode, Property, Quality, Rational, Renderer,
SequenceInfo, ShapeDetail, ShapeGroup, ShapeItem, ShapePath, SpatialKey, Stroke, StrokeCap, StrokeJoin, StyleRun, TextBox, TextDocument, TextValue,
TimeBase, TimeDisplay, TimeRemap, TimeRemapKey, TrackMatte, Tracked, Transform, ValueSource, Walk, WorkingSpace, curve_lut, effect_map,
identifier_channel_type, is_aepx, parse, parse_any, parse_text_dict, parse_with_limits, read_aepx, read_aepx_with_limits, style_map, write, write_to,
};
+1
View File
@@ -14,6 +14,7 @@ mod effects_batch5;
mod effects_quickwins;
mod effects_round2;
mod effects_round3;
mod effects_thirdparty;
mod fx_inject;
mod markers;
mod masks;
+1 -1
View File
@@ -72,7 +72,7 @@ fn the_table_names_tint_gradient_ramp_and_exposure() {
] {
assert!(t.contains(&want), "{want:?}");
}
assert_eq!(t.len(), 62);
assert_eq!(t.len(), 65);
for (_, id) in t {
assert!(effectcraft_effects::find(id).is_some(), "{id}");
}
@@ -0,0 +1,179 @@
//! CC Color Neutralizer, EXtractoR and IDentifier (#3112) arrive as EffectCraft's effects of the
//! same name. Each is built into `synthetic.aep` (`fx_inject`) with the definitions After Effects
//! 2026 wrote for it in the importer block of 2026-11-05 (`1300-thirdparty.aep`) and the values
//! of that block's compositions; the Channel Info data is laid out as those instances store it.
use crate::import::fx_inject::{Default, Fx, losses, near, other, popup, run, slider, v};
use effectcraft_aep::import::LossCategory;
use effectcraft_aep::{ExtractorChannels, ParamKind as K, identifier_channel_type};
use effectcraft_keyframe::Value;
fn neutralizer(values: Vec<(usize, f64)>) -> Fx {
let range = |name| slider(K::FloatSlider, name, 0.0, (-100.0, 100.0));
let group = |name| other(K::GroupStart, name, Default::None);
let end = || other(K::GroupEnd, "", Default::None);
Fx {
match_name: "CS Color Neutralizer",
name: "CC Color Neutralizer",
defs: vec![
other(K::Color, "Shadows Unbalance", Default::Color([0, 0, 0, 0])),
group("Shadows"),
range("Red - Shadows"),
range("Green - Shadows"),
range("Blue - Shadows"),
end(),
other(K::Color, "Midtones Unbalance", Default::Color([255, 127, 127, 127])),
group("Midtones"),
range("Red - Midtones"),
range("Green - Midtones"),
range("Blue - Midtones"),
end(),
other(K::Color, "Highlights Unbalance", Default::Color([255, 255, 255, 255])),
group("Highlights"),
range("Red - Highlights"),
range("Green - Highlights"),
range("Blue - Highlights"),
end(),
slider(K::FloatSlider, "Pinning", 0.0, (0.0, 100.0)),
slider(K::FloatSlider, "Blend w. Original", 0.0, (0.0, 100.0)),
group("Special"),
popup("View", 1, &["Final Result", "Correction B to W", "Correction Stair"]),
slider(K::FloatSlider, "Black Point", 0.0, (0.0, 49.9)),
slider(K::FloatSlider, "White Point", 100.0, (50.1, 100.0)),
end(),
],
values,
data: vec![],
}
}
#[test]
fn cc_color_neutralizer_pinning_and_blend() {
// `ccneutral-00-defaults` and the Pinning / Blend of `ccneutral-01-set-all`.
let (g, r) = run(&neutralizer(vec![(19, 16.0), (20, 19.0)]), "ec.color.cccolorneutralizer");
assert_eq!(losses(&r, LossCategory::Effects, "CS Color Neutralizer"), Vec::<String>::new());
let g = g.expect("imported");
near(&g, "pinning", 16.0);
near(&g, "blendWOriginal", 19.0);
// EffectCraft's own (identity) balance stays.
assert_eq!(v(&g, "midtonesUnbalance"), v(&g, "midtonesBalance"));
}
#[test]
fn cc_color_neutralizer_corrections_without_a_counterpart_are_reported() {
// `ccneutral-03-sec-shadows`, `ccneutral-09-pop22-2`, `ccneutral-06-sec-special`.
let (g, r) = run(&neutralizer(vec![(3, 13.0), (4, 16.0), (5, 19.0), (22, 2.0), (23, 10.0), (24, 87.0)]), "ec.color.cccolorneutralizer");
let found = losses(&r, LossCategory::Effects, "CS Color Neutralizer");
for name in ["Red - Shadows", "Green - Shadows", "Blue - Shadows", "View", "Black Point", "White Point"] {
assert!(found.iter().any(|d| d.contains(&format!("parameter {name} (")) && d.contains("not imported")), "{name}: {found:?}");
}
assert_eq!(found.len(), 6, "{found:?}");
assert!(g.is_some(), "the effect itself is imported");
}
/// EXtractoR's Channel Info data with these record kinds and names.
fn channels(recs: [(u32, &str); 4]) -> Vec<u8> {
let mut d = vec![0u8; ExtractorChannels::LEN];
d[..4].copy_from_slice(&1u32.to_le_bytes());
for (k, (kind, name)) in recs.iter().enumerate() {
let at = 32 + k * 164;
d[at..at + 4].copy_from_slice(&kind.to_le_bytes());
d[at + 35..at + 35 + name.len()].copy_from_slice(name.as_bytes());
}
d
}
const DEFAULT_CHANNELS: [(u32, &str); 4] = [(1, "R"), (1, "G"), (1, "B"), (2, "(copy)")];
fn extractor(values: Vec<(usize, f64)>, data: Vec<u8>) -> Fx {
let mut values = values;
values.insert(0, (1, 0.0));
Fx {
match_name: "EXtractoR",
name: "EXtractoR",
defs: vec![
other(K::ArbitraryData, "Channel Info", Default::None),
other(K::GroupStart, "Process", Default::None),
slider(K::FloatSlider, "Black Point", 0.0, (-f64::from(f32::MAX), f64::from(f32::MAX))),
slider(K::FloatSlider, "White Point", 1.0, (-f64::from(f32::MAX), f64::from(f32::MAX))),
other(K::Checkbox, "", Default::Number(0.0)),
other(K::GroupEnd, "", Default::None),
],
values,
data: vec![(1, data)],
}
}
#[test]
fn extractor_channel_info_layout() {
let c = ExtractorChannels::parse(&channels(DEFAULT_CHANNELS)).expect("the default layout");
assert!(c.is_default());
let c = ExtractorChannels::parse(&channels([(1, "Z"), (1, "G"), (1, "B"), (2, "(copy)")])).expect("parses");
assert!(!c.is_default());
assert_eq!(c.channels[0], (1, "Z".to_string()));
assert_eq!(ExtractorChannels::parse(&[0u8; 10]), None);
}
#[test]
fn extractor_points_unmult_and_default_channels() {
// `extractor-01-set-all`: Black Point 10, White Point 14, UnMult on.
let (g, r) = run(&extractor(vec![(3, 10.0), (4, 14.0), (5, 1.0)], channels(DEFAULT_CHANNELS)), "ec.3d.extractor");
assert_eq!(losses(&r, LossCategory::Effects, "EXtractoR"), Vec::<String>::new());
let g = g.expect("imported");
near(&g, "blackPoint", 10.0);
near(&g, "whitePoint", 14.0);
assert_eq!((v(&g, "unMult"), v(&g, "clip")), (Value::Bool(true), Value::Bool(false)));
// After Effects' `(copy)` Alpha is the input's alpha: EffectCraft's A.
for (id, want) in [("red", "R"), ("green", "G"), ("blue", "B"), ("alpha", "A")] {
assert_eq!(v(&g, id), Value::Str(want.into()), "{id}");
}
}
#[test]
fn extractor_channels_other_than_the_default_are_reported() {
let (g, r) = run(&extractor(vec![], channels([(1, "Z"), (1, "Z"), (1, "Z"), (2, "(copy)")])), "ec.3d.extractor");
let found = losses(&r, LossCategory::Effects, "EXtractoR");
assert!(found.len() == 1 && found[0].contains("the channels Z, Z, Z, (copy) are not imported"), "{found:?}");
assert_eq!(v(&g.expect("imported"), "red"), Value::Str("R".into()));
}
/// IDentifier's Channel Info: the channel type's name at 19 among bytes that are not read.
fn id_info(name: &str) -> Vec<u8> {
let mut d: Vec<u8> = (0..148u32).map(|i| (i * 37 % 251) as u8 | 1).collect();
d[19..19 + name.len()].copy_from_slice(name.as_bytes());
d[19 + name.len()] = 0;
d
}
fn identifier(values: Vec<(usize, f64)>, data: Vec<u8>) -> Fx {
let mut values = values;
values.insert(0, (1, 0.0));
Fx {
match_name: "IDentifier",
name: "IDentifier",
defs: vec![
other(K::ArbitraryData, "Channel Info", Default::None),
popup("Display", 1, &["Colors", "Luma Matte", "Alpha Matte", "Raw"]),
slider(K::IntSlider, "ID", 0.0, (0.0, 2_147_483_647.0)),
],
values,
data: vec![(1, data)],
}
}
#[test]
fn identifier_display_id_and_channel_type() {
assert_eq!(identifier_channel_type(&id_info("Object ID")), Some("Object ID"));
// `identifier-01-set-all` (ID 10) and `identifier-05-pop2-3` (Alpha Matte).
let (g, r) = run(&identifier(vec![(2, 3.0), (3, 10.0)], id_info("Object ID")), "ec.3d.identifier");
// Which channel After Effects reads is not decoded (it left the sampled EXR unchanged): reported.
let found = losses(&r, LossCategory::Effects, "IDentifier");
assert!(found.len() == 1 && found[0].contains("is not decoded"), "{found:?}");
let g = g.expect("imported");
assert_eq!((v(&g, "display"), v(&g, "channelType")), (Value::Enum(2), Value::Enum(0)));
near(&g, "id", 10.0);
// A channel type that is not sampled is reported.
let (_, r) = run(&identifier(vec![], id_info("Material ID")), "ec.3d.identifier");
let found = losses(&r, LossCategory::Effects, "IDentifier");
assert!(found.len() == 1 && found[0].contains("\"Material ID\""), "{found:?}");
}
+16
View File
@@ -533,6 +533,22 @@ compositions, the items from one composition per popup item).** Each set value i
| `ADBE Offset` | 1 Shift Center To: point, default the layer centre; 2 Blend With Original: fixed, **max 1 (a 0..1 fraction)**, default 0 |
| `ADBE Deflicker` (Color Stabilizer) | 1 Stabilize: popup (Brightness, Levels, Curves), **default 2 (Levels)**; 2 Black Point, 3 Mid Point, 4 White Point: points, defaults 0.2 / 0.5 / 0.8 of the width at half the height; 5 Sample Size: fixed, max 50, default 3. The reference frame the Set Frame button picks is not among the parameters |
**Importer block 2026-11-05, #3112 (`1300-thirdparty.aep`; V for the parameter lists, H for the
data layouts, read from default instances only):**
| Effect (match name, plug-in version) | Parameters (`parT` index: type, valid range, default) |
|---|---|
| `CS Color Neutralizer` (CC Color Neutralizer, 1.3) | 1 / 7 / 13 Shadows / Midtones / Highlights Unbalance: colours (black, 127 grey, white; pickers: setting one sets the range's sliders, e.g. [0.27, 0.55, 0.85] gives Red 42.94, Green 1.18, Blue -44.12 = (mean - channel) x 150 / 255 on 8-bit values, and leaves no colour record); 2-6, 8-12, 14-18 a group per range (start, Red, Green, Blue, end) with float sliders -100..100 (slider -50..50), default 0; 19 Pinning: float 0..100 (slider 0..50); 20 Blend w. Original: float 0..100; 21-25 the Special group: 22 View popup (Final Result, Correction B to W, Correction Stair), default 1, 23 Black Point float 0..49.9, default 0, 24 White Point float 50.1..100, default 100 |
| `EXtractoR` (2.6.6) | 1 Channel Info (`EXtractoR-0013`): arbitrary data, 688 bytes, below; 2-6 the Process group: 3 Black Point and 4 White Point float sliders, valid ±3.4028235e38 (the float32 range; slider ±1 000), default 0 / 1; 5 UnMult: checkbox (empty `pard` name, the label in `pdnm`), default off |
| `IDentifier` (2.6.6) | 1 Channel Info (`IDentifier-0005`): arbitrary data, 148 bytes, below; 2 Display: popup (Colors, Luma Matte, Alpha Matte, Raw), default 1; 3 ID: integer slider, max 2 147 483 647, default 0 |
EXtractoR's Channel Info is the plug-in's own **little-endian** layout: `u32` 1 and 28 zero bytes,
then four 164-byte records (Red, Green, Blue, Alpha), each a `u32` kind (1 in R, G, B; 2 in Alpha,
whose name is `(copy)`) and the channel name NUL-terminated from record byte 35 (`R`, `G`, `B`).
The data is the same in every composition of the block. IDentifier's data differs between
instances with the same settings except for the channel type's name, NUL-terminated from byte 19
(`Object ID`); the other bytes are not read. API: `ExtractorChannels`, `identifier_channel_type`.
**Curves and Mesh Warp** (961-965, 985; applied from presets, the data is not scriptable): their
arbitrary-data parameter (type 11) always has a record (`tdbs` with a 4-byte `cdat`) and a `LIST
aRbs` / `aRbp`, also at the default. Curves (`ADBE CurvesCustom-0001`, 1644 bytes): `u16` 1, 1;
+13
View File
@@ -179,6 +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 |
| Importer block 2026-11-05 (`1100-quickwins.aep`: per effect defaults, set values, minimum and maximum, every popup item, a keyed and an expression parameter): `ADBE HDR ToneMap` (HDR Highlight Compression) 1 Amount; `ADBE Echo` 1 Echo Time, 2 Number Of Echoes, 3 Starting Intensity, 4 Decay, 5 Echo Operator; `ADBE Linear Wipe` 1 Transition Completion, 2 Wipe Angle, 3 Feather; `ADBE Offset` 1 Shift Center To, 2 Blend With Original; `ADBE Deflicker` (Color Stabilizer) 1 Stabilize, 2-4 Black / Mid / White Point, 5 Sample Size | `ec.utility.hdrcompression` `amount`; `ec.time.echo` `echoTime`, `numberOfEchoes`, `startingIntensity`, `decay`, `echoOperator`; `ec.transition.linearwipe` `completion`, `angle`, `feather`; `ec.distort.offset` `shift`, `blend`; `ec.color.colorstabilizer` `stabilize`, `blackPoint`, `midPoint`, `whitePoint`, `sampleSize` | percent (float, valid 0..100, default 100); seconds (float, valid ±30 000), integer (valid 30 000), 0..1 (float), Decay as stored (float, valid 0..100), popup by name: EffectCraft's Echo uses at most 255 echoes and a Decay up to 1, a value or key beyond that is imported as stored and **reported**; percent (fixed, 100), degrees, pixels (fixed, 32 000); point in layer pixels, Blend 0..1 x 100 = percent; popup by name (**fresh default Levels**), points in layer pixels, Sample Size as stored (fixed, valid 50, default 3; its unit, pixels here, is not verified). Color Stabilizer's reference frame (Set Frame) is not a parameter in the file and is not imported (EffectCraft's Reference Frame stays 0 s) | `tests/it/import/effects_quickwins.rs`; render comparison below |
| Importer block 2026-11-05 (`1300-thirdparty.aep`), plug-ins that ship with After Effects: `CS Color Neutralizer` (CC Color Neutralizer) 19 Pinning, 20 Blend w. Original; `EXtractoR` 1 Channel Info data, 3 Black Point, 4 White Point, 5 UnMult; `IDentifier` 1 Channel Info data, 2 Display, 3 ID | `ec.color.cccolorneutralizer` `pinning`, `blendWOriginal`; `ec.3d.extractor` `red`, `green`, `blue`, `alpha`, `blackPoint`, `whitePoint`, `unMult`; `ec.3d.identifier` `channelType`, `display`, `id` | CC Color Neutralizer: percent (float, valid 100); its correction is the Red / Green / Blue slider of each range (the Unbalance colours are pickers that set them and keep no record), with View and Black / White Point: **EffectCraft's effect has no counterpart, each is reported when stored** (EffectCraft's Unbalance / Balance stay at their identity defaults). EXtractoR: the default channels (R, G, B, `(copy)`) are EffectCraft's R, G, B, A, other channels are reported (`ExtractorChannels`; only the default is sampled), Black and White Point as stored (float, valid ±3.4e38; a value beyond ±1e9 is reported as damaged data, as for every number), UnMult a checkbox, Clip stays off. IDentifier: Display by name, ID as stored (integer, valid 2 147 483 647); the Channel Info names only the channel type (Object ID, `identifier_channel_type`; another is reported), not the channel After Effects reads, so **every instance is reported**: EffectCraft draws the footage's object ID channel where After Effects left the sampled EXR unchanged | `tests/it/import/effects_thirdparty.rs`; render comparison below |
| `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 |
- **Nothing to import:** the implicit input-layer parameter (index 0), the Swap Colors buttons
@@ -325,6 +326,18 @@ frame blending, 3D material options, audio levels, guides, the mask colour, lock
over 25 (EffectCraft's samples drift, After Effects' frame stays unchanged) and with keyed
sample points. The renders do not exercise a Decay above 1 (After Effects' set value has
Starting Intensity 0) or what Color Stabilizer corrects (the test card does not flicker).
- `crates/aep/tests/it/import/effects_thirdparty.rs` (#3112): CC Color Neutralizer, EXtractoR and
IDentifier built into `synthetic.aep` like the #3110 effects. Render check of the 29 compositions
of `1300-thirdparty.aep` (as above): CC Color Neutralizer at its defaults and the minimum and
maximum compositions match within one level (no composition isolates Pinning: the maximum has
Blend w. Original 100, which shows the original), the compositions with reported Red /
Green / Blue corrections or View differ as reported (up to 5 levels, View 25); EXtractoR at its
defaults and with UnMult within 3 levels on 6 % of the pixels, keyed and expression Black Point
within one level, the ±3.4e38 compositions differ (reported as damaged data). IDentifier: After
Effects' frame is the unchanged EXR in every variant (its default Object ID finds no channel in
that EXR), while EffectCraft's IDentifier finds the EXR's `ID` channel and draws it; the import
reports this for every IDentifier. No sample changes EXtractoR's channels or IDentifier's channel
type (the presets for that were missing from the block).
- `crates/aep/tests/it/import/combined.rs`: masks and effects on the same layer, an effect on a text
layer, and the same through `.aepx` (built in-test from the fixtures: the effect parade of one
fixture grafted onto the layer of another).