Files
craft-libs/crates/aep/examples/aepdump.rs
Dylan M. Taylorandarnav-hiwarkar 119ac0771f Add craft-aep: After Effects project container and typed view from EffectCraft (#888)
* 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>
2026-11-11 16:42:18 -05:00

76 lines
2.6 KiB
Rust

//! Developer tool: prints the chunk tree of an .aep (ids, sizes, short data), match names inline.
#![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)]
use craft_aep::{Body, Chunk};
/// Ends this developer tool with a message instead of a panic.
trait OrExit<T> {
fn or_exit(self, what: &str) -> T;
}
impl<T> OrExit<T> for Option<T> {
fn or_exit(self, what: &str) -> T {
match self {
Some(v) => v,
None => exit(what),
}
}
}
impl<T, E: std::fmt::Debug> OrExit<T> for Result<T, E> {
fn or_exit(self, what: &str) -> T {
match self {
Ok(v) => v,
Err(e) => exit(&format!("{what}: {e:?}")),
}
}
}
fn exit(msg: &str) -> ! {
eprintln!("{msg}");
std::process::exit(1)
}
fn show(c: &Chunk<'_>, depth: usize, max: usize, hex: usize) {
let ind = " ".repeat(depth);
let id = String::from_utf8_lossy(&c.id).to_string();
match &c.body {
Body::Data(d) if &c.id == b"tdmn" => {
let n: String = d.iter().take_while(|b| **b != 0).map(|b| *b as char).collect();
println!("{ind}tdmn {n}");
}
Body::Data(d) => {
let h: String = d.iter().take(hex).map(|b| format!("{b:02x}")).collect();
let a: String = d.iter().take(hex).map(|b| if (32..127).contains(b) { *b as char } else { '.' }).collect();
println!("{ind}{id} [{}] {h} |{a}|", d.len());
}
Body::Chunks(k) => {
println!("{ind}{id} (nested)");
if depth < max {
for x in k {
show(x, depth + 1, max, hex);
}
}
}
Body::List { list_type, children } => {
println!("{ind}LIST {} ({} children)", String::from_utf8_lossy(list_type), children.len());
if depth < max {
for x in children {
show(x, depth + 1, max, hex);
}
}
}
Body::OpaqueList { list_type, data } => println!("{ind}LIST {} opaque [{}]", String::from_utf8_lossy(list_type), data.len()),
}
}
fn main() {
let a: Vec<String> = std::env::args().collect();
let bytes = std::fs::read(a.get(1).or_exit("usage: see the comment at the top of this file")).or_exit("read");
let max: usize = a.get(2).and_then(|s| s.parse().ok()).unwrap_or(4);
let hex: usize = a.get(3).and_then(|s| s.parse().ok()).unwrap_or(32);
let f = craft_aep::parse(&bytes).or_exit("parse");
for c in &f.chunks {
show(c, 0, max, hex);
}
println!("trailer {} bytes", f.trailer.len());
}