mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-14 19:26:56 -04:00
AF.3: native Affinity import with artboards, vectors, text and images
Open and Place read Affinity documents natively (.af from Affinity 3, .afdesign/.afphoto/.afpub from Affinity 1 and 2): layers and sublayers, groups, artboards with their names and backgrounds, Publisher pages side by side, curves with live corners, parametric shapes, compound shapes, clipping by nesting, vector and pixel masks, solid and gradient fills, strokes, fill layers, artistic and frame text, placed images (their original JPEG/PNG), pixel layers, embedded documents as their cached picture, opacity, visibility, locking and blend modes, at the document's resolution in points. Unsupported content (effects, adjustments, brushes, master pages, special shapes, Affinity-only blend modes...) is listed in the import warning, one line per kind. A file whose native data can't be read opens as its embedded preview with the reason, and that preview cannot be placed or used as a template or library. Save never writes back over the Affinity file and there is no Affinity export. The document semantics were fitted to the thumbnails Affinity embeds in 176 public documents. `cargo xtask corpus --affinity` fetches 22 public CC0/MIT/Apache-2.0 documents at pinned commits, sha256-verified; the new engine test renders each and compares it with Affinity's own thumbnail (0-4.4 of 255 mean difference, per-file ceilings). import_fuzz mutates a synthetic native document's stream and container through open, render and export; the fuzz crate gains a stream target. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01GQPh8UDgMyARR5Hat9UchC
This commit is contained in:
1 parent
1c47480a09
commit
297a01b6aa
30 files changed
+3081
-147
No files matched your search
@@ -0,0 +1,22 @@
|
||||
c499cf3ca73cf7cd3e2e65bed048d1b8ecf03e7167b74d54f0ae6db31868cab8 affinity3-mexican-guy.af
|
||||
730e3fae51d9044ada58f51c7b5f9327d99e8d62376453bc251d0d26cda47743 affinity3-mexican-man.af
|
||||
5f5fccc0bd8be2bb3b2a07461ec77a012a61eef1883485d7c80f72d4383231ce affinity3-mexican-woman.af
|
||||
0a52bb923c949c566a4339b996bce1a67b683f61def1a73dfefb910effaa450d affinity3-playing-cards.af
|
||||
87a7bd93de16c404f6c6dd9cdebff96ec47832a3b342d2ce0af73d9a87e209e0 designer-beer.afdesign
|
||||
09ba790c428e8e09dc0b9ddebb4f3db6daefaa2af1da492ee34e73fba1b8af69 designer-cactus.afdesign
|
||||
c3bcd0395b407493bc9d2877d424029b70f0e4c817a56e893f7ac74973df52e5 designer-car.afdesign
|
||||
112562bc91e7cccf003d34364430544208373c4fb4bb4ab9acdaf27cf0125047 designer-flowers.afdesign
|
||||
023531b8216f2d4da24ab715e3b313506712e50d5f5bf7da927f434def1d0ea2 designer-lion-track.afdesign
|
||||
cc8ca0db0d8fc545eb29153be900f153fa6b7b7eac068d05e54a502aba1264fe afdesignload-color.afdesign
|
||||
9a17f500d09fb3b1a70d24aabdc2882955ec3eac3700a474549e6100be3a8484 afdesignload-layer_mode.afdesign
|
||||
77ae60c02749224fe2c52691c951568138eaed5f8584ebc5da83715fae0234dd afdesignload-layer_test.afdesign
|
||||
04944918ff8e29c5c1b466b7e43c232f7ab80db15fb934080bbcd7baaf093e22 afdesignload-margins.afdesign
|
||||
35afa31b89f2b9970eb3701ee64e2a19476383637e8b07689334d6ffe556b593 afdesignload-raster_test.afdesign
|
||||
e56c35f876277b101790ff4384cfde103a093ed6c81790a2d62176de21da339f afdesignload-revision_test.afdesign
|
||||
08946c60c0da1c8aeea92d65acfb244721eeb3b480ed358609f4a48bbecfc5b9 afdesignload-shape_test.afdesign
|
||||
7306ab4fcbdc225d816f32a0d2aca15ac887053f60170459ac947e70f4ef343d afdesignload-slice_test.afdesign
|
||||
e5d04bac0235af9386aea251cdd6903ed7810d335fdb6194d0483ea30b14e24a afdesignload-test_path.afdesign
|
||||
fbb196e918e302c16f8de9f2e6dffaef70ab3c317b32a9f44607fd4b2ead0206 jac21-SimpleLogo.afdesign
|
||||
9d874245936483de3ec04cca48b383028d33666801dff1814246a44e695e29b8 jac21-SimpleLogoBanner.afdesign
|
||||
e1b121d7c3d02356baa5dea512f5aa1fe77249c28d7b227e29cab2bc67ec323a eviltwo-AssetStore.aftemplate
|
||||
ec1072cb03fcd0ed7ac1dfc7e087f4920df545fe9c84a386a7dac2fd33fbbe1f leakcanary-vector_icon.afdesign
|
||||
@@ -0,0 +1,110 @@
|
||||
//! `cargo xtask corpus --affinity`: public Affinity documents saved by Affinity 1.x to 3.x, from
|
||||
//! their upstream repositories at pinned commits, each checked against `xtask/affinity-corpus.sha256`.
|
||||
//! They land in `corpus/affinity/` (gitignored, never committed). `vectorcraft-affinity`'s
|
||||
//! `real_files` test and the engine's `affinity_corpus` test use them when present.
|
||||
//!
|
||||
//! Only permissively licensed files (CC0, MIT, Apache-2.0) whose metadata holds no personal paths
|
||||
//! are listed. Moving a pin: change the commit, re-run, review the manifest diff.
|
||||
|
||||
use std::path::Path;
|
||||
use std::process::Command;
|
||||
|
||||
use crate::{root, run, sha256};
|
||||
|
||||
/// (repository, commit, licence, [(path in the repository, file name in corpus/affinity)]).
|
||||
type Source = (&'static str, &'static str, &'static str, &'static [(&'static str, &'static str)]);
|
||||
|
||||
const SOURCES: &[Source] = &[
|
||||
(
|
||||
"samuel-etver/vector-art",
|
||||
"255f8add3c8f0740196e22bd59502b811b532f0b",
|
||||
"CC0-1.0",
|
||||
&[
|
||||
("simple/mexican-guy.af", "affinity3-mexican-guy.af"),
|
||||
("simple/mexican-man.af", "affinity3-mexican-man.af"),
|
||||
("simple/mexican-woman.af", "affinity3-mexican-woman.af"),
|
||||
("simple/playing-cards.af", "affinity3-playing-cards.af"),
|
||||
("simple/beer.afdesign", "designer-beer.afdesign"),
|
||||
("simple/cactus.afdesign", "designer-cactus.afdesign"),
|
||||
("simple/car.afdesign", "designer-car.afdesign"),
|
||||
("simple/flowers.afdesign", "designer-flowers.afdesign"),
|
||||
("simple/lion-track.afdesign", "designer-lion-track.afdesign"),
|
||||
],
|
||||
),
|
||||
(
|
||||
"NickBeeuwsaert/AFDesignLoad",
|
||||
"a18dd50a7079fb861a28835eeefa0d015f03f4a1",
|
||||
"MIT",
|
||||
&[
|
||||
("testDesigns/color.afdesign", "afdesignload-color.afdesign"),
|
||||
("testDesigns/layer_mode.afdesign", "afdesignload-layer_mode.afdesign"),
|
||||
("testDesigns/layer_test.afdesign", "afdesignload-layer_test.afdesign"),
|
||||
("testDesigns/margins.afdesign", "afdesignload-margins.afdesign"),
|
||||
("testDesigns/raster_test.afdesign", "afdesignload-raster_test.afdesign"),
|
||||
("testDesigns/revision_test.afdesign", "afdesignload-revision_test.afdesign"),
|
||||
("testDesigns/shape_test.afdesign", "afdesignload-shape_test.afdesign"),
|
||||
("testDesigns/slice_test.afdesign", "afdesignload-slice_test.afdesign"),
|
||||
("testDesigns/test_path.afdesign", "afdesignload-test_path.afdesign"),
|
||||
],
|
||||
),
|
||||
(
|
||||
"Jac21/Branding",
|
||||
"57ae3f45bf4637f6927c0b07de25b922c55f944f",
|
||||
"MIT",
|
||||
&[
|
||||
("Logos/JC/DesignerFiles/Affinity/SimpleLogo.afdesign", "jac21-SimpleLogo.afdesign"),
|
||||
("Logos/JC/DesignerFiles/Affinity/SimpleLogoBanner.afdesign", "jac21-SimpleLogoBanner.afdesign"),
|
||||
],
|
||||
),
|
||||
(
|
||||
"eviltwo/AssetStoreTemplate",
|
||||
"f671981ee89d15526285b3cc87b4393d3defe644",
|
||||
"MIT",
|
||||
&[("AssetStoreTemplate/AssetStore.aftemplate", "eviltwo-AssetStore.aftemplate")],
|
||||
),
|
||||
(
|
||||
"square/leakcanary",
|
||||
"00880a8fb66eb4046a0124bff8303a04874bc07e",
|
||||
"Apache-2.0",
|
||||
&[("docs/assets/vector_icon.afdesign", "leakcanary-vector_icon.afdesign")],
|
||||
),
|
||||
];
|
||||
|
||||
const MANIFEST: &str = "xtask/affinity-corpus.sha256";
|
||||
|
||||
pub fn fetch() -> Result<(), String> {
|
||||
let root = root();
|
||||
let manifest = std::fs::read_to_string(root.join(MANIFEST)).map_err(|e| format!("{MANIFEST}: {e}"))?;
|
||||
let expected: Vec<(&str, &str)> = manifest.lines().filter_map(|l| l.split_once(" ")).collect();
|
||||
let dest = root.join("corpus").join("affinity");
|
||||
std::fs::create_dir_all(&dest).map_err(|e| format!("create {}: {e}", dest.display()))?;
|
||||
let mut sources = String::from(
|
||||
"# Public Affinity documents\n\nFetched by `cargo xtask corpus --affinity` and checked against `xtask/affinity-corpus.sha256`.\nGitignored; never commit these files.\n\n| File | Source | Licence |\n|---|---|---|\n",
|
||||
);
|
||||
let mut count = 0;
|
||||
for (repo, commit, licence, files) in SOURCES {
|
||||
for (path, name) in *files {
|
||||
let sha = expected.iter().find(|(_, n)| n == name).map(|(s, _)| *s).ok_or_else(|| format!("{name} is missing from {MANIFEST}"))?;
|
||||
let out = dest.join(name);
|
||||
if !verified(&out, sha) {
|
||||
let url = format!("https://raw.githubusercontent.com/{repo}/{commit}/{path}");
|
||||
let mut curl = Command::new("curl");
|
||||
curl.args(["-fsSL", "--retry", "3", "-A", "VectorCraft-dev", "-o"]).arg(&out).arg(&url);
|
||||
run(curl, &format!("curl {url}"))?;
|
||||
if !verified(&out, sha) {
|
||||
let _ = std::fs::remove_file(&out);
|
||||
return Err(format!("{name}: sha256 does not match {MANIFEST}"));
|
||||
}
|
||||
}
|
||||
sources.push_str(&format!("| {name} | https://github.com/{repo}/blob/{commit}/{path} | {licence} |\n"));
|
||||
count += 1;
|
||||
}
|
||||
}
|
||||
std::fs::write(dest.join("SOURCES.md"), sources).map_err(|e| format!("SOURCES.md: {e}"))?;
|
||||
println!("Affinity: {count} documents verified in {}", dest.display());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn verified(path: &Path, sha: &str) -> bool {
|
||||
std::fs::read(path).is_ok_and(|b| sha256::hex(&b) == sha)
|
||||
}
|
||||
+8
-2
@@ -3,11 +3,13 @@
|
||||
//! Pure Rust (std + serde_json). External tools (`cargo`, `curl`, `tar`) are
|
||||
//! invoked through `std::process::Command`.
|
||||
|
||||
mod affinity_corpus;
|
||||
mod assets;
|
||||
mod brands;
|
||||
mod bundle;
|
||||
mod ico;
|
||||
mod layers;
|
||||
mod sha256;
|
||||
mod stats;
|
||||
mod version;
|
||||
|
||||
@@ -23,8 +25,9 @@ commands:
|
||||
layers enforce the crate dependency layering (plan/architecture.md §3)
|
||||
wasm cargo check --target wasm32-unknown-unknown for the wasm-safe crates
|
||||
ci fmt --check, clippy -D warnings, test, assets, brands, layers, wasm (stops at first failure)
|
||||
corpus [--download]
|
||||
show where test corpora live; --download fetches PngSuite into corpus/pngsuite
|
||||
corpus [--download] [--affinity]
|
||||
show where test corpora live; --download fetches PngSuite into corpus/pngsuite,
|
||||
--affinity the pinned public Affinity documents into corpus/affinity
|
||||
bundle build dist/VectorCraft.app (macOS) with assets/app-icon/vectorcraft.icns
|
||||
ico <out.ico> <png>...
|
||||
pack PNGs into a Windows .ico (used by packaging/icons.sh)
|
||||
@@ -40,6 +43,7 @@ fn main() -> ExitCode {
|
||||
Some("layers") => cmd_layers(),
|
||||
Some("wasm") => cmd_wasm(),
|
||||
Some("ci") => cmd_ci(),
|
||||
Some("corpus") if rest.contains(&"--affinity") => affinity_corpus::fetch(),
|
||||
Some("corpus") => cmd_corpus(rest.contains(&"--download")),
|
||||
Some("bundle") => bundle::run(&root()),
|
||||
Some("ico") => ico::run(&rest),
|
||||
@@ -231,6 +235,8 @@ Tests that use a corpus skip cleanly when it is absent.
|
||||
corpus/tiff/ libtiff pics (optional)
|
||||
corpus/exr/ OpenEXR sample images (optional)
|
||||
corpus/raw/ raw.pixls.us samples, CC0 (optional)
|
||||
corpus/affinity/ public Affinity documents (CC0/MIT/Apache-2.0) at pinned commits, sha256
|
||||
verified: oracles for the Affinity reader. Fetch: cargo xtask corpus --affinity
|
||||
",
|
||||
corpus.display()
|
||||
);
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
//! SHA-256 (FIPS 180-4), std only, for verifying downloaded test corpora.
|
||||
|
||||
const K: [u32; 64] = [
|
||||
0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5, 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3,
|
||||
0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174, 0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da,
|
||||
0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967, 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13,
|
||||
0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85, 0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070,
|
||||
0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3, 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208,
|
||||
0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2,
|
||||
];
|
||||
|
||||
/// Lower-case hex digest of `data`.
|
||||
pub fn hex(data: &[u8]) -> String {
|
||||
let mut h: [u32; 8] = [0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, 0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19];
|
||||
let mut msg = data.to_vec();
|
||||
let bits = (data.len() as u64).wrapping_mul(8);
|
||||
msg.push(0x80);
|
||||
while msg.len() % 64 != 56 {
|
||||
msg.push(0);
|
||||
}
|
||||
msg.extend(bits.to_be_bytes());
|
||||
for block in msg.chunks_exact(64) {
|
||||
let mut w = [0u32; 64];
|
||||
for (i, word) in block.chunks_exact(4).enumerate() {
|
||||
w[i] = u32::from_be_bytes([word[0], word[1], word[2], word[3]]);
|
||||
}
|
||||
for i in 16..64 {
|
||||
let s0 = w[i - 15].rotate_right(7) ^ w[i - 15].rotate_right(18) ^ (w[i - 15] >> 3);
|
||||
let s1 = w[i - 2].rotate_right(17) ^ w[i - 2].rotate_right(19) ^ (w[i - 2] >> 10);
|
||||
w[i] = w[i - 16].wrapping_add(s0).wrapping_add(w[i - 7]).wrapping_add(s1);
|
||||
}
|
||||
let [mut a, mut b, mut c, mut d, mut e, mut f, mut g, mut hh] = h;
|
||||
for i in 0..64 {
|
||||
let s1 = e.rotate_right(6) ^ e.rotate_right(11) ^ e.rotate_right(25);
|
||||
let ch = (e & f) ^ (!e & g);
|
||||
let t1 = hh.wrapping_add(s1).wrapping_add(ch).wrapping_add(K[i]).wrapping_add(w[i]);
|
||||
let s0 = a.rotate_right(2) ^ a.rotate_right(13) ^ a.rotate_right(22);
|
||||
let maj = (a & b) ^ (a & c) ^ (b & c);
|
||||
let t2 = s0.wrapping_add(maj);
|
||||
(hh, g, f, e, d, c, b, a) = (g, f, e, d.wrapping_add(t1), c, b, a, t1.wrapping_add(t2));
|
||||
}
|
||||
for (x, y) in h.iter_mut().zip([a, b, c, d, e, f, g, hh]) {
|
||||
*x = x.wrapping_add(y);
|
||||
}
|
||||
}
|
||||
h.iter().map(|x| format!("{x:08x}")).collect()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#[test]
|
||||
fn known_digests() {
|
||||
assert_eq!(super::hex(b""), "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855");
|
||||
assert_eq!(super::hex(b"abc"), "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad");
|
||||
let million = vec![b'a'; 1_000_000];
|
||||
assert_eq!(super::hex(&million), "cdc76e5c9914fb9281a1c7e284d73e67f1809a48a497200e046d39ccc7112cd0");
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user