mirror of
https://github.com/storytold/craft-libs.git
synced 2026-11-12 08:52:32 +00:00
* Add craft-aep: After Effects project container and typed view from EffectCraft The .aep (RIFX 'Egg!') and .aepx container as a lossless chunk tree that writes back byte for byte, and the typed, read-only view over it: compositions, layers, properties, keyframes, effects and their settings data, masks, shapes, text documents, markers, cameras, track mattes, footage, project settings and the saving version. Moved from effectcraft crates/aep/src/container at f6b4da0a with its unit tests, the container integration tests, the aepdump and stubicc tools and docs/aep-format.md. The match-name tables (effect_map, style_map), the importer and the exporter are written against EffectCraft's model and stay there. ExtractorChannels::sources takes the caller's name for a copied channel instead of an EffectCraft constant. * craft-aep: read the Photoshop layer a footage item shows A footage item made from one layer of a .psd stores the layer in its `opti` (tag `8BPS`): the layer record's index (big-endian i32 at 14, -1 for the merged image), the document's two sides (little-endian u32 at 32 and 36), the layer's bounds (little-endian i32 at 78, 82, 86, 90) and its name (UTF-8 from 344). `Footage::psd_layer` reads them as `PsdLayer`. The format part of storytold/effectcraft#3449; the importer's use of it stays in EffectCraft. --------- Co-authored-by: arnav-hiwarkar <asharnav2008@gmail.com>
171 lines
7.3 KiB
Rust
171 lines
7.3 KiB
Rust
//! Batch 5 checks of fields decoded before (markers with several entries and fractional
|
|
//! durations, 509 and 510).
|
|
|
|
use crate::container::helpers::{first_comp, open};
|
|
use craft_aep::Project;
|
|
|
|
#[test]
|
|
fn two_composition_markers_in_time_order_with_label_and_duration() {
|
|
let bytes = fixture!("509-marker-comp-two.aep");
|
|
let file = open(&bytes);
|
|
let project = Project::new(&file).expect("project");
|
|
let m = first_comp(&project).markers();
|
|
let got: Vec<_> = m.iter().map(|m| (m.time_units, m.comment.as_str(), m.duration_seconds, m.label)).collect();
|
|
assert_eq!(got, vec![(25_600, "M1", Some(0.0), Some(2)), (64_000, "M2", Some(0.5), Some(0))]);
|
|
}
|
|
|
|
#[test]
|
|
fn a_fractional_marker_time_and_duration() {
|
|
let bytes = fixture!("510-marker-layer-0.52-dur0.3.aep");
|
|
let file = open(&bytes);
|
|
let project = Project::new(&file).expect("project");
|
|
let comp = first_comp(&project);
|
|
let layer = comp.layers().next().expect("L");
|
|
let m = layer.markers();
|
|
assert_eq!(m.len(), 1);
|
|
// 0.52 s = 13 312 / 25 600; 0.3 s = 180 / 600.
|
|
assert_eq!((m[0].time_units, m[0].comment.as_str()), (13_312, "LM"));
|
|
assert!((m[0].duration_seconds.expect("duration") - 0.3).abs() < 1e-12);
|
|
}
|
|
|
|
/// The stored value of the property at `path` (match names) of the first layer.
|
|
fn shape_value(name: &str, path: &[&str]) -> Option<Option<Vec<f64>>> {
|
|
Some({
|
|
let bytes = crate::common::try_fixture(name)?;
|
|
let file = open(&bytes);
|
|
let project = Project::new(&file).expect("project");
|
|
let comp = first_comp(&project);
|
|
let layer = comp.layers().next().expect("S");
|
|
match layer.properties()?.find(path)? {
|
|
craft_aep::PropNode::Property(p) => p.static_value(),
|
|
_ => None,
|
|
}
|
|
})
|
|
}
|
|
|
|
const G1: [&str; 2] = ["ADBE Root Vectors Group", "ADBE Vector Group"];
|
|
|
|
#[test]
|
|
fn shape_values_are_stored_as_shown() {
|
|
let at = |rest: &[&'static str]| -> Vec<&'static str> { G1.iter().chain(rest.iter()).copied().collect() };
|
|
assert_eq!(
|
|
skip!(shape_value(
|
|
"451-shape-ellipse-400x300-fill.aep",
|
|
&at(&["ADBE Vectors Group", "ADBE Vector Shape - Ellipse", "ADBE Vector Ellipse Size"])
|
|
)),
|
|
Some(vec![400.0, 300.0])
|
|
);
|
|
assert_eq!(
|
|
skip!(shape_value(
|
|
"451-shape-ellipse-400x300-fill.aep",
|
|
&at(&["ADBE Vectors Group", "ADBE Vector Graphic - Fill", "ADBE Vector Fill Color"])
|
|
)),
|
|
Some(vec![255.0, 0.0, 255.0, 0.0])
|
|
);
|
|
// Group scale in percent, not a fraction.
|
|
assert_eq!(
|
|
skip!(shape_value("457-shape-group-scale-50-80.aep", &at(&["ADBE Vector Transform Group", "ADBE Vector Scale"]))),
|
|
Some(vec![50.0, 80.0])
|
|
);
|
|
assert_eq!(
|
|
skip!(shape_value(
|
|
"464-shape-stroke-opacity-50.aep",
|
|
&at(&["ADBE Vectors Group", "ADBE Vector Graphic - Stroke", "ADBE Vector Stroke Opacity"])
|
|
)),
|
|
Some(vec![50.0])
|
|
);
|
|
assert_eq!(
|
|
skip!(shape_value("467-shape-trim-end-70.aep", &at(&["ADBE Vectors Group", "ADBE Vector Filter - Trim", "ADBE Vector Trim End"]))),
|
|
Some(vec![70.0])
|
|
);
|
|
assert_eq!(
|
|
skip!(shape_value("469-shape-trim-type-2.aep", &at(&["ADBE Vectors Group", "ADBE Vector Filter - Trim", "ADBE Vector Trim Type"]))),
|
|
Some(vec![2.0])
|
|
);
|
|
assert_eq!(
|
|
skip!(shape_value("471-shape-rect-roundness-30.aep", &at(&["ADBE Vectors Group", "ADBE Vector Shape - Rect", "ADBE Vector Rect Roundness"]))),
|
|
Some(vec![30.0])
|
|
);
|
|
assert_eq!(
|
|
skip!(shape_value("473-shape-star-points-6.aep", &at(&["ADBE Vectors Group", "ADBE Vector Shape - Star", "ADBE Vector Star Points"]))),
|
|
Some(vec![6.0])
|
|
);
|
|
// The shape layer's own anchor: pixels as stored.
|
|
assert_eq!(skip!(shape_value("462-shape-layer-anchor-10-20.aep", &["ADBE Transform Group", "ADBE Anchor Point"])), Some(vec![10.0, 20.0, 0.0]));
|
|
// A new Trim Paths and every default item of 519 have no record.
|
|
for item in [
|
|
"ADBE Vector Shape - Rect",
|
|
"ADBE Vector Shape - Ellipse",
|
|
"ADBE Vector Shape - Star",
|
|
"ADBE Vector Graphic - Fill",
|
|
"ADBE Vector Filter - Trim",
|
|
] {
|
|
let bytes = fixture!("519-shape-all-items-default.aep");
|
|
let file = open(&bytes);
|
|
let project = Project::new(&file).expect("project");
|
|
let comp = first_comp(&project);
|
|
let layer = comp.layers().next().expect("S");
|
|
let Some(craft_aep::PropNode::Group(g)) = layer.properties().and_then(|p| p.find(&at(&["ADBE Vectors Group", item]))) else {
|
|
panic!("{item}")
|
|
};
|
|
assert!(g.entries().iter().all(|e| !matches!(e.node, craft_aep::PropNode::Property(p) if p.flags() == Some(1))), "{item} has a value record");
|
|
}
|
|
}
|
|
|
|
/// The baseline option of the first style run of the first text layer.
|
|
fn script_of(name: &str) -> Option<craft_aep::FontBaseline> {
|
|
Some({
|
|
let bytes = crate::common::try_fixture(name)?;
|
|
let file = open(&bytes);
|
|
let project = Project::new(&file).expect("project");
|
|
let comp = first_comp(&project);
|
|
let layer = comp.layers().find(|l| l.text_document().is_some()).expect("a text layer");
|
|
let doc = layer.text_document().expect("document");
|
|
assert!(doc.undecoded_style_fields.is_empty(), "{name}: {:?}", doc.undecoded_style_fields);
|
|
doc.style_runs.first().expect("a run").baseline
|
|
})
|
|
}
|
|
|
|
#[test]
|
|
fn superscript_and_subscript_are_style_slot_13() {
|
|
use craft_aep::FontBaseline;
|
|
// The character state the corpus was made in is superscript (526, and 243, 517).
|
|
assert_eq!(skip!(script_of("526-text-point-HHHH-default.aep")), FontBaseline::Superscript);
|
|
assert_eq!(skip!(script_of("243-text-multiline-A-CR-B.aep")), FontBaseline::Superscript);
|
|
assert_eq!(skip!(script_of("527-text-point-HHHH-baseline-normal.aep")), FontBaseline::Normal);
|
|
assert_eq!(skip!(script_of("528-text-point-HHHH-baseline-subscript.aep")), FontBaseline::Subscript);
|
|
}
|
|
|
|
#[test]
|
|
fn a_wrapping_box_text() {
|
|
let bytes = fixture!("517-text-box-600x400-wrap.aep");
|
|
let file = open(&bytes);
|
|
let project = Project::new(&file).expect("project");
|
|
let comp = first_comp(&project);
|
|
let doc = comp.layers().find_map(|l| l.text_document()).expect("text");
|
|
let b = doc.text_box.expect("box text");
|
|
assert_eq!((b.x, b.y, b.width, b.height), (-300.0, -200.0, 600.0, 400.0));
|
|
assert_eq!(doc.raw_text, "The quick brown fox jumps over the lazy dog and keeps running far away\r");
|
|
assert_eq!(doc.style_runs[0].font_size, 100.0);
|
|
}
|
|
|
|
fn layer_l<T>(name: &str, f: impl Fn(&craft_aep::Layer<'_, '_>) -> T) -> Option<T> {
|
|
Some({
|
|
let bytes = crate::common::try_fixture(name)?;
|
|
let file = open(&bytes);
|
|
let project = Project::new(&file).expect("project");
|
|
let comp = first_comp(&project);
|
|
let layer = comp.layers().next().expect("the solid L, the only layer");
|
|
f(&layer)
|
|
})
|
|
}
|
|
|
|
#[test]
|
|
fn orientation_is_otst_otda() {
|
|
let o = skip!(layer_l("488-layer-3d-orientation-10-20-30.aep", |l| l.orientation())).expect("orientation");
|
|
assert_eq!((o.degrees, o.animated, o.has_expression), ([10.0, 20.0, 30.0], false, false));
|
|
for name in ["485-layer-3d-on.aep", "486-layer-3d-rotx-30.aep", "487-layer-3d-roty-45.aep"] {
|
|
assert_eq!(skip!(layer_l(name, |l| l.orientation())), None, "{name}: absent at the default");
|
|
}
|
|
}
|