.aep import: a test pins the Orientation expression import (#3087) (#3313)

* .aep import: Photoshop layers, placeholder effects, shape direction and Orientation expressions (#3087)

- A footage item showing one Photoshop layer shows that layer (record index, bounds and name
  from its opti) and is named <layer>/<file>; an adjustment layer shows white where it applies
  (opt-in through SourceLayer::adjustment_white, the direct Photoshop import is unchanged).
- Effects without a counterpart arrive as placeholder effects with their parameters, so
  expressions reading them keep working; pseudo effects are no loss, a Dropdown Menu Control
  becomes EffectCraft's. Slider ranges are checked, sizes are bounded.
- The loss prompt shows every detail line the report keeps and how many more there are.
- Shape direction and gradient highlight records in the default state are no loss.
- An expression on Orientation is imported with its value or keys.

* .aep import: report placeholder records and popup defaults that borrowed definitions leave open (#3087)

A value record the borrowed definitions do not name is reported instead of dropped, and a
borrowed popup without a record reports that its default item is not known (the first is used).

* Merge upstream/main into aep-import-bugs-3087
This commit is contained in:
Valentin Klein authored and GitHub committed 2026-11-13 00:15:49 +01:00
1 parent 4035d18132
commit 8028f46808
2 files changed
+91

No files matched your search

+1
View File
@@ -18,6 +18,7 @@ mod effects_round2;
mod effects_round3;
mod effects_thirdparty;
mod fx_inject;
mod issue_3087;
mod lumetri;
mod markers;
mod masks;
+90
View File
@@ -0,0 +1,90 @@
//! #3087: an expression on Orientation, built into the committed `synthetic.aep` (no external
//! files; the values are made up, the layouts are docs/aep-format.md's). The Photoshop layers and
//! placeholders of the issue are tested in `synthetic.rs` and `placeholders.rs`.
use effectcraft_aep::import::{ImportOptions, ImportResult, LossCategory, import};
use effectcraft_aep::{Body, Chunk, parse, write};
use effectcraft_keyframe::Value;
use effectcraft_project::Project;
const SYNTHETIC: &[u8] = include_bytes!("../../fixtures/synthetic.aep");
fn visit(chunks: &mut [Chunk<'_>], f: &mut dyn FnMut(&mut Chunk<'_>)) {
for c in chunks {
f(c);
visit(c.children_mut(), f);
}
}
fn import_all(bytes: &[u8]) -> (Project, ImportResult) {
let mut project = Project::default();
let none = |_: &str| false;
let r = import(&mut project, bytes, &ImportOptions { allow_losses: true, ..ImportOptions::new(&none) }).unwrap();
(project, r)
}
/// `synthetic.aep` with every Orientation set to `[10, 20, 30]` and the expression `text`.
fn with_orientation_expression(text: &str) -> Vec<u8> {
let mut file = parse(SYNTHETIC).unwrap();
let mut patched = 0;
visit(&mut file.chunks, &mut |c| {
if !c.is_list(b"otst") {
return;
}
for k in c.children_mut() {
if k.is_list(b"tdbs") {
let kids = k.children_mut();
for r in kids.iter_mut() {
if let Body::Data(d) = &mut r.body {
let d = d.to_mut();
match &r.id {
b"tdsb" => d.copy_from_slice(&1u32.to_be_bytes()),
b"tdb4" => {
d[119] = 0;
d[120] = 1;
}
_ => {}
}
}
}
let Body::List { children, .. } = &mut k.body else { continue };
children.push(Chunk::data(*b"Utf8", text.as_bytes().to_vec()));
} else if k.is_list(b"otky") {
let mut otda = Vec::new();
for v in [10.0f64, 20.0, 30.0] {
otda.extend(v.to_be_bytes());
}
*k = Chunk::list(*b"otky", vec![Chunk::data(*b"otda", otda)]);
}
}
patched += 1;
});
assert!(patched > 0, "the synthetic project has an Orientation");
write(&file).unwrap()
}
/// An expression on Orientation arrives with the value (both were dropped and reported "not
/// decoded").
#[test]
fn an_expression_on_orientation_is_imported() {
let (p, r) = import_all(&with_orientation_expression("[1, 2, 3]"));
let mut seen = 0;
for (_, c) in p.comps() {
for l in &c.layers {
let Some(o) = l.props.prop("transform/orientation") else { continue };
assert_eq!(o.value, Value::Vec3([10.0, 20.0, 30.0]), "{}", l.name);
assert_eq!(o.expr.as_ref().map(|e| (e.text.as_str(), e.enabled)), Some(("[1, 2, 3]", true)), "{}", l.name);
seen += 1;
}
}
assert!(seen > 0);
let orientation: Vec<String> = r
.report
.losses
.iter()
.filter(|l| l.category == LossCategory::TransformProperties)
.flat_map(|l| l.details.clone())
.filter(|d| d.contains("Orientation"))
.collect();
assert!(orientation.is_empty(), "{orientation:?}");
}