mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-01 13:47:21 -06:00
fix(eps): an envelope's contents being edited aren't read as art
An envelope whose contents are being edited is written the other way round from one that isn't: its contents, a group on `%_` lines, then its dictionary, which holds the mesh that shapes them. Only a group of meshes was left out before an envelope's dictionary, so those contents were read as art and their undistorted shapes drew over the distorted art that follows on plain lines (and that the page draws). The group on `%_` lines before an envelope's dictionary is now left out whatever it holds; text in it stays as hidden text, as the dictionary's own source text does. # Conflicts: # docs/file-format-parity.md
This commit is contained in:
@@ -338,23 +338,26 @@ 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.
|
||||
pub(super) 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,
|
||||
}
|
||||
}
|
||||
/// Drop the object just read when it is an envelope's own: a group on `%_` lines, before the
|
||||
/// envelope's dictionary. It holds the mesh that shapes the envelope, or, while the envelope's
|
||||
/// contents are being edited, those contents (the mesh then is in the dictionary); either way the
|
||||
/// distorted art follows on plain lines. The source text in the contents stays, hidden, as the
|
||||
/// dictionary's does. Whether it was dropped.
|
||||
pub(super) fn drop_envelope_art(&mut self) -> bool {
|
||||
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) {
|
||||
if !matches!(n.kind, NodeKind::Group { .. }) || !self.hidden_ids.contains(&id) {
|
||||
return false;
|
||||
}
|
||||
if let Some(f) = self.frames.last_mut() {
|
||||
f.children.pop();
|
||||
let mut slots = vec![];
|
||||
if let Some(f) = self.frames.last_mut()
|
||||
&& let Some(group) = f.children.pop()
|
||||
{
|
||||
envelope_text_slots(&group, &mut slots);
|
||||
for mut slot in slots {
|
||||
slot.visible = false;
|
||||
f.children.push(slot);
|
||||
}
|
||||
}
|
||||
self.hidden_ids.remove(&id);
|
||||
self.after_object = false;
|
||||
|
||||
@@ -229,9 +229,10 @@ impl<'a> Reader<'a> {
|
||||
self.blend_roles.insert(id, (kind, o.text("ShapeBlendPathID")));
|
||||
}
|
||||
// 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() {
|
||||
// a group holding the mesh that shapes it (or its contents, while they are being edited);
|
||||
// the distorted art the file's PDF part draws follows on plain lines. That group isn't
|
||||
// art, so it goes (#2405).
|
||||
if (o.get("AIDeformStyle").is_some() || o.get("DeformValue").is_some()) && self.drop_envelope_art() {
|
||||
return;
|
||||
}
|
||||
if let Some(mask) = o.obj("AI9_Mask").or_else(|| o.obj("Mask")) {
|
||||
|
||||
@@ -1687,6 +1687,33 @@ fn an_envelope_mesh_is_not_read_as_art() {
|
||||
assert!(art(&doc).iter().any(|n| has_mesh(n)), "{:#?}", art(&doc));
|
||||
}
|
||||
|
||||
/// An envelope whose contents are being edited is written the other way round: its contents, a
|
||||
/// group on `%_` lines, then its dictionary, which holds the mesh that shapes them. The contents
|
||||
/// aren't drawn as they are, only the distorted art on the plain lines after them.
|
||||
#[test]
|
||||
fn an_envelope_s_contents_being_edited_are_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%_0 0 Xd\n%_6 () XW\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 contents: String = rect_path(0.0, 0.0, 80.0, 20.0).lines().map(|l| format!("%_{l}\n")).collect();
|
||||
let body = format!(
|
||||
"%_u\n%_u\n%_0 0 1 0 0 0 1 Xa\n{contents}%_f\n%_U\n%_9 () XW\n%_U\n%_/ArtDictionary :\n%_1 /Bool (AIDeformEditModeKey) ,\n\
|
||||
%_3 /Int (AIDeformStyle) ,\n%_X=\n{mesh}%_X+\n%_ (AIDeformArtKey) ,\n%_;\n%_\n%_9 () XW\n{RED}{}f\n",
|
||||
rect_path(0.0, 0.0, 40.0, 40.0)
|
||||
);
|
||||
let doc = read_ok(&layer(&body)).doc;
|
||||
let a = art(&doc);
|
||||
assert_eq!(a.len(), 1, "{a:#?}");
|
||||
assert_eq!(fill(&a[0]).color(), Some(Color::rgb(1.0, 0.0, 0.0)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_gradient_mesh_reads_as_a_patch() {
|
||||
let corner = |rgb: &str, x: i32, y: i32, next: (i32, i32), prev: (i32, i32)| {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# File format parity
|
||||
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-31 · **Change:** minor (a transparency an Illustrator file sets in a dictionary's art, as an area text's frame does, stays for the objects after it) · **Target:** Adobe Illustrator 2026 (30.x)
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-31 · **Change:** minor (an Illustrator envelope whose contents are being edited no longer draws its undistorted contents over its result) · **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-31 | minor | `.ai`/EPS editing data: an envelope whose contents are being edited is written the other way round from one that isn't: its contents, a group on `%_` lines, then its dictionary, which holds the mesh that shapes them. Those contents were read as art, so the undistorted art drew over the distorted art the page draws. The group on `%_` lines before an envelope's dictionary is now left out whatever it holds (text in it stays as hidden text, as the dictionary's own does), as the envelope's mesh already was (#2405). Test `import::ai::tests::an_envelope_s_contents_being_edited_are_not_read_as_art`; checked locally against a user's file: it opens from its layers, 0.0% from its page (it was 9.7%, over the 5% limit); scores unchanged |
|
||||
| 2026-10-31 | minor | `.ai`/EPS editing data: a transparency (`Xy`) set by art inside a dictionary, as an area text's frame (its `/ConfiningPath`) or a mask's art sets one, stays the current one after the dictionary: the writer goes on from it, so the objects after such a text have no `Xy` of their own. It was dropped when the dictionary closed, and a user's file drew its logo, calligraphy, footer and type after an area text with the Lighten of the glow before it, so they faded into the light background. Test `import::ai::tests::a_transparency_set_in_a_dictionary_s_art_stays_after_it`; checked locally against that file: 8.1% → 1.7% from its page, so it opens from its layers (5% is the limit); scores unchanged |
|
||||
| 2026-10-31 | minor | `.ai`/EPS editing data: area type made from the file's text document put its first baseline the font's ascent below the frame's top, where the file's own layout (and its page) can have it higher: Oswald's and Urbanist's headings came in a third and an eighth of their size lower than on the page, and each text over them was taken to differ. When the text document's first line is more than half a point from where it is laid out here, the first baseline is fixed (Area Type Options › First Baseline › Fixed) where the file has it; the type's own font metrics aren't changed. Test `import::ate::tests::area_type_s_first_line_goes_where_the_file_lays_it_out`; checked locally against users' files: two election posters come 5.4% → 1.1% and 8.6% → 4.3% from their pages, so they open from their layers, and another 1.7% → 0.3%; scores unchanged |
|
||||
| 2026-10-31 | minor | `.ai`/EPS editing data: type made from the file's text document took each character attribute its run doesn't set from the document's base attributes, not from the character style the run names (`[Normal Character Style]` or another of the file's, and the styles each is based on). Where they differ the type came in wrong: a user's file whose base fill is white and whose Normal Character Style is black came in with its text white, so most of its type was missing wherever the page's own type didn't stand in for it. A run now takes its own attributes, then its character style's, then the document's ([Character and paragraph styles](https://helpx.adobe.com/illustrator/using/character-paragraph-styles.html)). Test `import::tests_native::type_from_the_text_document_takes_its_character_styles_attributes`; checked locally against 377 users' files: none newly opens from its PDF part, and with #4791 that file opens from its layers (6.3% → 1.1% from its page); scores unchanged |
|
||||
|
||||
Reference in New Issue
Block a user