#2405: an envelope's mesh in an .ai isn't read as art

An envelope is written on `%_` lines as a group holding the mesh that shapes
it, with the distortion (`AIDeformStyle`, `DeformValue`) in the group's
dictionary; the distorted art the PDF part draws follows on plain lines. The
reader kept that group, so the mesh drew as a grey gradient mesh behind the
art. A group on `%_` lines that holds only meshes and carries a distortion is
now dropped; a mesh with no distortion is still art.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P6znVFRqGqvB9f7njwZ46r
This commit is contained in:
jmverges
2026-10-17 14:46:20 +07:00
committed by bflatastic
co-authored by Claude Opus 5.5
parent 3f81c70ade
commit 1946ffca9c
2 changed files with 69 additions and 0 deletions
+29
View File
@@ -1434,6 +1434,29 @@ impl<'a> Reader<'a> {
}
}
/// Drop the object just read when it is an envelope's own mesh: a group on `%_` lines holding
/// only gradient meshes. Whether it was dropped.
fn drop_envelope_mesh(&mut self) -> bool {
fn only_meshes(n: &Node) -> bool {
match &n.kind {
NodeKind::Mesh(_) => true,
NodeKind::Group { children, .. } => !children.is_empty() && children.iter().all(|c| only_meshes(c)),
_ => false,
}
}
let Some(n) = self.last_object() else { return false };
let id = n.id;
if !matches!(n.kind, NodeKind::Group { .. }) || !only_meshes(n) || !self.hidden_ids.contains(&id) {
return false;
}
if let Some(f) = self.frames.last_mut() {
f.children.pop();
}
self.hidden_ids.remove(&id);
self.after_object = false;
true
}
/// Do the layers being read show?
fn shown(&self) -> bool {
self.frames.iter().all(|f| !matches!(&f.kind, Kind::Layer(a) if !a.visible))
@@ -1442,6 +1465,12 @@ impl<'a> Reader<'a> {
/// An object's dictionary: its name (`AIArtName`, or the XML id `AI10_ArtUID`), and the
/// opacity mask it may carry.
fn art_dictionary(&mut self, o: &Obj) {
// An envelope (a distortion, `AIDeformStyle` / `DeformValue`) is written on `%_` lines as
// a group holding the mesh that shapes it; the distorted art the file's PDF part draws
// follows on plain lines. That mesh isn't art, so it goes (#2405).
if (o.get("AIDeformStyle").is_some() || o.get("DeformValue").is_some()) && self.drop_envelope_mesh() {
return;
}
if let Some(mask) = o.obj("AI9_Mask").or_else(|| o.obj("Mask")) {
if let Some(art) = &mask.art {
let on = |key: &str| mask.nums(key).first().copied() == Some(1.0);
+40
View File
@@ -1226,6 +1226,46 @@ fn a_styled_object_keeps_the_transparency_written_on_its_look() {
assert_eq!((a[0].opacity, a[0].blend), (0.6, BlendMode::Multiply));
}
/// An envelope is written on `%_` lines as a group holding the mesh that shapes it, with its
/// distortion in the group's dictionary; the distorted art follows on plain lines. Only that art
/// is drawn, as in the file's PDF part, not the envelope's mesh (#2405).
#[test]
fn an_envelope_mesh_is_not_read_as_art() {
let corner = |x: i32, y: i32, next: (i32, i32), prev: (i32, i32)| {
format!("%_[0.82 0.83 0.83 1 {x} {y} {} {} 1 {} {} 1] /N X#\n", next.0, next.1, prev.0, prev.1)
};
let mesh = format!(
"%_/Mesh X!\n%_(DeviceRGB) /CS X#\n%_[0 0] /P X#\n{}{}{}{}%_0 /E X#\n%_/End X!\n",
corner(0, 0, (0, 10), (10, 0)),
corner(0, 40, (10, 40), (0, 30)),
corner(40, 40, (40, 30), (30, 40)),
corner(40, 0, (30, 0), (40, 10)),
);
let envelope = |dictionary: &str| {
format!(
"%_0 Ae\n%_u\n{mesh}%_0 0 Xd\n%_6 () XW\n%_U\n%_/ArtDictionary :\n{dictionary}%_;\n%_\n%_0 0 Xd\n%_6 () XW\n{RED}{}f\n",
rect_path(0.0, 0.0, 40.0, 40.0)
)
};
let has_mesh = |n: &Node| -> bool {
fn walk(n: &Node) -> bool {
matches!(n.kind, NodeKind::Mesh(_)) || n.children().is_some_and(|c| c.iter().any(|k| walk(k)))
}
walk(n)
};
let warped = envelope("%_(Warp: Arch) /UnicodeString (DisplayString) ,\n%_0.27 /Real (DeformValue) ,\n%_1 /Int (AIDeformStyle) ,\n");
let doc = read_ok(&layer(&warped)).doc;
let a = art(&doc);
assert!(!a.iter().any(|n| has_mesh(n)), "{a:#?}");
assert_eq!(a.len(), 1, "{a:#?}");
assert_eq!(fill(&a[0]).color(), Some(Color::rgb(1.0, 0.0, 0.0)));
// A mesh in a group with no distortion is still art.
let doc = read_ok(&layer(&envelope(""))).doc;
assert!(art(&doc).iter().any(|n| has_mesh(n)), "{:#?}", art(&doc));
}
#[test]
fn a_gradient_mesh_reads_as_a_patch() {
let corner = |rgb: &str, x: i32, y: i32, next: (i32, i32), prev: (i32, i32)| {