Merge pull request #3064 from nrq6993-a11y/stream/vc-feat3-g9-ai-feather-art-style

G9: .ai import reads Adobe Feather art styles as live effects
This commit is contained in:
BFlat
2026-10-20 02:33:20 +07:00
committed by GitHub
3 changed files with 55 additions and 5 deletions
+35 -4
View File
@@ -21,7 +21,7 @@
//! ```
//!
//! A style is *plain* when it is one fill and one stroke at most, in solid colours (a fill's gradient
//! is the object's own), and only effects read here (Outer Glow, Drop Shadow) after them. Such
//! is the object's own), and only effects read here (Outer Glow, Drop Shadow, Feather) after them. Such
//! objects are read as themselves with that fill, stroke and those effects; others keep their
//! drawn look. A fill's or stroke's style says whether it paints with `stroke fill Xd`.
@@ -36,9 +36,10 @@ use super::obj::V;
use super::paint;
/// The filters a plain style may chain.
const PLAIN: [&str; 7] =
["Chain Style Filter", "Stack Style Filter", "Stroke Style Filter", "Fill Style Filter", "Conduit Filter", "Blend Style Filter", GLOW];
const PLAIN: [&str; 8] =
["Chain Style Filter", "Stack Style Filter", "Stroke Style Filter", "Fill Style Filter", "Conduit Filter", "Blend Style Filter", GLOW, FEATHER];
const GLOW: &str = "Adobe Outer Glow"; // brand-ok: the filter's name in the files
const FEATHER: &str = "Adobe Feather"; // brand-ok: the filter's name in the files
const SHADOW: &str = "Adobe Drop Shadow"; // brand-ok: the filter's name in the files
/// A plain style's paint, as the object comes in.
@@ -307,7 +308,7 @@ fn style(filters: &[Filter]) -> ArtStyle {
}
}
for (i, f) in filters.iter().enumerate() {
let effect = f.name == GLOW || f.name == SHADOW;
let effect = f.name == GLOW || f.name == FEATHER || f.name == SHADOW;
if !PLAIN.contains(&f.name.as_str()) && f.name != SHADOW {
plain = false;
}
@@ -330,6 +331,12 @@ fn style(filters: &[Filter]) -> ArtStyle {
params: json!({"mode": mode(2), "opacity": param("opac").unwrap_or(0.75) * 100.0, "blur": 2.0 * param("blur").unwrap_or(5.0), "color": color}),
visible: true,
}),
// LiveEffect XML uses `radius`; art styles abbreviate it `rad` (public scripting posts on applyEffect).
FEATHER => effects.push(Effect {
id: "stylize.feather".into(),
params: json!({"radius": param("rad").or_else(|| param("radius")).filter(|v| v.is_finite()).unwrap_or(5.0)}),
visible: true,
}),
SHADOW => {
let mut params = json!({
"mode": mode(1),
@@ -440,6 +447,30 @@ mod tests {
}
}
#[test]
fn a_feather_reads_its_radius_from_rad() {
let data = b"/KnownStyle :\n(Soft) /Name ,\n/ActiveStyle :\n/CompoundFilter :\n(Chain Style Filter) 0 0 /Filter ,\n1 /Visible ,\n\
/BasicFilter :\n(Adobe Feather) 1 0 /Filter ,\n1 /Visible ,\n/Dictionary : /NotRecorded ,\n12 /Real (rad) ,\n; /Dict ;\n /Part ,\n;\n";
let styles = art_styles(data);
let [feather] = styles["Soft"].effects.as_slice() else { panic!() };
assert_eq!((feather.id.as_str(), feather.params["radius"].as_f64()), ("stylize.feather", Some(12.0)));
}
#[test]
fn a_feather_falls_back_to_radius_and_defaults_to_five_points() {
let with_name = b"/KnownStyle :\n(Soft) /Name ,\n/ActiveStyle :\n/CompoundFilter :\n(Chain Style Filter) 0 0 /Filter ,\n1 /Visible ,\n\
/BasicFilter :\n(Adobe Feather) 1 0 /Filter ,\n1 /Visible ,\n/Dictionary : /NotRecorded ,\n3 /Real (radius) ,\n; /Dict ;\n /Part ,\n;\n";
let styles = art_styles(with_name);
let [feather] = styles["Soft"].effects.as_slice() else { panic!() };
assert_eq!(feather.params["radius"].as_f64(), Some(3.0));
let bare = b"/KnownStyle :\n(Soft) /Name ,\n/ActiveStyle :\n/CompoundFilter :\n(Chain Style Filter) 0 0 /Filter ,\n1 /Visible ,\n\
/BasicFilter :\n(Adobe Feather) 1 0 /Filter ,\n1 /Visible ,\n/Dictionary : /NotRecorded ,\n; /Dict ;\n /Part ,\n;\n";
let styles = art_styles(bare);
let [feather] = styles["Soft"].effects.as_slice() else { panic!() };
assert_eq!(feather.params["radius"].as_f64(), Some(5.0));
}
#[test]
fn hostile_dash_offsets_and_width_profiles_are_tamed() {
let mut f = Filter::default();
+19
View File
@@ -1243,6 +1243,25 @@ fn an_object_with_a_style_this_reads_comes_in_as_itself() {
assert!(!r.warnings.iter().any(|w| w.contains("drawn look")), "{:?}", r.warnings);
}
#[test]
fn an_object_with_a_feather_style_comes_in_with_a_live_effect() {
let feather_style = "%AI9_BeginArtStyles\n\
/KnownStyle :\n(Soft) /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\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\
/BasicFilter :\n(Adobe Feather) 1 0 /Filter ,\n1 /Visible ,\n/Dictionary : /NotRecorded ,\n8 /Real (rad) ,\n; /Dict ;\n /Part ,\n;\n\
%AI9_EndArtStyles\n";
let hidden_rect = rect_path(0.0, 0.0, 10.0, 10.0).replace('\n', "\n%_");
let art_ = format!("0 Ae\nu\n{RED}{}f\nU\n%_Bb\n%_{hidden_rect}f\n%_0 BB\n1 (Soft) XW\n", rect_path(0.0, 0.0, 10.0, 10.0));
let r = read_ok(&format!("{feather_style}{}", layer(&art_)));
let a = art(&r.doc);
assert_eq!(a.len(), 1);
let [feather] = a[0].appearance.effects.as_slice() else { panic!("{:#?}", a[0].appearance) };
assert_eq!((feather.id.as_str(), feather.params["radius"].as_f64()), ("stylize.feather", Some(8.0)));
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.
+1 -1
View File
@@ -99,7 +99,7 @@ from G14 and the budget run of G15: **~170–270 h** one agent, ~50–80 h wall
- **Missing:** faithful layer and group structure from the editing data in real files (#951: 109 layers instead
of 14), guides and non-printing construction
lines (#779), open speed on large files (#758), live effects, brushes and symbols as live objects (#637; pattern fills and global colours are read since #1025, placed (linked) files since #1264). Writing Illustrator's own editing data is out of scope
lines (#779), open speed on large files (#758), live effects, brushes and symbols as live objects (#637; plain art styles read Drop Shadow, Outer Glow and Feather as live effects; pattern fills and global colours are read since #1025, placed (linked) files since #1264). Writing Illustrator's own editing data is out of scope
(undocumented): a PDF-compatible `.ai` opens in Illustrator 2018 without its layers and groups (#868). Save As
`.ai` › Version: Legacy (version 7) writes the PostScript form from the published version 7 specification
instead, keeping layers (names, order, visibility, lock), groups, compound paths, clipping groups, gradients and