mirror of
https://github.com/storytold/craft-libs.git
synced 2026-11-12 10:03:51 +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>
76 lines
2.6 KiB
Rust
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());
|
|
}
|