AEP import: EXtractoR's and IDentifier's chosen channels by name

Presets of our own (blocks 1300 and 1310) show what the Channel Info stores for a chosen
channel.

- EXtractoR: each record is a kind in byte 0 and the channel name. Named channels are
  imported by name (Z, Z, Z renders equal to After Effects) and `(copy)` as the input's
  own channel. The kind is byte 0 only: one definition default has `02 00 00 32`.
- IDentifier: byte 19 holds the chosen channel's name, `Object ID` when none is chosen.
  After Effects lists only UINT channels and leaves the layer unchanged without one, which
  the loss now says. A chosen `ObjectID` / `MaterialID` channel (any case) is the one
  EffectCraft reads itself, so it imports without a loss; other names are reported.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
vangel76andClaude Opus 5.5 committed 2026-11-05 17:45:19 +01:00
1 parent 77e91199e8
commit 9c25944c70
6 files changed
+149 -43

No files matched your search

+31 -8
View File
@@ -309,10 +309,14 @@ impl MeshData {
}
/// 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.
/// layout; read from the instances of the importer block of 2026-11-05: the defaults and a preset
/// of our own that reads `Z` into Red, Green and Blue): a `u32` 1 and 28 bytes, then four records
/// of 164 bytes (Red, Green, Blue, Alpha), each a kind in byte 0 (1 a channel of the footage by
/// name, 2 the input's own channel: `(copy)`; bytes 1..4 are not always zero, `0x32` at 3 in one
/// definition's default, so only byte 0 is read) and the channel name from byte 35,
/// NUL-terminated. The other bytes are not read (they differ between instances with the same
/// channels; the `u32` at 4 of a chosen channel looks like its position in the footage's sorted
/// channel list, 13 of 14 for `Z`, where the defaults hold 0). Only the name is read.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ExtractorChannels {
/// Per output channel (red, green, blue, alpha): the kind and the name.
@@ -333,7 +337,7 @@ impl ExtractorChannels {
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 kind = u32::from(*rec.first()?);
let name = rec.get(35..)?;
let end = name.iter().position(|b| *b == 0)?;
let name = std::str::from_utf8(name.get(..end)?).ok()?;
@@ -348,11 +352,30 @@ impl ExtractorChannels {
let want = [(1, "R"), (1, "G"), (1, "B"), (2, "(copy)")];
self.channels.iter().zip(want).all(|((k, n), (wk, wn))| *k == wk && n == wn)
}
/// Per output channel (red, green, blue, alpha) the channel it reads, `Err` with the
/// position of the first record of another kind or without a name: a named channel by its
/// name, `(copy)` as the input's own channel of the same position (`R`, `G`, `B`, `A`).
pub fn sources(&self) -> Result<[String; 4], usize> {
const OWN: [&str; 4] = ["R", "G", "B", "A"];
let mut out: [String; 4] = Default::default();
for (k, ((kind, name), slot)) in self.channels.iter().zip(out.iter_mut()).enumerate() {
*slot = match (kind, name.trim()) {
(1, n) if !n.is_empty() => n.to_string(),
(2, _) => OWN.get(k).ok_or(k)?.to_string(),
_ => return Err(k),
};
}
Ok(out)
}
}
/// 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).
/// IDentifier's Channel Info data (`IDentifier-0005`, 148 bytes): the chosen channel's name,
/// NUL-terminated from byte 19, or `Object ID` when none is chosen (importer blocks 1300 and 1310
/// of 2026-11-05: `objectId` in a preset of our own that chose that UINT channel, `Object ID` in
/// every other instance). The bytes around it differ between instances with the same settings
/// and are not read (a chosen channel has 15 at byte 4, which looks like its position in the
/// EXR's sorted channel list, as for EXtractoR).
pub fn identifier_channel_type(data: &[u8]) -> Option<&str> {
let s = data.get(19..)?;
let end = s.iter().position(|b| *b == 0)?;
+2 -2
View File
@@ -138,9 +138,9 @@ pub enum Extra {
Curves,
/// Mesh Warp's Distortion Mesh (965, 985, `MeshData`).
MeshWarp,
/// EXtractoR's Channel Info (`ExtractorChannels`): the default channels only.
/// EXtractoR's Channel Info (`ExtractorChannels`): the channels by name.
Extractor,
/// IDentifier's Channel Info (`identifier_channel_type`): Object ID only.
/// IDentifier's Channel Info (`identifier_channel_type`): the chosen channel by name.
Identifier,
}
+36 -13
View File
@@ -465,44 +465,67 @@ 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")] {
Extra::Extractor => match ExtractorChannels::parse(data).map(|c| (c.sources(), c)) {
// Each output channel reads the footage's channel of the stored name, which
// EffectCraft's EXtractoR finds by the same name (`Z`); After Effects' `(copy)` is the
// input's own channel, EffectCraft's R, G, B or A (the fresh channels are R, G, B, A).
Some((Ok(names), _)) => {
for (id, name) in ["red", "green", "blue", "alpha"].into_iter().zip(names) {
if let Some(prop) = group.prop_mut(id) {
prop.value = Value::Str(name.into());
prop.value = Value::Str(name);
}
}
}
Some(c) => {
Some((Err(k), 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(", ")),
format!(
"{here}: the channels {} are not imported (record {} is of an unknown kind or names no channel), R, G, B, A used",
names.join(", "),
k.saturating_add(1)
),
);
}
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) {
// No channel chosen (the type's label): After Effects lists only the footage's integer
// (UINT) channels and, with none chosen, leaves the layer unchanged, where EffectCraft
// draws the footage's object ID channel.
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"),
format!("{here}: no ID channel is chosen (Channel Info), so After Effects leaves the layer unchanged; EffectCraft draws 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")),
// A chosen channel, by name. EffectCraft's IDentifier has no channel of its own: it
// reads the footage's first `ObjectID` (`MaterialID`) channel, names compared without
// regard to case, so a channel of that name is the one After Effects reads.
Some(name) => match IDENTIFIER_OWN.iter().position(|own| own.eq_ignore_ascii_case(name.trim())) {
Some(kind) => {
if let Some(prop) = group.prop_mut("channelType") {
prop.value = Value::Enum(u32::try_from(kind).unwrap_or(0));
}
}
None => col.lose(
LossCategory::Effects,
format!("{here}: the ID channel '{name}' (Channel Info) is not imported; EffectCraft reads the footage's object ID channel (ObjectID, object_id, objectId, id)"),
),
},
None => col.lose(LossCategory::Effects, format!("{here}: the Channel Info data is not in the sampled layout, the object ID channel used")),
},
Extra::None => {}
}
}
/// The channels EffectCraft's IDentifier reads first for its Channel Type Object ID and Material
/// ID (`AuxChannels::object_id`, `material_id`).
const IDENTIFIER_OWN: [&str; 2] = ["ObjectID", "MaterialID"];
/// Curves' EffectCraft parameters, in the stored order (RGB, Red, Green, Blue, Alpha).
const CURVE_IDS: [&str; 5] = ["rgb", "red", "green", "blue", "alpha"];
@@ -112,6 +112,11 @@ fn extractor_channel_info_layout() {
assert!(!c.is_default());
assert_eq!(c.channels[0], (1, "Z".to_string()));
assert_eq!(ExtractorChannels::parse(&[0u8; 10]), None);
// The kind is byte 0 of a record: the definition default of `extractor-00-defaults` has
// `02 00 00 32` on Alpha, still the input's own channel.
let c = ExtractorChannels::parse(&channels([(1, "R"), (1, "G"), (1, "B"), (0x3200_0002, "(copy)")])).expect("parses");
assert!(c.is_default());
assert_eq!(c.sources(), Ok(["R", "G", "B", "A"].map(String::from)));
}
#[test]
@@ -129,12 +134,32 @@ fn extractor_points_unmult_and_default_channels() {
}
}
/// `extractor-07-preset-channel-z` (a preset of our own): `Z` into Red, Green and Blue, Alpha
/// `(copy)`. EffectCraft's EXtractoR reads the footage's channel by the same name.
#[test]
fn extractor_channels_other_than_the_default_are_reported() {
fn extractor_channels_are_imported_by_name() {
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()));
assert_eq!(losses(&r, LossCategory::Effects, "EXtractoR"), Vec::<String>::new());
let g = g.expect("imported");
for (id, want) in [("red", "Z"), ("green", "Z"), ("blue", "Z"), ("alpha", "A")] {
assert_eq!(v(&g, id), Value::Str(want.into()), "{id}");
}
// `(copy)` on another channel is that channel of the input.
let (g, _) = run(&extractor(vec![], channels([(2, "(copy)"), (1, "CryptoObject00.R"), (2, "(copy)"), (1, "Z")])), "ec.3d.extractor");
let g = g.expect("imported");
for (id, want) in [("red", "R"), ("green", "CryptoObject00.R"), ("blue", "B"), ("alpha", "Z")] {
assert_eq!(v(&g, id), Value::Str(want.into()), "{id}");
}
}
#[test]
fn extractor_channels_of_an_unknown_kind_are_reported() {
for recs in [[(1, "Z"), (3, "Z"), (1, "Z"), (2, "(copy)")], [(1, "Z"), (1, ""), (1, "Z"), (2, "(copy)")]] {
let (g, r) = run(&extractor(vec![], channels(recs)), "ec.3d.extractor");
let found = losses(&r, LossCategory::Effects, "EXtractoR");
assert!(found.len() == 1 && found[0].contains("are not imported (record 2 is of an unknown kind or names no channel)"), "{recs:?}: {found:?}");
assert_eq!(v(&g.expect("imported"), "red"), Value::Str("R".into()), "{recs:?}");
}
}
/// IDentifier's Channel Info: the channel type's name at 19 among bytes that are not read.
@@ -164,16 +189,31 @@ fn identifier(values: Vec<(usize, f64)>, data: Vec<u8>) -> Fx {
#[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).
// `identifier-01-set-all` (ID 10) and `identifier-05-pop2-3` (Alpha Matte): no channel chosen
// (After Effects found none or none was picked), so After Effects leaves the layer unchanged.
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:?}");
assert!(found.len() == 1 && found[0].contains("no ID channel is chosen"), "{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:?}");
}
/// `identifier-09-preset-channel-objectid-id2` (1310, a preset of our own): the UINT channel
/// `objectId` chosen and ID 2. EffectCraft's IDentifier reads that channel itself (`ObjectID`
/// without regard to case), so nothing is lost; another name is reported.
#[test]
fn identifier_chosen_channels_by_name() {
for (name, kind) in [("objectId", 0), ("ObjectID", 0), ("materialid", 1)] {
let (g, r) = run(&identifier(vec![(3, 2.0)], id_info(name)), "ec.3d.identifier");
assert_eq!(losses(&r, LossCategory::Effects, "IDentifier"), Vec::<String>::new(), "{name}");
let g = g.expect("imported");
assert_eq!(v(&g, "channelType"), Value::Enum(kind), "{name}");
near(&g, "id", 2.0);
}
for name in ["IndexOB.X", "ID", "Material ID"] {
let (_, r) = run(&identifier(vec![], id_info(name)), "ec.3d.identifier");
let found = losses(&r, LossCategory::Effects, "IDentifier");
assert!(found.len() == 1 && found[0].contains(&format!("the ID channel '{name}' (Channel Info) is not imported")), "{name}: {found:?}");
}
}
+18 -6
View File
@@ -542,12 +542,24 @@ data layouts, read from default instances only):**
| `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`.
EXtractoR's Channel Info is the plug-in's own **little-endian** layout: `u32` 1 and 28 bytes, then
four 164-byte records (Red, Green, Blue, Alpha), each a kind in byte 0 (1 a channel of the footage by
name: `R`, `G`, `B` by default; 2 the input's own channel, named `(copy)`: Alpha by default) and the
channel name NUL-terminated from record byte 35. The rest is not read: it differs between instances
with the same channels (uninitialised-looking values, also in bytes 1..4 of a record: `02 00 00 32`
on Alpha in one definition's default, which is why only byte 0 is the kind). **A chosen channel**
(`extractor-07-preset-channel-z`, a preset of our own that reads `Z` into Red, Green and Blue) is kind
1 with its name (`Z`), and the `u32` at record byte 4 is then 13, which looks like the channel's
position in the EXR's sorted channel list of 14 (the defaults hold 0 there: H). IDentifier's data
differs between instances with the same settings except for **the chosen channel's name**,
NUL-terminated from byte 19, which is `Object ID` when no channel is chosen. After Effects lists
only integer (UINT) channels: in 1300's EXR (a FLOAT `ID` channel) it found none ("No ID channels
available"), so that block's preset `identifier-09-preset-channel-id` only sets ID 2; in 1310's
EXR (UINT `objectId` and `IndexOB.X` added) the preset `identifier-09-preset-channel-objectid-id2`
stores `objectId`, with 15 at byte 4 (the channel's position in the sorted list of 16, as for
EXtractoR: H). Without a chosen channel After Effects leaves the layer unchanged (1300 and 1310
defaults). 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
+11 -3
View File
@@ -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 |
| 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 |
| 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: each output channel by the footage channel's name, which EffectCraft's EXtractoR reads by the same name (the default R, G, B; `Z` from our preset), and `(copy)` as the input's own channel of that position (EffectCraft's R, G, B or A; the default Alpha is A); a record of another kind or without a name is reported and R, G, B, A used (`ExtractorChannels`), 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 the chosen channel (`identifier_channel_type`): After Effects lists only the footage's integer (UINT) channels, and with none chosen (the data then says `Object ID`) it leaves the layer unchanged, where EffectCraft draws the footage's object ID channel: **reported**; a chosen channel named `ObjectID` or `MaterialID` (without regard to case, `objectId` in our preset) is the one EffectCraft's IDentifier reads itself (Channel Type Object ID or Material ID), any other name is reported (EffectCraft's IDentifier has no channel choice) | `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
@@ -336,8 +336,16 @@ frame blending, 3D material options, audio levels, guides, the mask colour, lock
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).
reports this for every IDentifier. The follow-up presets (our own): `extractor-07-preset-channel-z`
(`Z` into Red, Green and Blue) imports without a loss and renders equal to After Effects (within
3e-5 at After Effects' 1/10 PNG scale; the background's Z of 100 clips there in both). IDentifier
needs an integer channel: block 1310 (`1310-identifier-uint.aep`, the EXR with UINT channels
`objectId` and `IndexOB.X` added) with the preset `identifier-09-preset-channel-objectid-id2`
(`objectId`, ID 2) imports without a loss, and EffectCraft's IDentifier finds the same channel:
the ID regions are the same pixels (8 449, 2 400, 1 515, 1 256, 780), the colours differ (After
Effects draws flat colours per ID and does not highlight the chosen ID, EffectCraft draws its
own ID colours with the chosen ID white). At its defaults (no channel chosen) After Effects
leaves the EXR unchanged in 1310 too.
- `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).