mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-11 15:21:58 +00:00
Merge pull request #5952 from dotCooCoo/feat/ai-pathfinder-effect-commands
.ai import: the other Effect › Pathfinder operations open as live effects on a group
This commit is contained in:
@@ -4564,11 +4564,15 @@ proptest! {
|
||||
|
||||
/// A group with an Effect › Pathfinder style whose dictionary is hostile: any `Command`, keys of
|
||||
/// any type and value (missing, repeated, unknown, not finite), shown or hidden, the style cut
|
||||
/// short anywhere; in editing data, an EPS and a `.ai`. A `Command` other than 0 never becomes
|
||||
/// a live effect.
|
||||
/// short anywhere; in editing data, an EPS and a `.ai`. The dictionary may also start with every
|
||||
/// option at the value read and any `Mix`. A `Command` other than 0 to 11 never becomes a live
|
||||
/// effect, and a live Soft Mix's Mixing Rate is a number from 1 to 100.
|
||||
#[test]
|
||||
fn ai_pathfinder_styles_never_panic(
|
||||
command in prop_oneof![(-3i64..16).prop_map(|c| c.to_string()), arb_num()],
|
||||
command in prop_oneof![(-3i64..16).prop_map(|c| c.to_string()), Just("11".to_string()), arb_num()],
|
||||
defaults in any::<bool>(),
|
||||
bare in any::<bool>(),
|
||||
mix in prop_oneof![arb_num(), prop::sample::select(vec!["0.5", "0.3", "0.01", "0.0099", "1", "1.0001", "NaN", "1e999"]).prop_map(str::to_string)],
|
||||
keys in prop::collection::vec(
|
||||
(
|
||||
prop::sample::select(vec!["Mix", "Precision", "RemovePoints", "ExtractUnpainted", "ConvertCustom", "TrapTint", "TrapThickness", "Command", "Extra"]),
|
||||
@@ -4581,7 +4585,19 @@ proptest! {
|
||||
cut in 0usize..4_000,
|
||||
) {
|
||||
use vectorcraft_testkit::ai;
|
||||
let options = if defaults {
|
||||
format!(
|
||||
"1 /Bool (ConvertCustom) ,\n1 /Bool (ExtractUnpainted) ,\n{mix} /Real (Mix) ,\n10 /Real (Precision) ,\n0 /Bool (RemovePoints) ,\n\
|
||||
1 /Real (TrapAspect) ,\n0 /Bool (TrapConvertCustom) ,\n1 /Real (TrapMaxTint) ,\n0 /Bool (TrapReverse) ,\n0.25 /Real (TrapThickness) ,\n\
|
||||
0.4 /Real (TrapTint) ,\n0.05 /Real (TrapTintTolerance) ,\n"
|
||||
)
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
// With every option there, the random keys are left out half the time, so the style can be read.
|
||||
let keys = if defaults && bare { &keys[..0] } else { &keys[..] };
|
||||
let dict: String = keys.iter().map(|(k, kind, v)| format!("{v} /{kind} ({k}) ,\n")).collect();
|
||||
let dict = format!("{options}{dict}");
|
||||
let styles = format!(
|
||||
"%AI9_BeginArtStyles\n/KnownStyle :\n(PF) /Name ,\n/ActiveStyle :\n/CompoundFilter :\n(Chain Style Filter) 0 0 /Filter ,\n1 /Visible ,\n\
|
||||
/BasicFilter :\n(Adobe Pathfinder) 1 0 /Filter ,\n{} /Visible ,\n/Dictionary : /NotRecorded ,\n{command} /Int (Command) ,\n{dict}; /Dict ;\n /Part ,\n\
|
||||
@@ -4593,12 +4609,17 @@ proptest! {
|
||||
let group = "0 Ae\nu\n1 0 0 1 0 0 Xa\n10 10 m\n10 60 L\n80 60 L\n80 10 L\nf\nU\n\
|
||||
%_0 Ae\n%_u\n%_1 0 0 1 0 0 Xa\n%_10 10 m\n%_10 50 L\n%_50 50 L\n%_50 10 L\n%_f\n%_0 0 1 0 0 0 1 Xa\n%_40 20 m\n%_40 60 L\n%_80 60 L\n%_80 20 L\n%_f\n%_U\n1 (PF) XW\n";
|
||||
let data = ai::editing_data(200.0, 100.0, &format!("{styles}{}", ai::layer("L", group)));
|
||||
let live = command.parse::<f64>().is_ok_and(|c| c == 0.0);
|
||||
let live = command.parse::<f64>().is_ok_and(|c| (0.0..=11.0).contains(&c) && c.fract() == 0.0);
|
||||
let check = |d: Option<Document>| {
|
||||
if let Some(d) = &d
|
||||
&& !live
|
||||
{
|
||||
d.walk(|n| assert!(!n.appearance.effects.iter().any(|e| e.id.starts_with("pathfinder.")), "Command {command}: {:?}", n.appearance.effects));
|
||||
if let Some(d) = &d {
|
||||
d.walk(|n| {
|
||||
let effects = &n.appearance.effects;
|
||||
assert!(live || !effects.iter().any(|e| e.id.starts_with("pathfinder.")), "Command {command}: {effects:?}");
|
||||
for e in effects.iter().filter(|e| e.id == "pathfinder.softMix") {
|
||||
let rate = e.params.get("mixingRate").and_then(Value::as_f64);
|
||||
assert!(rate.is_some_and(|r| (1.0..=100.0).contains(&r)), "Mix {mix}: {effects:?}");
|
||||
}
|
||||
});
|
||||
}
|
||||
d
|
||||
};
|
||||
|
||||
@@ -111,12 +111,32 @@ pub(super) fn warp_style(n: i64) -> Option<&'static str> {
|
||||
id.strip_prefix("warp.")
|
||||
}
|
||||
|
||||
/// The keys of an Effect › Pathfinder › Add filter's dictionary besides `DisplayString`, each with
|
||||
/// the kind and the one value read: `Command` 0 is Add, and the rest are its Pathfinder Options,
|
||||
/// Soft Mix and Trap settings
|
||||
/// (https://community.adobe.com/questions-652/liveeffect-xml-effect-is-added-under-the-object-795137).
|
||||
const PATHFINDER_ADD: [(&str, Kind, f64); 13] = [
|
||||
("Command", Kind::Int, 0.0),
|
||||
/// The Effect › Pathfinder effects by the `Command` number of their filter. `Command` 0 is Add
|
||||
/// (https://community.adobe.com/questions-652/liveeffect-xml-effect-is-added-under-the-object-795137);
|
||||
/// the numbers 1 to 11 are VectorCraft's own reader rules, learned from saved files. Trap (12) isn't
|
||||
/// read.
|
||||
const PATHFINDER_COMMANDS: [&str; 12] = [
|
||||
"pathfinder.add",
|
||||
"pathfinder.intersect",
|
||||
"pathfinder.exclude",
|
||||
"pathfinder.subtract",
|
||||
"pathfinder.minusBack",
|
||||
"pathfinder.divide",
|
||||
"pathfinder.outline",
|
||||
"pathfinder.trim",
|
||||
"pathfinder.merge",
|
||||
"pathfinder.crop",
|
||||
"pathfinder.hardMix",
|
||||
"pathfinder.softMix",
|
||||
];
|
||||
|
||||
/// The keys of an Effect › Pathfinder filter's dictionary besides `Command` and `DisplayString`,
|
||||
/// each with the kind and the one value read: its Pathfinder Options, Soft Mix's mix and the Trap
|
||||
/// settings. Every operation needs each of them at this value, even where the operation doesn't use
|
||||
/// it, except that Soft Mix reads a `Mix` from 0.01 to 1 as its Mixing Rate (VectorCraft's own
|
||||
/// reader rule, learned from saved files: the rate is `Mix` times 100, within the effect's 1 to 100;
|
||||
/// a saved `Mix` outside that range keeps the drawn look).
|
||||
const PATHFINDER_OPTIONS: [(&str, Kind, f64); 12] = [
|
||||
("ConvertCustom", Kind::Bool, 1.0),
|
||||
("ExtractUnpainted", Kind::Bool, 1.0),
|
||||
("Mix", Kind::Real, 0.5),
|
||||
@@ -1055,18 +1075,38 @@ fn effect(f: &Filter, under: Under) -> Option<Effect> {
|
||||
Some(Effect { id: id.into(), params, visible: f.visible })
|
||||
}
|
||||
|
||||
/// Effect › Pathfinder › Add, when every key of [`PATHFINDER_ADD`] is there with its kind and value
|
||||
/// and the only other key is the `DisplayString` text, each written once. `None` for anything else,
|
||||
/// which keeps the drawn look.
|
||||
/// The Effect › Pathfinder effect of [`PATHFINDER_COMMANDS`] that an integer `Command` names, when
|
||||
/// every key of [`PATHFINDER_OPTIONS`] is there with its kind and value (Soft Mix's `Mix` from 0.01
|
||||
/// to 1, as its Mixing Rate in percent) and the only other key is the `DisplayString` text, each
|
||||
/// written once. `None` for anything else, which keeps the drawn look.
|
||||
fn pathfinder_effect(f: &Filter) -> Option<Effect> {
|
||||
let known = f.kinds.iter().all(|(key, kind)| (key == "DisplayString" && *kind == Kind::Other) || PATHFINDER_ADD.iter().any(|(k, ..)| k == key));
|
||||
let known = f
|
||||
.kinds
|
||||
.iter()
|
||||
.all(|(key, kind)| (key == "DisplayString" && *kind == Kind::Other) || key == "Command" || PATHFINDER_OPTIONS.iter().any(|(k, ..)| k == key));
|
||||
if !known || f.repeated || !f.arrays.is_empty() || f.fill.is_some() || f.stroke.is_some() || f.gradient || f.pattern.is_some() {
|
||||
return None;
|
||||
}
|
||||
if !PATHFINDER_ADD.iter().all(|(key, kind, value)| f.kinds.get(*key) == Some(kind) && f.params.get(*key) == Some(value)) {
|
||||
if f.kinds.get("Command") != Some(&Kind::Int) {
|
||||
return None;
|
||||
}
|
||||
Some(Effect { id: "pathfinder.add".into(), params: json!({}), visible: f.visible })
|
||||
let command = f.params.get("Command").copied().filter(|c| c.is_finite() && c.fract() == 0.0 && *c >= 0.0)?;
|
||||
let id = *PATHFINDER_COMMANDS.get(usize::try_from(command as i64).ok()?)?;
|
||||
let soft_mix = id == "pathfinder.softMix";
|
||||
let options = PATHFINDER_OPTIONS
|
||||
.iter()
|
||||
.all(|(key, kind, value)| f.kinds.get(*key) == Some(kind) && ((soft_mix && *key == "Mix") || f.params.get(*key) == Some(value)));
|
||||
if !options {
|
||||
return None;
|
||||
}
|
||||
let params = if soft_mix {
|
||||
let mix = f.params.get("Mix").copied().filter(|m| (0.01..=1.0).contains(m))?;
|
||||
// Rounded to six decimals: a `Mix` of 0.3 is a rate of 30.
|
||||
json!({"mixingRate": (mix * 100.0 * 1e6).round() / 1e6})
|
||||
} else {
|
||||
json!({})
|
||||
};
|
||||
Some(Effect { id: id.into(), params, visible: f.visible })
|
||||
}
|
||||
|
||||
/// The fill or stroke basic filter `f` paints, `Ok(None)` when it paints nothing, `Err` when it
|
||||
|
||||
@@ -516,8 +516,46 @@ fn pathfinder_add_is_read_as_its_effect() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pathfinder_commands_other_than_add_keep_the_drawn_look() {
|
||||
for command in 1..=12 {
|
||||
fn each_pathfinder_command_is_read_as_its_effect() {
|
||||
// GUARDS: only `Command` 0 (Add) was read; a group with any other operation came in as its drawn look.
|
||||
let ids = [
|
||||
(1, "pathfinder.intersect"),
|
||||
(2, "pathfinder.exclude"),
|
||||
(3, "pathfinder.subtract"),
|
||||
(4, "pathfinder.minusBack"),
|
||||
(5, "pathfinder.divide"),
|
||||
(6, "pathfinder.outline"),
|
||||
(7, "pathfinder.trim"),
|
||||
(8, "pathfinder.merge"),
|
||||
(9, "pathfinder.crop"),
|
||||
(10, "pathfinder.hardMix"),
|
||||
];
|
||||
for (command, id) in ids {
|
||||
let st = group_style(&[pathfinder(&pathfinder_keys(&command.to_string(), "0.5"))]);
|
||||
assert!(st.plain && st.simple && st.pathfinder, "{command}: {st:?}");
|
||||
let [e] = st.effects.as_slice() else { panic!("{command}: {st:?}") };
|
||||
assert_eq!((e.id.as_str(), &e.params, e.visible), (id, &json!({}), true), "{command}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn soft_mix_is_read_with_its_mixing_rate() {
|
||||
for (mix, rate) in [("0.5", 50.0), ("0.3", 30.0), ("0.01", 1.0), ("1", 100.0)] {
|
||||
let st = group_style(&[pathfinder(&pathfinder_keys("11", mix))]);
|
||||
assert!(st.plain && st.simple && st.pathfinder, "{mix}: {st:?}");
|
||||
let [e] = st.effects.as_slice() else { panic!("{mix}: {st:?}") };
|
||||
assert_eq!((e.id.as_str(), &e.params), ("pathfinder.softMix", &json!({"mixingRate": rate})), "{mix}");
|
||||
}
|
||||
// A rate the effect doesn't take keeps the drawn look.
|
||||
for mix in ["0", "0.005", "1.5", "-0.5", "1e999"] {
|
||||
let st = group_style(&[pathfinder(&pathfinder_keys("11", mix))]);
|
||||
assert!(!st.plain && !st.pathfinder && st.effects.is_empty(), "{mix}: {st:?}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn trap_and_unknown_pathfinder_commands_keep_the_drawn_look() {
|
||||
for command in [12, 13, 40] {
|
||||
let st = group_style(&[pathfinder(&pathfinder_keys(&command.to_string(), "0.5"))]);
|
||||
assert!(!st.plain && !st.pathfinder && st.effects.is_empty(), "{command}: {st:?}");
|
||||
}
|
||||
@@ -623,7 +661,15 @@ fn pathfinder_settings_this_does_not_read_keep_the_drawn_look() {
|
||||
("unpainted art kept", add.replace("1 /Bool (ExtractUnpainted)", "0 /Bool (ExtractUnpainted)")),
|
||||
("other trap tint", add.replace("0.4 /Real (TrapTint)", "0.25 /Real (TrapTint)")),
|
||||
("Mix on Add", add.replace("0.5 /Real (Mix)", "0.3 /Real (Mix)")),
|
||||
("Mix on Intersect", pathfinder_keys("1", "0.3")),
|
||||
("Mix on Hard Mix", pathfinder_keys("10", "0.3")),
|
||||
("Mix as a bool", add.replace("0.5 /Real (Mix)", "1 /Bool (Mix)")),
|
||||
("Soft Mix with Mix as an int", pathfinder_keys("11", "0.5").replace("0.5 /Real (Mix)", "1 /Int (Mix)")),
|
||||
("Soft Mix without Mix", pathfinder_keys("11", "0.5").replace("0.5 /Real (Mix) ,\n", "")),
|
||||
("Soft Mix with other Precision", pathfinder_keys("11", "0.3").replace("10 /Real (Precision)", "5 /Real (Precision)")),
|
||||
("Intersect with other Precision", pathfinder_keys("1", "0.5").replace("10 /Real (Precision)", "5 /Real (Precision)")),
|
||||
("Divide keeping unpainted art", pathfinder_keys("5", "0.5").replace("1 /Bool (ExtractUnpainted)", "0 /Bool (ExtractUnpainted)")),
|
||||
("Command twice, Intersect then Add", add.replace("0 /Int (Command)", "1 /Int (Command) ,\n0 /Int (Command)")),
|
||||
];
|
||||
for (case, keys) in cases {
|
||||
let st = group_style(&[pathfinder(&keys)]);
|
||||
|
||||
@@ -5153,11 +5153,69 @@ fn a_group_with_a_pathfinder_effect_keeps_its_members_and_the_effect() {
|
||||
assert!(!r.warnings.iter().any(|w| w.contains("drawn look")), "{:?}", r.warnings);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn each_pathfinder_operation_on_a_group_draws_as_vectorcraft_draws_it() {
|
||||
// GUARDS: only Add (`Command` 0) was read; the other operations came in as the drawn look's one path.
|
||||
let ops = [
|
||||
(1, "pathfinder.intersect"),
|
||||
(2, "pathfinder.exclude"),
|
||||
(3, "pathfinder.subtract"),
|
||||
(4, "pathfinder.minusBack"),
|
||||
(5, "pathfinder.divide"),
|
||||
(6, "pathfinder.outline"),
|
||||
(7, "pathfinder.trim"),
|
||||
(8, "pathfinder.merge"),
|
||||
(9, "pathfinder.crop"),
|
||||
(10, "pathfinder.hardMix"),
|
||||
(11, "pathfinder.softMix"),
|
||||
];
|
||||
for (command, id) in ops {
|
||||
let mix = if command == 11 { 0.3 } else { 0.5 };
|
||||
let r = read_ok(&format!("{}{}", pathfinder_styles(command, mix), layer(&pathfinder_group())));
|
||||
let a = art(&r.doc);
|
||||
assert_eq!(a.len(), 1, "{command}: {a:#?}");
|
||||
let members = a[0].children().cloned().unwrap_or_default();
|
||||
assert_eq!(members.len(), 2, "{command}: the squares, not the look: {a:#?}");
|
||||
let [e] = a[0].appearance.effects.as_slice() else { panic!("{command}: {:?}", a[0].appearance) };
|
||||
assert_eq!((e.id.as_str(), e.visible), (id, true), "{command}");
|
||||
let params = if command == 11 { serde_json::json!({"mixingRate": 30.0}) } else { serde_json::json!({}) };
|
||||
assert_eq!(e.params, params, "{command}");
|
||||
assert!(!r.warnings.iter().any(|w| w.contains("drawn look")), "{command}: {:?}", r.warnings);
|
||||
// The same squares in a group made in VectorCraft, with the effect of that operation.
|
||||
let square = |m: &Arc<Node>| {
|
||||
let bounds = m.geometric_bounds().unwrap();
|
||||
Arc::new(Node::path(m.id, vectorcraft_geom::shapes::rectangle(bounds), vectorcraft_doc::Appearance::basic(fill(m), Paint::None, 0.0)))
|
||||
};
|
||||
let mut own = Node::group(a[0].id, members.iter().map(square).collect());
|
||||
own.appearance.effects.push(vectorcraft_doc::Effect { id: id.into(), params: params.clone(), visible: true });
|
||||
// Each result's box and paints; Outline's lines start where each square's path starts, so
|
||||
// the results are compared in a sorted order.
|
||||
let faces = |g: &Node| -> Vec<String> {
|
||||
let mut faces: Vec<String> = vectorcraft_effects::pathfinder_children(g, None)
|
||||
.unwrap()
|
||||
.iter()
|
||||
.map(|n| {
|
||||
let b = n.geometric_bounds().unwrap_or_default();
|
||||
let round = |v: f64| (v * 1000.0).round() / 1000.0;
|
||||
let paints = (n.appearance.fill_paint().color(), n.appearance.stroke_paint().color());
|
||||
format!("{:?} {paints:?}", Rect::new(round(b.x0), round(b.y0), round(b.x1), round(b.y1)))
|
||||
})
|
||||
.collect();
|
||||
faces.sort();
|
||||
faces
|
||||
};
|
||||
let (opened, made) = (faces(&a[0]), faces(&own));
|
||||
assert!(!made.is_empty(), "{command}");
|
||||
assert_eq!(opened, made, "{command}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_pathfinder_style_this_does_not_read_keeps_the_drawn_look() {
|
||||
// Another command (Intersect, Trap), and a Precision other than the one read.
|
||||
// Trap, and a Precision other than the one read.
|
||||
let other = pathfinder_styles(0, 0.5).replace("10 /Real (Precision)", "5 /Real (Precision)");
|
||||
for styles in [pathfinder_styles(1, 0.5), pathfinder_styles(12, 0.5), other] {
|
||||
let intersect = pathfinder_styles(1, 0.5).replace("10 /Real (Precision)", "5 /Real (Precision)");
|
||||
for styles in [pathfinder_styles(12, 0.5), other, intersect] {
|
||||
let r = read_ok(&format!("{styles}{}", layer(&pathfinder_group())));
|
||||
let a = art(&r.doc);
|
||||
assert_eq!(a.len(), 1, "{a:#?}");
|
||||
|
||||
@@ -1214,7 +1214,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur
|
||||
A PDF saved with Illustrator's editing data opens through it as a PDF-compatible `.ai` does, with its layers and groups and with art reaching past its page unclipped; such a PDF holds the data in `AIPDFPrivateData` streams (part of #868). A shape a PDF or `.ai` paints with a gradient (a clip path round a shading) opens as one path filled with the gradient, not a clip group round a rectangle, so Shape Builder, Live Paint and Pathfinder work on its outline (#1304).
|
||||
In a `.ai` opened from its editing data, a text object on a layer that shows and doesn't print stays on that layer, made from the file's text document, even where the page doesn't draw it (`crates/eps/src/import/tests_native.rs`). Such a `.ai` opens without the PDF part's warnings that text was kept as outlines when no outlined glyphs come in, and a warning counts the text objects that hold the page's type next to glyphs the page has only as outlines, which don't come in (`crates/engine/src/cmd/fileio/tests_aiimport.rs`). Where the art of a `.ai` or EPS opened from its editing data differs a little from the file's page, the warning gives the share of the page that differs away from type. When that share is 0.2% or less, the warning names the type and gives the share of the page that differs where the type is; when all the type is made from the text document, the type's share has a warning of its own beside the art's (`crates/eps/src/import/tests_native.rs`).
|
||||
In a `.ai` or EPS opened from its editing data, where a layer's or group's drawn look (an effect can be attached to a group or a layer, [Vector and raster effects](https://helpx.adobe.com/illustrator/desktop/special-effects-styles/apply-filter-effects/vector-and-raster-effects.html)) draws a copy of art with type, the import puts the page's type that lies over the copy's other art into the copy's group, instead of giving it to other text objects by painting order. Type beside the copy's art, and every copy's type in a file past the import's text-object limits, still goes by painting order. Without a page, such a copy has no type, and no warning counts it as left out. How the page's type is matched with a copy is VectorCraft's own rule (`crates/eps/src/import/tests_native.rs`).
|
||||
A `.ai` opened from its editing data keeps Effect › Pathfinder on a group with no fills or strokes of its own as a live effect, with the group's members editable: Add, shown or hidden, in its order among the group's effects. The other operations, Pathfinder Options values other than the ones the reader accepts, and the effect on a path, on type, or on a fill or stroke open as the drawn look (`crates/eps/src/import/ai/style.rs`, `pathfinder_effect`).
|
||||
A `.ai` opened from its editing data keeps Effect › Pathfinder on a group with no fills or strokes of its own as a live effect, with the group's members editable ([Pathfinder effects](https://helpx.adobe.com/illustrator/desktop/manage-objects/reshape-transform-objects/combine-objects-using-pathfinder-effects.html)): Add, Intersect, Exclude, Subtract, Minus Back, Divide, Outline, Trim, Merge, Crop, Hard Mix and Soft Mix (with its Mixing Rate), shown or hidden, in its order among the group's effects. Which saved `Command` number names each operation other than Add, and reading Soft Mix's `Mix` from 0.01 to 1 as a Mixing Rate of `Mix` times 100, are VectorCraft's own reader rules. Trap, and the effect on a path, on type, on a layer or on a fill or stroke, open as the drawn look. So does any operation whose saved Pathfinder Options (Precision, Remove Redundant Points, Divide and Outline Will Remove Unpainted Artwork), `ConvertCustom`, `Mix` or Trap settings differ from the values the reader accepts (one value for each key, and Soft Mix's `Mix` range), although only Soft Mix uses `Mix` and none of the operations read here uses the Trap settings (`crates/eps/src/import/ai/style.rs`, `pathfinder_effect`).
|
||||
PDF text opens with every glyph where the file draws it (#722): the font's kerning or none, whichever the file followed, word and character spacing, `TJ` adjustments and horizontal scaling kept per character as tracking, text rise as baseline shift within its line, vertical glyphs narrower than an em centred on their column (`/W2`, `/W`, `/DW`), justified text boxes with word spacing back as justified area type (`tests_text_placement`, fixtures written from ISO 32000-1 §9).
|
||||
A `.ai` or PDF whose editing data holds a readable variable library opens with its variable definitions, data-set rows, text, visibility and linked-file values, active row and lock in the Variables panel, whether its layers are read or it comes in as its PDF part; graph values and malformed entries are skipped with a note. With its layers read, the objects its text, visibility and image variables are bound to (type for a text variable, any object for a visibility variable, a linked image for an image variable, found by their XML ids) come in bound, so selecting a data set changes them; locked and hidden objects, objects in a locked or hidden layer or group and objects on a locked artboard keep their bindings, and a data set leaves the locked ones as they are. Bindings whose object didn't come in or is of another kind are skipped with a note, as are graph bindings and every binding of a file that comes in as its PDF part (`crates/eps/src/import/ai/tests.rs`, `crates/eps/src/import/tests_native.rs`, `crates/engine/src/cmd/fileio/tests_aiimport.rs`).
|
||||
A `.ai` or EPS opened from its editing data keeps each artboard's center mark, cross hairs, video safe areas and video ruler pixel aspect ratio, and the file's Document Raster Effects Settings; the author comes from the header's `%%For` (the editing data's, else an EPS's own) when the file's File Info gives none, the header's custom colors that no swatch has come in as spot swatches, a named color the file defines takes the place of a default swatch of that name when the file has no palette, and an older file's `%%TemplateBox` places its artboard. A value of another type or out of range keeps its default (`crates/eps/src/import/ai/tests.rs`, `crates/eps/src/import/tests_native.rs`, `crates/engine/src/cmd/fileio/tests_aiimport.rs`).
|
||||
|
||||
Reference in New Issue
Block a user