mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-01 05:37:23 -06:00
Merge pull request #4744 from mockydevs/fix/eps-mesh-transparency
.ai/EPS import: a gradient mesh takes the transparency written after it
This commit is contained in:
@@ -183,6 +183,8 @@ struct Reader<'a> {
|
||||
blend_budget: usize,
|
||||
/// A group just ended, its state (`Xy`, `Xd`, `XW`) not yet read.
|
||||
group_ended: bool,
|
||||
/// A gradient mesh just ended (`/End X!`), its own state (`Xy`) not yet read.
|
||||
mesh_ended: bool,
|
||||
/// A layer's own state (`Xy` after its art, before `LB`), until something says the art moves on.
|
||||
layer_state: Option<(f32, BlendMode, bool, Knockout)>,
|
||||
/// Painted copies of clipping paths that have a fill (drawn under what they clip).
|
||||
@@ -288,6 +290,7 @@ impl<'a> Reader<'a> {
|
||||
compound_groups: HashSet::new(),
|
||||
text_looks: HashSet::new(),
|
||||
group_ended: false,
|
||||
mesh_ended: false,
|
||||
layer_state: None,
|
||||
styles: HashMap::new(),
|
||||
palette: super::palette::Palette::default(),
|
||||
|
||||
@@ -235,7 +235,9 @@ impl<'a> Reader<'a> {
|
||||
let mut node = self.new_node(NodeKind::Mesh(one), &gs);
|
||||
node.opacity = gs.opacity;
|
||||
node.blend = gs.blend;
|
||||
return self.add(node, hidden);
|
||||
self.add(node, hidden)?;
|
||||
self.mesh_ended = true;
|
||||
return Ok(());
|
||||
}
|
||||
Err(crate::import::ai::mesh::NotOne::Unjoined) => {
|
||||
self.warn("a gradient mesh whose patches don't join up comes in as a group of its patches")
|
||||
@@ -249,7 +251,9 @@ impl<'a> Reader<'a> {
|
||||
};
|
||||
node.opacity = gs.opacity;
|
||||
node.blend = gs.blend;
|
||||
self.add(node, hidden)
|
||||
self.add(node, hidden)?;
|
||||
self.mesh_ended = true;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Stroke the path just painted (a gradient-filled one, `1 BB` or `2 BB`) with the stroke's state.
|
||||
|
||||
@@ -79,6 +79,7 @@ impl<'a> Reader<'a> {
|
||||
self.layer_state = None;
|
||||
}
|
||||
let group_ended = std::mem::take(&mut self.group_ended);
|
||||
let mesh_ended = std::mem::take(&mut self.mesh_ended);
|
||||
let vals = self.operands();
|
||||
let vals = self.take_head(vals);
|
||||
// Only `Bh` reads an undefined number. Anywhere else it would reach a point or a size, so
|
||||
@@ -201,7 +202,7 @@ impl<'a> Reader<'a> {
|
||||
}
|
||||
}
|
||||
// Transparency and state.
|
||||
"Xy" => self.transparency(&vals),
|
||||
"Xy" => self.transparency(&vals, mesh_ended),
|
||||
// A group's own state (`Xd`, `6 () XW`) without an `Xy` keeps the current transparency, as
|
||||
// the file's PDF part draws it. A look's group (`9 () XW`) has no state of its own.
|
||||
"Xd" if group_ended => {
|
||||
@@ -388,8 +389,9 @@ impl<'a> Reader<'a> {
|
||||
});
|
||||
}
|
||||
|
||||
/// `mode opacity isolate knockout shape Xy`; after a group's end, the group's too.
|
||||
fn transparency(&mut self, vals: &[V]) {
|
||||
/// `mode opacity isolate knockout shape Xy`; after a group's end, the group's too, and right
|
||||
/// after a gradient mesh's `/End X!` (before its `Xd` and `XW`), the mesh's.
|
||||
fn transparency(&mut self, vals: &[V], mesh_ended: bool) {
|
||||
let n = Self::nums(vals);
|
||||
let [mode, opacity, rest @ ..] = n.as_slice() else { return };
|
||||
let gs = &mut self.gs;
|
||||
@@ -403,7 +405,9 @@ impl<'a> Reader<'a> {
|
||||
};
|
||||
gs.knockout_shape = rest.get(2).is_some_and(|v| *v != 0.0);
|
||||
let gs = self.gs.clone();
|
||||
if self.last_object().is_some_and(|n| matches!(n.kind, NodeKind::Group { .. } | NodeKind::Compound { .. })) {
|
||||
if self.last_object().is_some_and(|n| {
|
||||
matches!(n.kind, NodeKind::Group { .. } | NodeKind::Compound { .. }) || (mesh_ended && matches!(n.kind, NodeKind::Mesh(_)))
|
||||
}) {
|
||||
if let Some(n) = self.last_object() {
|
||||
n.opacity = gs.opacity;
|
||||
n.blend = gs.blend;
|
||||
|
||||
@@ -410,6 +410,35 @@ fn transparency_of_objects_and_groups() {
|
||||
assert_eq!((a[2].opacity, a[2].blend), (1.0, BlendMode::Normal), "the next object's state isn't the group's");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_gradient_mesh_takes_the_transparency_written_after_it() {
|
||||
// GUARDS: a mesh's own state (`Xy`) comes right after its `/End X!`, before `Xd` and `XW`, as a
|
||||
// group's does after `U`; read as the next object's, a Multiply shadow mesh painted opaque.
|
||||
let corner =
|
||||
|x: i32, y: i32, next: (i32, i32), prev: (i32, i32)| format!("%_[1 1 1 1 {x} {y} {} {} 1 {} {} 1] /N X#\n", next.0, next.1, prev.0, prev.1);
|
||||
let mesh = |x: i32| {
|
||||
format!(
|
||||
"/Mesh X!\n%_(DeviceRGB) /CS X#\n%_[0 0] /P X#\n{}{}{}{}%_0 /E X#\n/End X!\n",
|
||||
corner(x, 0, (x, 10), (x + 10, 0)),
|
||||
corner(x, 40, (x + 10, 40), (x, 30)),
|
||||
corner(x + 40, 40, (x + 40, 30), (x + 30, 40)),
|
||||
corner(x + 40, 0, (x + 30, 0), (x + 40, 10)),
|
||||
)
|
||||
};
|
||||
let art_ = format!(
|
||||
"0 1 0 0 0 Xy\n{}1 0.8 0 0 0 Xy\n0 0 Xd\n6 () XW\n{}0 0 Xd\n6 () XW\n2 0.7 0 0 0 Xy\n{RED}{}f\n",
|
||||
mesh(0),
|
||||
mesh(50),
|
||||
rect_path(100.0, 0.0, 110.0, 10.0)
|
||||
);
|
||||
let doc = read_ok(&layer(&art_)).doc;
|
||||
let a = art(&doc);
|
||||
assert!(matches!(a[0].kind, NodeKind::Mesh(_)) && matches!(a[1].kind, NodeKind::Mesh(_)), "{a:#?}");
|
||||
assert_eq!((a[0].opacity, a[0].blend), (0.8, BlendMode::Multiply), "the state after the mesh is its own");
|
||||
assert_eq!((a[1].opacity, a[1].blend), (0.8, BlendMode::Multiply), "the next mesh keeps the current state");
|
||||
assert_eq!((a[2].opacity, a[2].blend), (0.7, BlendMode::Screen), "a state after the mesh's `Xd` and `XW` is the next object's");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn gradients() {
|
||||
let defs = "%AI5_BeginGradient: (G)\n(G) 1 2 Bd\n[\n<00FF>\n0\n0 0 0 0 1 0 0 2 1 6 %_BS\n\
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# File format parity
|
||||
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-30 · **Change:** minor (`.ai`/EPS editing data: mesh corners with handle weights of 0 are read) · **Target:** Adobe Illustrator 2026 (30.x)
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-30 · **Change:** minor (`.ai`/EPS editing data: a gradient mesh takes the transparency written after it) · **Target:** Adobe Illustrator 2026 (30.x)
|
||||
|
||||
Every format Illustrator opens, places, saves or exports, against VectorCraft's format table (`FORMATS`,
|
||||
`OPEN_EXTS`, `PLACE_EXTS` in `crates/engine/src/cmd/fileio/formats.rs`). Illustrator's list comes from its public
|
||||
@@ -132,6 +132,7 @@ the app's own editing copy of a file (an EPS after its `%%EOF`, a `.ai`'s `AIPri
|
||||
|
||||
| Date | Change | Summary |
|
||||
|---|---|---|
|
||||
| 2026-10-30 | minor | `.ai`/EPS editing data: a gradient mesh takes the transparency (`Xy`) written right after its `/End X!`, before its `Xd` and `XW`, as a group takes the one after its `U`; it was read as the next object's, so a Multiply shadow mesh painted opaque white over the art under it, where the file's page shows the shadow. A user's file now draws such a mesh as its page does. Own-fixture test in `import::ai::tests`; scores unchanged |
|
||||
| 2026-10-30 | minor | `.ai`/EPS editing data: a gradient mesh corner whose handle weights are 0 (one or both; the others are 1) is read like one whose weights are 1, so a mesh with such corners no longer leaves the whole file coming in as its page or PDF part; other weights still leave the mesh unread. Checked against the pages of two users' files: one opens from its layers 0.6% from its page, the other with 728 live meshes instead of its PDF part's 65,558 patches. Test `import::ai::mesh::tests::corners_with_handle_weights_of_zero_are_read`; scores unchanged |
|
||||
| 2026-10-30 | minor | `.ai` import: a PDF part whose pages are cropped to the art is recognised in a `UserUnit` page's units too (the art's box in those units, the artboard not the page in points), and its type is read from the first page only, since every such page holds all the art again: the copies on later pages were moved as the first page's and could be put into the layers' text objects. A page cropped to the art is taken only when every page is that box. Tests `import::tests_native::a_user_unit_page_cropped_to_the_art_is_read_in_its_units`, `type_on_the_copies_of_a_page_cropped_to_the_art_is_read_once`; scores unchanged |
|
||||
| 2026-10-30 | minor | `.ai` import: when the layers are compared with the page again with placed files at the corner of their box, the text objects the page doesn't draw are weighed with the images there, from the layers as read: before, the text an image covered at its first placement had already been left out, so a file whose image covered type only there came in without that type or as its PDF part. Test `import::tests_native::text_a_placed_file_covers_only_at_its_first_placement_is_kept`; scores unchanged |
|
||||
|
||||
Reference in New Issue
Block a user