mirror of
https://github.com/storytold/craft-libs.git
synced 2026-11-12 08:52:32 +00:00
* craft-affinity: bounded reader for native Affinity documents, moved from VectorCraft The reader of .af, .afdesign, .afphoto and .afpub files (archive, object stream and document model) as it is in storytold/vectorcraft at 6e873b80 (crates/affinity), under a neutral name. Its corpus tests read the pinned public documents scripts/fetch-affinity-corpus.sh fetches. * craft-affinity: fold in what only PhotoCraft's copy of the reader had From storytold/photocraft at 63b641b5 (crates/affinity), onto the VectorCraft copy: - Special shapes: clouds, hearts, cogs, callouts, arrows, double stars, tears, crescents, diamonds and segments come in as their own outlines, not their bounding ellipse. - Null slots of object lists are charged to the document's work before a list is scanned, on every visit of a shared object (children, attachments, effects, compound operands, page lists, paint lists, gradient colours, text blocks and runs, table cells, tile lists). - A colour or gradient position record is decoded only as far as its components are read. - 16-bit and 8-bit planes are interleaved through tables, not an f64 for each pixel. - Limits::max_pixel_bytes: the caller sets what the pixel layers of one import may hold. - Pixels::cmyka8_to_rgba8 for a caller without colour management. The crate README and docs describe the model on its own terms. * craft-affinity: a callout with a right-to-left box no longer panics; an oversized shape imports as its box Ported from storytold/photocraft#10473 by DJHoltkamp. A callout rectangle clamped its tail with clamp(x0, x1), which panics when the box is stored right to left. A box with finite corners whose size overflows gave infinite or NaN outline points; it now imports as its box with a warning. Also drops the inflated doc.dat once it is parsed.
69 lines
3.5 KiB
Rust
69 lines
3.5 KiB
Rust
//! Structural regression signatures, measured from the pinned files through our own reader.
|
|
//! These freeze feature retention; they are not independent visual or compatibility oracles.
|
|
//! The model CRC covers every decoded field via its Debug representation (including paths,
|
|
//! paint, masks, image pixels and text runs). A deliberate reader/Debug change needs review;
|
|
//! input authenticity is independently checked with SHA256 by the corpus gate.
|
|
use craft_affinity::model::{Document, Kind, Node};
|
|
use craft_affinity::paint::Paint;
|
|
use std::collections::{BTreeMap, BTreeSet};
|
|
|
|
pub fn signature(doc: &Document) -> String {
|
|
fn visit(nodes: &[Node], counts: &mut BTreeMap<&'static str, usize>, fonts: &mut BTreeSet<String>, boards: &mut Vec<String>, depth: usize) {
|
|
for n in nodes {
|
|
*counts.entry("nodes").or_default() += 1;
|
|
counts.entry("depth").and_modify(|v| *v = (*v).max(depth)).or_insert(depth);
|
|
let key = match &n.kind {
|
|
Kind::Layer => "layers",
|
|
Kind::Group => "groups",
|
|
Kind::Artboard { rect, .. } => {
|
|
boards.push(format!("{}:{:.4},{:.4},{:.4},{:.4}", n.name, rect.x0, rect.y0, rect.x1, rect.y1));
|
|
"artboards"
|
|
}
|
|
Kind::Shape { path, fills, strokes, .. } => {
|
|
*counts.entry("subpaths").or_default() += path.subpaths.len();
|
|
*counts.entry("segments").or_default() += path.subpaths.iter().map(|p| p.segments.len()).sum::<usize>();
|
|
*counts.entry("fills").or_default() += fills.len();
|
|
*counts.entry("strokes").or_default() += strokes.len();
|
|
*counts.entry("gradients").or_default() += fills.iter().filter(|p| matches!(p, Paint::Gradient(_))).count();
|
|
"shapes"
|
|
}
|
|
Kind::Text(t) => {
|
|
*counts.entry("text_runs").or_default() += t.runs.len();
|
|
*counts.entry("text_chars").or_default() += t.runs.iter().map(|r| r.text.chars().count()).sum::<usize>();
|
|
for r in &t.runs {
|
|
fonts.insert(format!("{}:{}:{}:{}", r.family, r.postscript, r.weight, r.italic));
|
|
}
|
|
"texts"
|
|
}
|
|
Kind::Table(t) => {
|
|
*counts.entry("table_cells").or_default() += t.cells.len();
|
|
"tables"
|
|
}
|
|
Kind::Image(_) => "images",
|
|
Kind::MasterInstance { .. } => "master_instances",
|
|
Kind::Unsupported => "unsupported",
|
|
};
|
|
*counts.entry(key).or_default() += 1;
|
|
for (key, yes) in
|
|
[("hidden", !n.visible), ("vector_masks", n.mask.is_some()), ("pixel_masks", n.pixel_mask.is_some()), ("opacity", n.opacity < 1.0)]
|
|
{
|
|
if yes {
|
|
*counts.entry(key).or_default() += 1;
|
|
}
|
|
}
|
|
visit(&n.children, counts, fonts, boards, depth + 1);
|
|
}
|
|
}
|
|
let (mut counts, mut fonts, mut boards) = (BTreeMap::new(), BTreeSet::new(), Vec::new());
|
|
for spread in &doc.spreads {
|
|
visit(&spread.nodes, &mut counts, &mut fonts, &mut boards, 1);
|
|
}
|
|
let model_crc32 = crc32fast::hash(format!("{:?}", doc.spreads).as_bytes());
|
|
format!(
|
|
"dpi={};spreads={};model_crc32={model_crc32:08x};counts={counts:?};boards={boards:?};fonts={fonts:?};warnings={:?}",
|
|
doc.dpi,
|
|
doc.spreads.len(),
|
|
doc.warnings
|
|
)
|
|
}
|