.aep import: recognize French names for Levels' Channel and Clip popups (#3432)

* .aep import: recognize French names for Levels' Channel and Clip popups

After Effects stores a popup's items in the UI's own language. A project saved
by a French After Effects writes "RVB", "Activé" and "Désactivé pour la
couleur 32 bpc" for Levels' Channel and Clip to Output Black / White.
EffectCraft's own popup options are English only, so these never matched by
name and the parameter fell back to its default, reported as "has no
counterpart".

Add popup_alias, a small table of confirmed After Effects labels in other
languages, used as a fallback in popup_mapping after the exact name match
fails. A synthetic test reproduces the three losses from a real file and
checks they are gone, with the same values as the English original.

* Cover Levels' French Red, Green and Blue popup items too

popup_alias only mapped "rvb" (RGB), so a French Levels with Red, Green or
Blue selected still lost that selection the same way. Add "rouge", "vert"
and "bleu" (from the same real pdnm list, confirmed against it); "Alpha" is
already the same word in both languages. A direct test on popup_alias covers
every item of the Channel popup it claims to know.

---------

Co-authored-by: sunkidda <sunkidda@users.noreply.github.com>
This commit is contained in:
sunkidda
2026-11-11 17:16:25 +05:30
committed by GitHub
co-authored by sunkidda
parent 367ef1f031
commit 12260b288c
3 changed files with 87 additions and 4 deletions
+22
View File
@@ -1147,6 +1147,28 @@ pub fn popup_key(item: &str) -> String {
s.strip_suffix(" channel").map(str::to_string).unwrap_or(s)
}
/// The English [`popup_key`] a non-English After Effects popup item name stands for, when this
/// table knows it; `None` otherwise. EffectCraft's own popup options are English only, so a
/// project saved by an After Effects set to another UI language never matches them by name and
/// the parameter falls back to its default (observed on a real "Niveaux" project, French After
/// Effects 2026 26.0.0x67: Levels' Channel and Clip to Output Black / White). Add an entry only
/// from a confirmed After Effects label, never a guess at a whole menu's translation.
pub fn popup_alias(key: &str) -> Option<&'static str> {
Some(match key {
// French: Levels' Channel popup (`ADBE Easy Levels2-0001`). "Alpha" is the same word in
// both languages, so it needs no entry.
"rvb" => "rgb",
"rouge" => "red",
"vert" => "green",
"bleu" => "blue",
// French: Levels' Clip to Output Black / White popups (`-0008`, `-0009`).
"activé" => "on",
"désactivé" => "off",
"désactivé pour la couleur 32 bpc" => "off for 32 bpc color",
_ => return None,
})
}
/// The table row of an After Effects match name.
pub fn by_match_name(match_name: &str) -> Option<&'static EffectMap> {
EFFECTS.iter().find(|m| m.match_name == match_name)
+9 -4
View File
@@ -34,7 +34,7 @@
//! import. Hue/Saturation's master values and Levels' values are arbitrary data, decoded by the
//! reader (444-446, 512-516).
use crate::effect_map::{CRYPTOMATTE_OUTPUT, Conv, EFFECTS, EffectMap, Extra, HUE_SATURATION_MASTER, LEVELS_RGB, ParamMap, popup_key};
use crate::effect_map::{CRYPTOMATTE_OUTPUT, Conv, EFFECTS, EffectMap, Extra, HUE_SATURATION_MASTER, LEVELS_RGB, ParamMap, popup_alias, popup_key};
use crate::{
CryptomatteData, CurvesData, DefinitionSource, Effect as AepEffect, EffectDefinitions, EffectParam, ExtractorChannels, HueSaturationData,
Layer as AepLayer, LayerRef, LevelsData, MeshData, ParamKind, ParamValue,
@@ -413,8 +413,8 @@ pub(crate) fn resolve_layer_refs(
}
}
/// The EffectCraft item of each file item (by name, ignoring case and separators); `None` when
/// the parameter here is not a popup.
/// The EffectCraft item of each file item (by name, ignoring case and separators, and a known
/// non-English name for it, [`popup_alias`]); `None` when the parameter here is not a popup.
fn popup_mapping(file_items: &[String], ui: &ParamUi) -> Option<Vec<Option<u32>>> {
let ParamUi::Popup { options } = ui else { return None };
let norm = popup_key;
@@ -425,7 +425,12 @@ fn popup_mapping(file_items: &[String], ui: &ParamUi) -> Option<Vec<Option<u32>>
if item.trim() == "(-" {
return None;
}
options.iter().position(|o| norm(o) == norm(item)).and_then(|i| u32::try_from(i).ok())
let key = norm(item);
options
.iter()
.position(|o| norm(o) == key)
.or_else(|| popup_alias(&key).and_then(|a| options.iter().position(|o| norm(o) == a)))
.and_then(|i| u32::try_from(i).ok())
})
.collect(),
)
+56
View File
@@ -842,3 +842,59 @@ fn a_default_state_colour_record_holds_the_value() {
assert_eq!(colour(&default_state), colour(&written));
assert_ne!(colour(&default_state), colour(&unchanged), "the recorded colour, not the default");
}
/// `SYNTHETIC` with Levels' Channel and Clip to Output Black / White popups' item names (`pdnm`)
/// rewritten to the French labels a French-language After Effects stores there, keeping the same
/// item order so the index each popup is set to is unaffected.
fn with_french_levels_popups() -> Vec<u8> {
let (mut channel, mut clip) = (0, 0);
let bytes = mutate(&mut |c| {
if !c.is(b"Utf8") {
return;
}
if c.bytes() == Some(b"RGB|Red|Green|Blue|Alpha".as_slice()) {
*c = Chunk::data(*b"Utf8", b"RVB|Rouge|Vert|Bleu|Alpha".to_vec());
channel += 1;
} else if c.bytes() == Some(b"On|Off|Off for 32 bpc Color".as_slice()) {
*c = Chunk::data(*b"Utf8", "Activé|Désactivé|Désactivé pour la couleur 32 bpc".as_bytes().to_vec());
clip += 1;
}
});
assert_eq!(channel, 2, "Levels' Channel pdnm");
assert_eq!(clip, 4, "Levels' two Clip pdnm (Black and White), each stored twice");
bytes
}
/// A popup's item names in the file are matched to EffectCraft's own (English) ones by name
/// (`popup_key`): a project saved by a French After Effects stores "RVB", "Activé", "Désactivé
/// pour la couleur 32 bpc" for Levels' Channel and Clip to Output Black / White (observed in a
/// real "Niveaux" effect, After Effects 2026 26.0.0x67). Before `popup_alias`, none of these
/// matched EffectCraft's English-only options, so each came back "has no counterpart, default
/// used" and the real selection was silently lost.
#[test]
fn french_levels_popup_items_are_recognized() {
let (unchanged, r1) = run(SYNTHETIC, true);
let (french, r2) = run(&with_french_levels_popups(), true);
let (r1, r2) = (r1.unwrap(), r2.unwrap());
let found = losses(&r2, LossCategory::Effects);
assert!(!found.iter().any(|d| d.contains("has no counterpart")), "{found:?}");
for id in ["channel", "clipToOutputBlack", "clipToOutputWhite"] {
let want: Vec<_> = effect_values(&unchanged).into_iter().filter(|(p, _)| p == id).collect();
let got: Vec<_> = effect_values(&french).into_iter().filter(|(p, _)| p == id).collect();
assert_eq!(got, want, "{id}");
}
assert_eq!(r1.report.imported.effects, r2.report.imported.effects);
}
/// `popup_alias` must cover every item of a popup it claims to know, not only the one a first
/// report happened to show: Channel's French "Rouge", "Vert", "Bleu" ("Alpha" is the same word in
/// both languages) alongside the already-covered "RVB" (`effect_map.rs`, reviewed on #3432).
#[test]
fn french_levels_channel_aliases_cover_every_item() {
use effectcraft_aep::effect_map::popup_alias;
for (french, english) in [("rvb", "rgb"), ("rouge", "red"), ("vert", "green"), ("bleu", "blue")] {
assert_eq!(popup_alias(french), Some(english), "{french}");
}
// "Alpha" needs no alias: the same word in both languages.
assert_eq!(popup_alias("alpha"), None);
}