Files
craft-libs/crates/psd/examples/animation_inspect.rs
Dylan M. Taylor ee78a9bd46 craft-psd: Photoshop PSD/PSB reader and writer, moved from PhotoCraft (#875)
PhotoCraft's `photocraft-psd` crate (crates/psd at storytold/photocraft
63b641b55e54bf8f5c83efd977fdb16a2c255431) as `craft-psd`: the lossless
PSD/PSB model, reader, writer and builder, descriptors, the Photoshop
resource files that share its primitives (.abr, .asl, .aco, .act, .ase,
.grd, .pat, .atn, .csh, .shc) and the Photoshop blocks of a layered TIFF.
The crate had no dependency on any other PhotoCraft crate.

Code is unchanged apart from the crate name, craft-libs' rustfmt width,
comments that named other PhotoCraft crates, issue references (now
`photocraft#N`), and the corpus tests' directory (`CRAFT_PSD_CORPUS` or
`corpus/` at the workspace root). Tests, proptests, fuzz targets and
fixtures move with it.

Part of #778, #786.
2026-11-11 17:00:33 -05:00

104 lines
4.7 KiB
Rust

//! Inspect a local PSD/PSB's animation structure without printing layer names, text, or metadata
//! values. No asset is bundled: pass a path explicitly. Does not decode pixels or modify the file.
//! `cargo run -p craft-psd --example animation_inspect -- /path/to/input.psd`
#[cfg(not(target_arch = "wasm32"))]
fn inspect() -> Result<(), Box<dyn std::error::Error>> {
use std::collections::BTreeMap;
use std::io::Read;
use craft_psd::animation::{frame_info, frame_sets, layer_frame_states};
use craft_psd::metadata::parse_shmd;
use craft_psd::{PsdFile, ResourceData};
let mut args = std::env::args_os().skip(1);
let path = args.next().ok_or("usage: animation_inspect <input.psd|input.psb>")?;
if args.next().is_some() {
return Err("expected exactly one input path".into());
}
// Diagnostic tool, not the production large-file importer. Bound even a file growing while read.
const MAX_BYTES: u64 = 128 * 1024 * 1024;
let file = std::fs::File::open(path)?;
if !file.metadata()?.is_file() || file.metadata()?.len() > MAX_BYTES {
return Err("expected a regular PSD/PSB no larger than 128 MiB".into());
}
let mut bytes = Vec::new();
file.take(MAX_BYTES + 1).read_to_end(&mut bytes)?;
if bytes.len() as u64 > MAX_BYTES {
return Err("input exceeds the diagnostic tool's 128 MiB limit".into());
}
let psd = PsdFile::from_bytes(&bytes)?;
println!("canvas={}x{} depth={} layers={}", psd.header.width, psd.header.height, psd.header.depth, psd.layers().len());
let mut animations = 0;
for resource in &psd.resources {
match resource.parsed() {
Some(Ok(ResourceData::Animation(animation))) => {
animations += 1;
let back = animation.to_bytes()?;
if back != resource.data {
return Err("animation resource failed its byte-exact round trip".into());
}
println!("animation resource: {} records; byte-exact round trip", animation.records.len());
for record in &animation.records {
if let Some(d) = record.descriptor() {
let d = d?;
let frames = frame_info(&d.descriptor)?;
let sets = frame_sets(&d.descriptor)?;
let mut delays = BTreeMap::new();
for frame in &frames {
*delays.entry(frame.delay_centiseconds).or_insert(0usize) += 1;
}
println!(" AnDs: {} definitions, {} frame sets", frames.len(), sets.len());
println!(" stored delays (centiseconds -> frame count): {delays:?}");
for set in sets {
let unresolved = set.frame_ids.iter().filter(|id| !frames.iter().any(|f| f.id == **id)).count();
println!(" frame set: {} ordered references, {unresolved} unresolved", set.frame_ids.len());
}
}
}
}
Some(Ok(ResourceData::TimelineInfo(d))) => println!("document timeline: {} descriptor fields", d.descriptor.items.len()),
Some(Err(e)) if resource.id == 1075 || (4000..=4999).contains(&resource.id) && resource.data.starts_with(b"mani") => return Err(e.into()),
_ => {}
}
}
let (mut states, mut tracks) = (0usize, 0usize);
for layer in psd.layers() {
for block in &layer.blocks {
if block.key != *b"shmd" {
continue;
}
let items = parse_shmd(&block.data)?;
for item in &items {
if let Some(d) = item.animation_descriptor() {
let d = d?;
if item.key == *b"mlst" {
layer_frame_states(&d.descriptor)?;
states += 1;
} else {
tracks += 1;
}
}
}
if craft_psd::metadata::write_shmd(&items) != block.data {
return Err("layer metadata failed its byte-exact round trip".into());
}
}
}
if psd.to_bytes()? != bytes {
return Err("PSD/PSB failed its byte-exact round trip".into());
}
println!("animation resources={animations}; layer frame-state descriptors={states}; layer timeline descriptors={tracks}");
println!("whole PSD/PSB: byte-exact round trip; input unchanged");
Ok(())
}
fn main() -> std::process::ExitCode {
#[cfg(not(target_arch = "wasm32"))]
if let Err(e) = inspect() {
eprintln!("animation inspection failed: {e}");
return std::process::ExitCode::FAILURE;
}
std::process::ExitCode::SUCCESS
}