.ai import: the document's graphic styles open in the panel with their objects linked

Squashed from #5155 onto main.
This commit is contained in:
dotCooCoo
2026-11-02 14:32:53 +07:00
committed by bflatastic
parent 0e38796573
commit 405372555d
13 changed files with 546 additions and 25 deletions
@@ -160,6 +160,36 @@ fn an_ai_file_opens_with_the_swatches_of_its_palette() {
);
}
/// A `.ai` opens with the graphic styles of its style list, in the list's order, and an object read
/// as itself with a listed style is linked to it.
#[test]
fn an_ai_file_opens_with_the_graphic_styles_of_its_style_list() {
let style = |name: &str, xa: &str| {
format!(
"/KnownStyle :\n({name}) /Name ,\n/ActiveStyle :\n/CompoundFilter :\n(Chain Style Filter) 0 0 /Filter ,\n1 /Visible ,\n\
/CompoundFilter :\n(Stack Style Filter) 0 0 /Filter ,\n1 /Visible ,\n\
/BasicFilter :\n(Fill Style Filter) 0 0 /Filter ,\n1 /Visible ,\n/Dictionary : /NotRecorded ,\n/FillStyle : 0 O\n{xa}0 1 Xd\n/Def ;\n (FillStyle) ,\n; /Dict ;\n /Part ,\n\
/BasicFilter :\n(Conduit Filter) 0 0 /Filter ,\n1 /Visible ,\n;\n /Part ,\n;\n /Part ,\n;\n"
)
};
let styles = format!(
"%AI9_BeginArtStyles\n{}{}%AI9_EndArtStyles\n%AI9_BeginArtStyleList\n(Red)\n(Blue)\n%AI9_EndArtStyleList\n",
style("Blue", "0 0 0 0 0 0 1 Xa\n"),
style("Red", "0 0 0 0 1 0 0 Xa\n")
);
let hidden = ai::rect(10.0, 50.0, 40.0, 90.0).replace('\n', "\n%_");
let art = format!("0 Ae\nu\n0 0 0 0 1 0 0 Xa\n{}f\nU\n%_{hidden}f\n1 (Red) XW\n", ai::rect(10.0, 50.0, 40.0, 90.0));
let data = ai::editing_data(200.0, 100.0, &format!("{styles}{}", ai::layer("Art", &art)));
let mut s = Session::new();
let r = open(&mut s, "styles.ai", &ai::ai(&compressed(data.as_bytes()), "1 0 0 rg 10 50 30 40 re f"), json!({})).unwrap();
let doc = &s.doc().unwrap().doc;
assert_eq!(names(&doc.layers), ["Art"], "opened from the editing data: {r}");
let styles: Vec<(&str, u32)> = doc.graphic_styles.iter().map(|g| (g.name.as_str(), g.id)).collect();
assert_eq!(styles, [("Red", 1), ("Blue", 2)]);
let square = &doc.layers[0].children().unwrap()[0];
assert_eq!(square.extras().graphic_style, Some(1), "{square:#?}");
}
/// A `.ai` whose editing data has a palette and no layers, as a swatch library is saved, opens as its
/// PDF part with the swatches and color groups of its palette in place of the default swatches.
#[test]
+52
View File
@@ -3230,6 +3230,46 @@ proptest! {
survive("mutated .ai Objects on Path", || Some(vectorcraft_eps::layered_ai(&private, Document::new(200.0, 100.0), vec![], false, true, 1.0).0))?;
}
/// Style lists (`%AI9_BeginArtStyleList`) of any entries, repeated, empty, malformed or not
/// defined, with or without their end, and objects whose `XW` has any code and any style name.
#[test]
fn ai_style_lists_and_style_links_never_panic(
listed in prop::collection::vec(prop_oneof![
prop::sample::select(vec!["(A)", "(B)", "()", "(Caf\\303\\251)", "(A(B))", "(x\\)", "(", ")", "<41>", "/A", "1"]).prop_map(str::to_string),
"[ -~]{0,12}",
], 0..12),
ended in any::<bool>(),
objects in prop::collection::vec((prop_oneof![arb_num(), prop::sample::select(vec!["1".to_string(), "6".into(), "9".into()])], prop::sample::select(vec!["A", "B", "", "Caf\u{e9}", "Missing"]), any::<bool>()), 0..6),
) {
use vectorcraft_testkit::ai;
let style = |name: &str| {
format!(
"/KnownStyle :\n({name}) /Name ,\n/ActiveStyle :\n/CompoundFilter :\n(Chain Style Filter) 0 0 /Filter ,\n1 /Visible ,\n\
/BasicFilter :\n(Fill Style Filter) 0 0 /Filter ,\n1 /Visible ,\n/Dictionary : /NotRecorded ,\n/FillStyle : 0 O\n0 0 0 0 1 0 0 Xa\n0 1 Xd\n/Def ;\n (FillStyle) ,\n; /Dict ;\n /Part ,\n\
/BasicFilter :\n(Adobe Drop Shadow) 1 0 /Filter ,\n1 /Visible ,\n; /Part ,\n;\n"
)
};
let end = if ended { "%AI9_EndArtStyleList\n" } else { "" };
let header = format!(
"%AI9_BeginArtStyles\n{}{}{}%AI9_EndArtStyles\n%AI9_BeginArtStyleList\n{}\n{end}",
style("A"),
style("B"),
style("Caf\u{e9}"),
listed.join("\n")
);
let art: String = objects
.iter()
.map(|(code, name, group)| {
let object = if *group { "%_0 Ae\n%_u\n%_0 0 m\n%_0 9 L\n%_9 9 L\n%_f\n%_U\n" } else { "%_0 0 m\n%_0 9 L\n%_9 9 L\n%_f\n" };
format!("0 Ae\nu\n0 0 m\n0 9 L\n9 9 L\nf\nU\n{object}{code} ({name}) XW\n")
})
.collect();
let data = ai::editing_data(200.0, 100.0, &format!("{header}%AI5_BeginLayer\n1 1 1 1 0 0 1 0 79 128 255 0 50 0 Lb\n(L) Ln\n{art}LB\n%AI5_EndLayer--\n"));
survive("style lists and links", || Some(vectorcraft_eps::layered_ai(data.as_bytes(), Document::new(200.0, 100.0), vec![], false, false, 1.0).0))?;
let eps = ai::eps(data.as_bytes(), ai::page_ps());
survive("style lists and links in an EPS", || vectorcraft_engine::cmd::fileio::load("x.eps", &eps).ok().map(|l| l.doc))?;
}
/// Objects on Path records of any shape: ids in any order, repeated or missing, any number of
/// objects, pivots and angles out of range or not finite, spacings, order tags that disagree,
/// groups nested or cut short, and versioned content left open.
@@ -3662,6 +3702,18 @@ fn arb_ai_token() -> impl Strategy<Value = String> {
"9 () XW",
"\n%AI9_BeginArtStyles\n",
"\n%AI9_EndArtStyles\n",
"\n%AI9_BeginArtStyleList\n",
"\n%AI9_EndArtStyleList\n",
"(style)\n",
"(st\\)y(le)\n",
"(\\303\\251",
"(style) XW",
"1 () XW",
"6 (style) XW",
"7 (style) XW",
"-1 (style) XW",
"1e308 (style) XW",
"1 (Caf\\351) XW",
"/KnownStyle :",
"(style) /Name ,",
"/ActiveStyle :",