mirror of
https://github.com/storytold/figcraft.git
synced 2026-10-26 11:26:50 -06:00
.fig files open through file.open, file.import, drag and drop, the dialogs, the CLI and MCP. A generic reader written from the public format description decodes the zip, the deflate/zstd chunks and the kiwi schema and message, then maps nodes by field name onto the REST importer: layer tree, geometry, paints with styles resolved, strokes, effects, text runs, vectors as outlines, auto layout including grid, constraints, components, sets, instances expanded from their main with overrides, styles, variables with modes and bindings, prototyping and image fills. Unsupported layers come back as warnings. Fuzzed; tested on fixtures generated in code. New dependency: ruzstd (pure-Rust zstd decoder).
53 lines
2.7 KiB
Rust
53 lines
2.7 KiB
Rust
//! `.fig` files through the file commands. The importer's own tests (fixtures built in code) are
|
||
//! in `figcraft_format::fig`; the local check here runs only when `FIGCRAFT_FIG_SAMPLES` names a
|
||
//! folder of `.fig` files with their frames' PNG exports next to them.
|
||
|
||
use figcraft_doc::{ExportFormat, NodeKind};
|
||
use serde_json::json;
|
||
|
||
use crate::Session;
|
||
|
||
#[test]
|
||
fn damaged_fig_files_fail_cleanly() {
|
||
let dir = std::env::temp_dir().join(format!("figcraft-fig-test-{}", std::process::id()));
|
||
std::fs::create_dir_all(&dir).unwrap();
|
||
let path = dir.join("broken.fig");
|
||
std::fs::write(&path, b"fig-kiwi\x65\0\0\0\xff\xff\xff\x7fjunk").unwrap();
|
||
let mut s = Session::new();
|
||
let r = s.execute("file.open", &json!({"path": path.to_string_lossy()}));
|
||
assert!(r.is_err());
|
||
let _ = std::fs::remove_dir_all(&dir);
|
||
}
|
||
|
||
/// Local only (`FIGCRAFT_FIG_SAMPLES`): each file opens without warnings, instances link to their
|
||
/// mains, and each top frame's 1× PNG export is close to the export saved next to the file.
|
||
#[test]
|
||
fn local_samples_match_their_exports() {
|
||
let Some(dir) = std::env::var_os("FIGCRAFT_FIG_SAMPLES") else { return };
|
||
let Ok(dir) = std::fs::read_dir(dir) else { return };
|
||
for p in dir.flatten().map(|e| e.path()).filter(|p| p.extension().is_some_and(|x| x == "fig")) {
|
||
let mut s = Session::new();
|
||
let r = s.execute("file.open", &json!({"path": p.to_string_lossy()})).unwrap();
|
||
assert_eq!(r["warnings"], json!([]), "{}", p.display());
|
||
let doc = s.doc().unwrap();
|
||
for n in doc.nodes().filter(|n| matches!(n.kind, NodeKind::Instance { .. })) {
|
||
assert!(n.children.iter().all(|c| doc.node(*c).is_some_and(|c| c.source.is_some())), "{}: {}", p.display(), n.name);
|
||
}
|
||
let page = doc.first_page().unwrap();
|
||
for f in doc.top_frames(page) {
|
||
let name = &doc.node(f).unwrap().name;
|
||
let Ok(want) = image::open(p.with_file_name(format!("{name}.png"))) else { continue };
|
||
let got = crate::cmd::file::export_bytes(doc, &[f], ExportFormat::Png, 1.0).unwrap();
|
||
let (got, want) = (image::load_from_memory(&got).unwrap().to_rgb8(), want.to_rgb8());
|
||
// Exports with effects may be cropped differently; compare the ones of the same size.
|
||
if got.dimensions() != want.dimensions() {
|
||
continue;
|
||
}
|
||
let diff: u64 = got.as_raw().iter().zip(want.as_raw()).map(|(a, b)| u64::from(a.abs_diff(*b))).sum();
|
||
let mean = diff as f64 / got.as_raw().len() as f64;
|
||
// Text may use another font where the sample's isn't installed.
|
||
assert!(mean < 4.0, "{name}: mean difference {mean:.2}");
|
||
}
|
||
}
|
||
}
|