mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-10 09:59:47 -06:00
.ai import: Convert to Shape opens live on paths and type
Squashed from #5142 onto main.
This commit is contained in:
@@ -59,6 +59,7 @@ const OFFSET: &str = "Adobe Offset Path"; // brand-ok: the filter's name in the
|
||||
const TRANSFORM: &str = "Adobe Transform"; // brand-ok: the filter's name in the files
|
||||
const GAUSSIAN: &str = "Adobe PSL Gaussian Blur"; // brand-ok: the filter's name in the files
|
||||
const BAS_RELIEF: &str = "PSAdapter_plugin_BsRl";
|
||||
const SHAPE_EFFECTS: &str = "Adobe Shape Effects"; // brand-ok: the filter's name in the files
|
||||
|
||||
const CHAIN: &str = "Chain Style Filter";
|
||||
const STACK: &str = "Stack Style Filter";
|
||||
@@ -99,7 +100,7 @@ pub(super) struct ArtStyle {
|
||||
pub plain: bool,
|
||||
/// It is one fill and one stroke at most, in solid colours or the object's own gradient, with
|
||||
/// no transparency or effects of their own, and only Drop Shadow, Inner Glow, Outer Glow,
|
||||
/// Feather, Round Corners and Scribble on the object: what any object is read with. Type takes the other plain styles too (its drawn look
|
||||
/// Feather, Round Corners, Scribble and Convert to Shape on the object: what any object is read with. Type takes the other plain styles too (its drawn look
|
||||
/// can't be retyped); other objects keep their drawn look, which is the app's own rendering.
|
||||
pub simple: bool,
|
||||
/// Its fills and strokes, bottom first.
|
||||
@@ -326,6 +327,11 @@ pub(super) fn art_styles(data: &[u8]) -> HashMap<String, ArtStyle> {
|
||||
Tok::Word(w) => {
|
||||
let entry = other_entry.take().flatten();
|
||||
let op = std::str::from_utf8(w).unwrap_or_default();
|
||||
if op == ","
|
||||
&& let (Some(key), Some(f)) = (entry, filters.last_mut())
|
||||
{
|
||||
f.kinds.insert(key, Kind::Other);
|
||||
}
|
||||
if op == "[" {
|
||||
array = Some(vec![]);
|
||||
continue;
|
||||
@@ -609,6 +615,55 @@ fn stylize(f: &Filter) -> Option<Option<(&'static str, serde_json::Value)>> {
|
||||
})
|
||||
}
|
||||
|
||||
/// The largest size or radius Convert to Shape reads, in points.
|
||||
const MAX_SHAPE_SIZE: f64 = 1e5;
|
||||
|
||||
/// Convert to Shape: Rectangle, Rounded Rectangle or Ellipse, with an absolute size or one relative
|
||||
/// to the object, and a corner radius
|
||||
/// (https://helpx.adobe.com/illustrator/desktop/manage-objects/reshape-transform-objects/distort-and-transform-effects-in-illustrator.html).
|
||||
/// The keys are LiveEffect XML's: `Shape` 0 Rectangle, 1 Rounded Rectangle, 2 Ellipse
|
||||
/// (https://community.adobe.com/questions-652/effect-convert-to-shape-ellipse-in-javascript-761645),
|
||||
/// `Absolute` 1 for an absolute `AbsWidth` × `AbsHeight`, else `RelWidth` and `RelHeight` added to
|
||||
/// each side, and `CornerRadius`; a missing size is 18 pt a side or 36 pt, and a missing radius
|
||||
/// 9 pt. `None`: a key, a kind or a value that isn't read, which keeps the drawn look.
|
||||
fn shape_effect(f: &Filter) -> Option<Effect> {
|
||||
const SIZES: [&str; 5] = ["AbsWidth", "AbsHeight", "RelWidth", "RelHeight", "CornerRadius"];
|
||||
let known = f.kinds.iter().all(|(key, kind)| match key.as_str() {
|
||||
"DisplayString" => *kind == Kind::Other,
|
||||
"Shape" => *kind == Kind::Int,
|
||||
key => (key == "Absolute" || SIZES.contains(&key)) && *kind == Kind::Real,
|
||||
});
|
||||
if !known || !f.arrays.is_empty() || f.fill.is_some() || f.stroke.is_some() || f.gradient || f.pattern.is_some() {
|
||||
return None;
|
||||
}
|
||||
// A written size is a finite number from `min`; a missing one is `default`.
|
||||
let size = |key: &str, min: f64, default: f64| match f.kinds.get(key) {
|
||||
None => Some(default),
|
||||
Some(_) => f.params.get(key).copied().filter(|v| v.is_finite() && (min..=MAX_SHAPE_SIZE).contains(v)),
|
||||
};
|
||||
let shape = f.params.get("Shape").copied().filter(|_| f.kinds.get("Shape") == Some(&Kind::Int))?;
|
||||
let absolute = match f.params.get("Absolute").copied() {
|
||||
Some(0.0) => false,
|
||||
Some(1.0) => true,
|
||||
_ => return None,
|
||||
};
|
||||
let (rel_w, rel_h) = (size("RelWidth", -MAX_SHAPE_SIZE, 18.0)?, size("RelHeight", -MAX_SHAPE_SIZE, 18.0)?);
|
||||
let (abs_w, abs_h, radius) = (size("AbsWidth", 0.0, 36.0)?, size("AbsHeight", 0.0, 36.0)?, size("CornerRadius", 0.0, 9.0)?);
|
||||
let id = match shape {
|
||||
0.0 => "convertToShape.rectangle",
|
||||
1.0 => "convertToShape.roundedRectangle",
|
||||
// A relative Ellipse in the file is drawn round the enlarged box, and VectorCraft's inside
|
||||
// it: it keeps its drawn look.
|
||||
2.0 if absolute => "convertToShape.ellipse",
|
||||
_ => return None,
|
||||
};
|
||||
let mut params = json!({"relative": !absolute, "extraW": 2.0 * rel_w, "extraH": 2.0 * rel_h, "width": abs_w, "height": abs_h});
|
||||
if id == "convertToShape.roundedRectangle" {
|
||||
params["radius"] = json!(radius);
|
||||
}
|
||||
Some(Effect { id: id.into(), params, visible: f.visible })
|
||||
}
|
||||
|
||||
/// The live effect filter `f` is, when it is one read here.
|
||||
fn effect(f: &Filter) -> Option<Effect> {
|
||||
if let Some(stylized) = stylize(f) {
|
||||
@@ -662,6 +717,7 @@ fn effect(f: &Filter) -> Option<Effect> {
|
||||
GAUSSIAN => ("blur.gaussian", json!({"radius": param("blur").unwrap_or(5.0).clamp(0.0, 1000.0)})),
|
||||
// Its settings are in a Photoshop plug-in's binary block: the effect's defaults stand in.
|
||||
BAS_RELIEF => ("sketch.basRelief", json!({"detail": 13.0, "smoothness": 3.0, "lightDirection": "bottom"})),
|
||||
SHAPE_EFFECTS => return shape_effect(f),
|
||||
_ => return None,
|
||||
};
|
||||
Some(Effect { id: id.into(), params, visible: f.visible })
|
||||
@@ -748,9 +804,11 @@ impl Builder<'_> {
|
||||
(look.blend, look.opacity) = (super::read::blend_mode(mode), opacity);
|
||||
}
|
||||
}
|
||||
// Convert to Shape on one fill or stroke isn't read yet: what it draws there is
|
||||
// the drawn look's.
|
||||
_ => match effect(c) {
|
||||
Some(e) => look.effects.push(e),
|
||||
None => self.plain = false,
|
||||
Some(e) if !e.id.starts_with("convertToShape.") => look.effects.push(e),
|
||||
_ => self.plain = false,
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -817,7 +875,15 @@ fn style(filters: &[Filter]) -> ArtStyle {
|
||||
&& st.effects.iter().all(|e| {
|
||||
matches!(
|
||||
e.id.as_str(),
|
||||
"stylize.dropShadow" | "stylize.innerGlow" | "stylize.outerGlow" | "stylize.feather" | "stylize.roundCorners" | "stylize.scribble"
|
||||
"stylize.dropShadow"
|
||||
| "stylize.innerGlow"
|
||||
| "stylize.outerGlow"
|
||||
| "stylize.feather"
|
||||
| "stylize.roundCorners"
|
||||
| "stylize.scribble"
|
||||
| "convertToShape.rectangle"
|
||||
| "convertToShape.roundedRectangle"
|
||||
| "convertToShape.ellipse"
|
||||
)
|
||||
});
|
||||
st
|
||||
|
||||
@@ -469,3 +469,139 @@ fn a_hidden_fill_comes_in_hidden() {
|
||||
assert!(st.plain, "{st:?}");
|
||||
assert_eq!(st.looks.iter().map(|l| l.visible).collect::<Vec<_>>(), [false, true]);
|
||||
}
|
||||
|
||||
/// A Convert to Shape filter whose dictionary holds `keys`.
|
||||
fn shape(keys: &str) -> String {
|
||||
basic("Adobe Shape Effects", &format!("(Shape) /UnicodeString (DisplayString) ,\n{keys}"))
|
||||
}
|
||||
|
||||
/// The style `Boxed`: a fill and a stroke, and `effects` on the whole object after them.
|
||||
fn shape_style(effects: &[String]) -> ArtStyle {
|
||||
let stack = compound(
|
||||
"Stack Style Filter",
|
||||
&[
|
||||
basic("Stroke Style Filter", "/StrokeStyle : 0 R\n0 0 0 1 0 0 0 XA\n0 J 0 j 2 w 10 M []0 d\n0 XR\n1 0 Xd\n/Def ;\n (StrokeStyle) ,\n"),
|
||||
fill("0 0 0 0 1 1 1 Xa"),
|
||||
],
|
||||
);
|
||||
let mut kids = vec![stack];
|
||||
kids.extend_from_slice(effects);
|
||||
let data = format!("/KnownStyle :\n(Boxed) /Name ,\n/ActiveStyle :\n{}", compound("Chain Style Filter", &kids));
|
||||
art_styles(data.as_bytes()).remove("Boxed").unwrap_or_default()
|
||||
}
|
||||
|
||||
/// Every setting of a Relative or Absolute size, with a corner radius.
|
||||
const SIZES: &str = "150 /Real (AbsWidth) ,\n40 /Real (AbsHeight) ,\n20 /Real (RelWidth) ,\n6 /Real (RelHeight) ,\n15 /Real (CornerRadius) ,\n";
|
||||
|
||||
#[test]
|
||||
fn convert_to_shape_comes_in_live_with_its_settings() {
|
||||
// GUARDS: `Adobe Shape Effects` wasn't read, so an object with it kept its drawn look.
|
||||
for (keys, id, want) in [
|
||||
(
|
||||
format!("0 /Int (Shape) ,\n0 /Real (Absolute) ,\n{SIZES}"),
|
||||
"convertToShape.rectangle",
|
||||
json!({"relative": true, "extraW": 40.0, "extraH": 12.0, "width": 150.0, "height": 40.0}),
|
||||
),
|
||||
(
|
||||
format!("1 /Int (Shape) ,\n0 /Real (Absolute) ,\n{SIZES}"),
|
||||
"convertToShape.roundedRectangle",
|
||||
json!({"relative": true, "extraW": 40.0, "extraH": 12.0, "width": 150.0, "height": 40.0, "radius": 15.0}),
|
||||
),
|
||||
(
|
||||
format!("2 /Int (Shape) ,\n1 /Real (Absolute) ,\n{SIZES}"),
|
||||
"convertToShape.ellipse",
|
||||
json!({"relative": false, "extraW": 40.0, "extraH": 12.0, "width": 150.0, "height": 40.0}),
|
||||
),
|
||||
(
|
||||
format!("0 /Int (Shape) ,\n1 /Real (Absolute) ,\n{SIZES}"),
|
||||
"convertToShape.rectangle",
|
||||
json!({"relative": false, "extraW": 40.0, "extraH": 12.0, "width": 150.0, "height": 40.0}),
|
||||
),
|
||||
// A relative size alone, and one that shrinks the shape: each is added to every side.
|
||||
(
|
||||
"0 /Int (Shape) ,\n0 /Real (Absolute) ,\n20 /Real (RelWidth) ,\n6 /Real (RelHeight) ,\n".to_string(),
|
||||
"convertToShape.rectangle",
|
||||
json!({"relative": true, "extraW": 40.0, "extraH": 12.0, "width": 36.0, "height": 36.0}),
|
||||
),
|
||||
(
|
||||
"0 /Int (Shape) ,\n0 /Real (Absolute) ,\n-10 /Real (RelWidth) ,\n-4 /Real (RelHeight) ,\n".to_string(),
|
||||
"convertToShape.rectangle",
|
||||
json!({"relative": true, "extraW": -20.0, "extraH": -8.0, "width": 36.0, "height": 36.0}),
|
||||
),
|
||||
// Missing sizes: 18 pt a side, 36 pt, and a 9 pt radius.
|
||||
(
|
||||
"0 /Int (Shape) ,\n0 /Real (Absolute) ,\n20 /Real (RelWidth) ,\n".to_string(),
|
||||
"convertToShape.rectangle",
|
||||
json!({"relative": true, "extraW": 40.0, "extraH": 36.0, "width": 36.0, "height": 36.0}),
|
||||
),
|
||||
(
|
||||
"1 /Int (Shape) ,\n1 /Real (Absolute) ,\n".to_string(),
|
||||
"convertToShape.roundedRectangle",
|
||||
json!({"relative": false, "extraW": 36.0, "extraH": 36.0, "width": 36.0, "height": 36.0, "radius": 9.0}),
|
||||
),
|
||||
] {
|
||||
let st = shape_style(&[shape(&keys)]);
|
||||
assert!(st.plain && st.simple, "{keys}: {st:?}");
|
||||
let [e] = st.effects.as_slice() else { panic!("{keys}: {:?}", st.effects) };
|
||||
assert_eq!((e.id.as_str(), &e.params, e.visible), (id, &want, true), "{keys}");
|
||||
assert_eq!(st.items.len(), 2, "{keys}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn convert_to_shape_keeps_its_place_among_effects_and_its_visibility() {
|
||||
let offset = basic("Adobe Offset Path", "2 /Int (jntp) ,\n4 /Real (mlim) ,\n4 /Real (ofst) ,\n");
|
||||
let st = shape_style(&[shape(&format!("0 /Int (Shape) ,\n0 /Real (Absolute) ,\n{SIZES}")), offset.clone()]);
|
||||
assert!(st.plain, "{st:?}");
|
||||
let ids: Vec<_> = st.effects.iter().map(|e| e.id.as_str()).collect();
|
||||
assert_eq!(ids, ["convertToShape.rectangle", "path.offsetPath"]);
|
||||
// Offset Path isn't read on objects other than type yet.
|
||||
assert!(!st.simple);
|
||||
// A hidden one stays, hidden, with its settings.
|
||||
let hidden = shape(&format!("1 /Int (Shape) ,\n0 /Real (Absolute) ,\n{SIZES}")).replacen("1 /Visible", "0 /Visible", 1);
|
||||
let st = shape_style(&[hidden]);
|
||||
assert!(st.plain && st.simple, "{st:?}");
|
||||
let [e] = st.effects.as_slice() else { panic!("{:?}", st.effects) };
|
||||
assert_eq!(
|
||||
(e.id.as_str(), e.visible, &e.params["radius"], &e.params["extraW"]),
|
||||
("convertToShape.roundedRectangle", false, &json!(15.0), &json!(40.0))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn convert_to_shape_settings_that_arent_read_keep_the_drawn_look() {
|
||||
let valid = format!("0 /Int (Shape) ,\n0 /Real (Absolute) ,\n{SIZES}");
|
||||
assert!(shape_style(&[shape(&valid)]).plain);
|
||||
for keys in [
|
||||
// Another shape, a shape that isn't a whole number or an /Int, and a missing one.
|
||||
valid.replace("0 /Int (Shape)", "3 /Int (Shape)"),
|
||||
valid.replace("0 /Int (Shape)", "0.5 /Int (Shape)"),
|
||||
valid.replace("0 /Int (Shape)", "0 /Real (Shape)"),
|
||||
valid.replace("0 /Int (Shape) ,\n", ""),
|
||||
// A mode other than 0 and 1, or a missing one.
|
||||
valid.replace("0 /Real (Absolute)", "2 /Real (Absolute)"),
|
||||
valid.replace("0 /Real (Absolute) ,\n", ""),
|
||||
valid.replace("0 /Real (Absolute)", "0 /Int (Absolute)"),
|
||||
// Sizes that aren't finite numbers, negative absolute sizes and radii, and huge ones.
|
||||
valid.replace("20 /Real (RelWidth)", "NaN /Real (RelWidth)"),
|
||||
valid.replace("6 /Real (RelHeight)", "1e999 /Real (RelHeight)"),
|
||||
valid.replace("6 /Real (RelHeight)", "(x) /Real (RelHeight)"),
|
||||
valid.replace("150 /Real (AbsWidth)", "-150 /Real (AbsWidth)"),
|
||||
valid.replace("15 /Real (CornerRadius)", "-1 /Real (CornerRadius)"),
|
||||
valid.replace("20 /Real (RelWidth)", "1e6 /Real (RelWidth)"),
|
||||
valid.replace("40 /Real (AbsHeight)", "40 /Int (AbsHeight)"),
|
||||
// A key that isn't read.
|
||||
format!("{valid}1 /Real (Future) ,\n"),
|
||||
format!("{valid}(x) /String (Future) ,\n"),
|
||||
// A relative Ellipse.
|
||||
valid.replace("0 /Int (Shape)", "2 /Int (Shape)"),
|
||||
] {
|
||||
let st = shape_style(&[shape(&keys)]);
|
||||
assert!(!st.plain, "{keys}: {st:?}");
|
||||
}
|
||||
// On one fill: the drawn look, for type too.
|
||||
let item = compound("Chain Style Filter", &[fill("0 0 0 1 0 0 0 Xa"), shape(&valid)]);
|
||||
let data =
|
||||
format!("/KnownStyle :\n(Odd) /Name ,\n/ActiveStyle :\n{}", compound("Chain Style Filter", &[compound("Stack Style Filter", &[item])]));
|
||||
assert!(!art_styles(data.as_bytes())["Odd"].plain);
|
||||
}
|
||||
|
||||
@@ -2128,6 +2128,45 @@ fn an_object_with_a_feather_style_comes_in_with_a_live_effect() {
|
||||
assert!(!r.warnings.iter().any(|w| w.contains("drawn look")), "{:?}", r.warnings);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_object_with_convert_to_shape_comes_in_as_itself_with_the_effect_live() {
|
||||
// GUARDS: `Adobe Shape Effects` wasn't read, so the object came in as its drawn look.
|
||||
let styles = |keys: &str| {
|
||||
format!(
|
||||
"%AI9_BeginArtStyles\n\
|
||||
/KnownStyle :\n(Boxed) /Name ,\n/ActiveStyle :\n/CompoundFilter :\n(Chain Style Filter) 0 0 /Filter ,\n1 /Visible ,\n\
|
||||
/BasicFilter :\n(Adobe Shape Effects) 1 0 /Filter ,\n1 /Visible ,\n(ShapeEffect.aip) /PluginFileName ,\n(Shape) /Title ,\n\
|
||||
/Dictionary : /NotRecorded ,\n(Rectangle) /UnicodeString (DisplayString) ,\n{keys}; /Dict ;\n /Part ,\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\n0 0 0 0 0 0 1 Xa\n0 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\
|
||||
%AI9_EndArtStyles\n"
|
||||
)
|
||||
};
|
||||
let hidden_rect = rect_path(20.0, 20.0, 90.0, 60.0).replace('\n', "\n%_");
|
||||
// The drawn look is the shape: 20 pt wider on each side and 6 pt taller.
|
||||
let art_ = format!("0 Ae\nu\n{RED}{}f\nU\n%_Bb\n%_{hidden_rect}f\n%_0 BB\n1 (Boxed) XW\n", rect_path(0.0, 14.0, 110.0, 66.0));
|
||||
let keys = "0 /Int (Shape) ,\n150 /Real (AbsWidth) ,\n0 /Real (Absolute) ,\n40 /Real (AbsHeight) ,\n20 /Real (RelWidth) ,\n15 /Real (CornerRadius) ,\n6 /Real (RelHeight) ,\n";
|
||||
let r = read_ok(&format!("{}{}", styles(keys), layer(&art_)));
|
||||
assert!(!r.warnings.iter().any(|w| w.contains("drawn look")), "{:?}", r.warnings);
|
||||
let a = art(&r.doc);
|
||||
assert_eq!(a.len(), 1, "{a:#?}");
|
||||
assert!(matches!(a[0].kind, NodeKind::Path { .. }), "the object, not its look: {a:#?}");
|
||||
let b = a[0].geometric_bounds().unwrap();
|
||||
assert_eq!((b.width(), b.height()), (70.0, 40.0), "the object keeps its own path");
|
||||
assert_eq!(fill(&a[0]).color(), Some(Color::rgb(0.0, 0.0, 1.0)));
|
||||
let [e] = a[0].appearance.effects.as_slice() else { panic!("{:?}", a[0].appearance.effects) };
|
||||
assert_eq!(
|
||||
(e.id.as_str(), e.params["relative"].as_bool(), e.params["extraW"].as_f64(), e.params["extraH"].as_f64()),
|
||||
("convertToShape.rectangle", Some(true), Some(40.0), Some(12.0))
|
||||
);
|
||||
// A shape that isn't read: the drawn look.
|
||||
let r = read_ok(&format!("{}{}", styles(&keys.replace("0 /Int (Shape)", "7 /Int (Shape)")), layer(&art_)));
|
||||
let a = art(&r.doc);
|
||||
assert!(matches!(a[0].kind, NodeKind::Group { .. }), "{a:#?}");
|
||||
assert!(r.warnings.iter().any(|w| w.contains("drawn look")), "{:?}", r.warnings);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_bare_comment_line_in_a_prolog_leaves_its_end_alone() {
|
||||
// GUARDS: a bare `%` line's empty marker matched the empty begin marker of a section that has none.
|
||||
|
||||
@@ -2956,6 +2956,44 @@ fn type_with_a_stacked_art_style_comes_in_as_type_with_that_appearance() {
|
||||
check(&r.document, &r.warnings);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn type_with_convert_to_shape_comes_in_as_type_with_it_live() {
|
||||
// GUARDS: `Adobe Shape Effects` wasn't read, and type with it came in without its art style,
|
||||
// the style's drawing beside it as paths.
|
||||
let shape = "/BasicFilter :\n(Adobe Shape Effects) 1 0 /Filter ,\n1 /Visible ,\n/Dictionary : /NotRecorded ,\n\
|
||||
(RoundedRectangle) /UnicodeString (DisplayString) ,\n1 /Int (Shape) ,\n0 /Real (Absolute) ,\n4 /Real (RelWidth) ,\n\
|
||||
2 /Real (RelHeight) ,\n3 /Real (CornerRadius) ,\n; /Dict ;\n /Part ,\n";
|
||||
let stack = "/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\n0 0 0 0 0 0 1 Xa\n0 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";
|
||||
let styles = format!(
|
||||
"%AI9_BeginArtStyles\n/KnownStyle :\n(Boxed) /Name ,\n/ActiveStyle :\n/CompoundFilter :\n(Chain Style Filter) 0 0 /Filter ,\n1 /Visible ,\n\
|
||||
{shape}{stack};\n /Part ,\n%AI9_EndArtStyles\n"
|
||||
);
|
||||
let doc = text_document(&[("Bang\r", 0, (8200.0, 8180.0), (0.0, 0.0), 0.0)]);
|
||||
let drawing = "0 0 1 0 k\n54 56 m\n100 56 L\n100 80 L\n54 80 L\nf";
|
||||
let look = format!("u\n/AI11Text :\n1 /FreeUndo ,\n0 /FrameIndex ,\n0 /StoryIndex ,\n;\n{drawing}\nU\n9 () XW\n");
|
||||
let own: String = text_object(0).lines().map(|l| format!("%_{l}\n")).collect();
|
||||
let art = native_with_text(&(styles + &layer("Art", true, &format!("{look}{own}1 (Boxed) XW\n"))), &doc);
|
||||
let (d, notes) = crate::ai_alone(&private(&art)).unwrap();
|
||||
let (mut texts, mut paths) = (vec![], 0);
|
||||
d.walk(|n| match &n.kind {
|
||||
NodeKind::Text(t) => texts.push((t.plain_text(), n.appearance.effects.clone())),
|
||||
NodeKind::Path { .. } => paths += 1,
|
||||
_ => {}
|
||||
});
|
||||
let [(text, effects)] = texts.as_slice() else { panic!("{texts:?} {notes:?}") };
|
||||
assert_eq!((text.as_str(), paths), ("Bang", 0), "{notes:?}");
|
||||
let [e] = effects.as_slice() else { panic!("{effects:?}") };
|
||||
assert_eq!(
|
||||
(e.id.as_str(), &e.params),
|
||||
(
|
||||
"convertToShape.roundedRectangle",
|
||||
&serde_json::json!({"relative": true, "extraW": 8.0, "extraH": 4.0, "width": 36.0, "height": 36.0, "radius": 3.0})
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn point_type_without_a_matrix_is_placed_by_its_layout() {
|
||||
// GUARDS: Illustrator 24 leaves an unturned, unscaled point type's matrix out of its frame; such
|
||||
|
||||
Reference in New Issue
Block a user