From 12260b288ca6b6e43e1904e468fc81a2c6cc618f Mon Sep 17 00:00:00 2001 From: sunkidda Date: Wed, 11 Nov 2026 12:46:25 +0100 Subject: [PATCH] .aep import: recognize French names for Levels' Channel and Clip popups (#3432) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * .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 --- crates/aep/src/container/effect_map.rs | 22 ++++++++++ crates/aep/src/import/effects.rs | 13 ++++-- crates/aep/tests/it/import/synthetic.rs | 56 +++++++++++++++++++++++++ 3 files changed, 87 insertions(+), 4 deletions(-) diff --git a/crates/aep/src/container/effect_map.rs b/crates/aep/src/container/effect_map.rs index aea320395..06b80cc3a 100644 --- a/crates/aep/src/container/effect_map.rs +++ b/crates/aep/src/container/effect_map.rs @@ -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) diff --git a/crates/aep/src/import/effects.rs b/crates/aep/src/import/effects.rs index 1ab4b208a..a09ef37b6 100644 --- a/crates/aep/src/import/effects.rs +++ b/crates/aep/src/import/effects.rs @@ -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>> { 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> 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(), ) diff --git a/crates/aep/tests/it/import/synthetic.rs b/crates/aep/tests/it/import/synthetic.rs index 8e4c28f16..5e01c9f9b 100644 --- a/crates/aep/tests/it/import/synthetic.rs +++ b/crates/aep/tests/it/import/synthetic.rs @@ -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 { + 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); +}