Files
photocraft/crates/psd/examples/atn_dump.rs
T
Fabio Lissi 74ad8ee5bc Pin a corpus of real Photoshop Actions sets and test the .atn parser and import on it (#219)
- corpus/atn: 16 sets from seven public repositories (MIT, BSD, Unlicense), fetched at pinned
  commits by cargo xtask corpus --atn, verified against xtask/atn-corpus.sha256, licences next to
  the files, rows in ATTRIBUTION.md and docs/development.md. photocraft-corpus takes only our own
  generated files, so third-party sets are pins here, like psd-tools and ag-psd.
- photocraft-psd corpus_atn: every set parses and rewrites byte for byte (floor 16 files).
  photocraft-engine corpus_atn: every set loads, saves and reloads to the same actions; the
  steps still kept as Photoshop steps are capped (517 of 5 296 measured; generative AI and
  scripts), and the first differing step is named on a mismatch.
- What the corpus found: localised panel names are platform legacy bytes (Mac Roman), now kept
  verbatim with AtnItem::label from the descriptor's Unicode class name; exporters emitted
  Photoshop defaults the importer had not read (now symmetric); setAdjustment carries its kind;
  and 300 more steps map: transform without a reference, selection nudges, moves to the bottom,
  View menu items, toolbox colours, clipping masks, ungroup, copy/paste styles, collapse groups,
  layer comps. New example atn_roundtrip shows the first differing byte.
2026-10-13 08:29:38 -05:00

85 lines
3.8 KiB
Rust

//! Print the actions and steps of `.atn` files, one event per line, with the top-level keys of
//! each step's descriptor: `cargo run -p photocraft-psd --example atn_dump -- set.atn…`
//!
//! Handy when adding a Photoshop event to the engine's `.atn` import table: it shows the event
//! id, the panel name and the parameter keys Photoshop recorded.
use photocraft_psd::atn;
use photocraft_psd::descriptor::{Descriptor, ReferenceItem, Value};
fn summary(v: &Value) -> String {
match v {
Value::Integer(i) => i.to_string(),
Value::Double(d) => format!("{d}"),
Value::UnitFloat { unit, value } => format!("{value}{}", String::from_utf8_lossy(unit)),
Value::Boolean(b) => b.to_string(),
Value::Text(t) => format!("{:?}", t.to_string_lossy()),
Value::Enumerated { type_id, value } => format!("{}.{}", String::from_utf8_lossy(type_id.as_bytes()), String::from_utf8_lossy(value.as_bytes())),
Value::Descriptor(d) | Value::GlobalObject(d) => format!("{{{}}}", keys(d)),
Value::List(l) => format!("[{}]", l.iter().map(summary).collect::<Vec<_>>().join(", ")),
Value::Reference(r) => format!("ref[{}]", r.iter().map(reference).collect::<Vec<_>>().join(" ")),
other => format!("{other:?}").chars().take(40).collect(),
}
}
fn id(i: &photocraft_psd::descriptor::Id) -> String {
String::from_utf8_lossy(i.as_bytes()).trim().to_string()
}
fn reference(item: &ReferenceItem) -> String {
match item {
ReferenceItem::Property { class, key } => format!("prop({},{})", id(&class.class_id), id(key)),
ReferenceItem::Class(c) => format!("Clss({})", id(&c.class_id)),
ReferenceItem::Enumerated { class, type_id, value } => format!("Enmr({},{}.{})", id(&class.class_id), id(type_id), id(value)),
ReferenceItem::Offset { class, offset } => format!("rele({},{offset})", id(&class.class_id)),
ReferenceItem::Identifier { class, id: v } => format!("Idnt({},{v})", id(&class.class_id)),
ReferenceItem::Index { class, index } => format!("indx({},{index})", id(&class.class_id)),
ReferenceItem::Name { class, name } => format!("name({},{:?})", id(&class.class_id), name.to_string_lossy()),
}
}
fn keys(d: &Descriptor) -> String {
d.items.iter().map(|(k, v)| format!("{}={}", String::from_utf8_lossy(k.as_bytes()), summary(v))).collect::<Vec<_>>().join(" ")
}
fn main() {
let mut failed = false;
for path in std::env::args().skip(1) {
let bytes = match std::fs::read(&path) {
Ok(b) => b,
Err(e) => {
eprintln!("{path}: {e}");
failed = true;
continue;
}
};
match atn::parse(&bytes) {
Ok(f) => {
println!(
"== {path}: set {:?}, {} actions{}",
f.name.to_string_lossy(),
f.actions.len(),
if f.warnings.is_empty() { String::new() } else { format!(" ({})", f.warnings.join("; ")) }
);
for a in &f.actions {
println!(" action {:?} ({} steps)", a.name.to_string_lossy(), a.items.len());
for item in &a.items {
let flags = format!("{}{}", if item.enabled { "" } else { " off" }, if item.with_dialog { " dialog" } else { "" });
match &item.descriptor {
Some(d) => println!(" {:<22} {:<28}{flags} {}", item.event.id(), item.label(), keys(d)),
None => println!(" {:<22} {:<28}{flags}", item.event.id(), item.label()),
}
}
}
}
Err(e) => {
println!("== {path}: {e}");
failed = true;
}
}
}
if failed {
std::process::exit(1);
}
}