diff --git a/crates/eps/src/import/ai/read.rs b/crates/eps/src/import/ai/read.rs index d32ae4c177..67928785ad 100644 --- a/crates/eps/src/import/ai/read.rs +++ b/crates/eps/src/import/ai/read.rs @@ -581,6 +581,10 @@ struct Reader<'a> { blend_budget: usize, /// The group on `%_` lines an envelope's dictionary just took (see [`Reader::drop_envelope_art`]). envelope_art: Option, + /// The envelope dictionaries whose `AIDeformArtKey` block (`X=` … `X+`) is being read, as how + /// many frames were open with each one innermost, innermost last. The block's art is art: a + /// dictionary in it is read as it is in a container ([`Reader::end_obj`]). + envelope_blocks: Vec, /// Envelopes waiting for their drawn art, innermost last. envelopes: Vec, /// How many more envelopes and warped text objects this import may draw to check @@ -813,6 +817,7 @@ impl<'a> Reader<'a> { blend_record: None, blend_budget: BLEND_BUDGET, envelope_art: None, + envelope_blocks: Vec::new(), envelopes: Vec::new(), envelope_checks: super::envelope::MAX_CHECKED, envelope_spent: super::envelope::Spent::default(), diff --git a/crates/eps/src/import/ai/read/containers.rs b/crates/eps/src/import/ai/read/containers.rs index 8e670b3f72..830e3a48d0 100644 --- a/crates/eps/src/import/ai/read/containers.rs +++ b/crates/eps/src/import/ai/read/containers.rs @@ -482,6 +482,13 @@ impl<'a> Reader<'a> { return Ok(()); } let Some(f) = self.frames.pop() else { return Ok(()) }; + let depth = self.frames.len(); + // A block left open ends with its dictionary, and envelopes still waiting in it keep their + // drawn art: nothing after the dictionary is theirs. + self.envelope_blocks.retain(|d| *d <= depth); + while let Some(p) = self.envelopes.pop_if(|p| p.depth > depth) { + self.warn(crate::import::ai::envelope::drawn_note(p.typed)); + } let Frame { kind, children, base, hidden, gs, pending_unread_fill, pending_unread_stroke, after, mesh_ended, state, .. } = f; let Kind::Obj(mut o) = kind else { return Ok(()) }; obj::close(&mut o, &mut self.stack, base); @@ -496,7 +503,10 @@ impl<'a> Reader<'a> { } self.after_object = after; self.mesh_ended = mesh_ended; - if matches!(self.frames.last(), Some(Frame { kind: Kind::Obj(_), .. })) { + // An object's dictionary in an envelope's `AIDeformArtKey` block is about the art before it + // there, as it is in a container: an envelope there is one of the outer envelope's contents. + let in_block = o.ty == "ArtDictionary" && self.envelope_blocks.last() == Some(&depth); + if !in_block && matches!(self.frames.last(), Some(Frame { kind: Kind::Obj(_), .. })) { return self.push(V::Obj(Rc::new(*o))); } match o.ty.as_str() { diff --git a/crates/eps/src/import/ai/read/ops.rs b/crates/eps/src/import/ai/read/ops.rs index 8c032d5d83..721a6988ba 100644 --- a/crates/eps/src/import/ai/read/ops.rs +++ b/crates/eps/src/import/ai/read/ops.rs @@ -41,8 +41,19 @@ impl<'a> Reader<'a> { } /// `X=` in a `/Mask` dictionary: the opacity mask's art starts, read until its `X+` - /// ([`Self::end_mask`]). + /// ([`Self::end_mask`]). In an envelope's dictionary, its `AIDeformArtKey` block starts + /// ([`Reader::envelope_blocks`]). fn open_mask_object(&mut self, hidden: bool) -> Result<(), String> { + if let Some(Frame { kind: Kind::Obj(o), .. }) = self.frames.last() + && o.ty == "ArtDictionary" + && o.get("AIDeformOptionsKey").is_some() + { + // One entry for each open dictionary, however many `X=` it has. + if self.envelope_blocks.last() != Some(&self.frames.len()) { + self.envelope_blocks.push(self.frames.len()); + } + return Ok(()); + } if !matches!(self.frames.last(), Some(Frame { kind: Kind::Obj(o), .. }) if o.ty == "Mask") { // `X=` also introduces legacy text frame art, read by `text_frames`. return Ok(()); @@ -63,6 +74,22 @@ impl<'a> Reader<'a> { self.open(Kind::Mask, hidden) } + /// `X+`: a dictionary's art block ends. The block of a dictionary whose `X=` opened no frame (an + /// envelope's, in an opacity mask's art too) ends with the dictionary still innermost; an opacity + /// mask's ends its art ([`Self::end_mask`]). + fn end_block(&mut self) -> Result<(), String> { + if matches!(self.frames.last(), Some(Frame { kind: Kind::Obj(_), .. })) { + if self.envelope_blocks.last() == Some(&self.frames.len()) { + self.envelope_blocks.pop(); + } + return Ok(()); + } + if self.frames.iter().any(|f| matches!(f.kind, Kind::Mask)) { + return self.end_mask(); + } + Ok(()) + } + /// `LB`: a graph started in this layer that hasn't ended doesn't go on past it; the layer ends. fn layer_end(&mut self) -> Result<(), String> { if self.graph.as_ref().is_some_and(|g| g.frames >= self.frames.len()) { @@ -159,8 +186,7 @@ impl<'a> Reader<'a> { } ";" => return self.end_obj(), "X=" => return self.open_mask_object(hidden), - "X+" if self.frames.iter().any(|f| matches!(f.kind, Kind::Mask)) => return self.end_mask(), - "X+" => return Ok(()), + "X+" => return self.end_block(), _ => {} } self.state_block_continues(w); diff --git a/crates/eps/src/import/ai/tests.rs b/crates/eps/src/import/ai/tests.rs index 7013468d25..f133a9ee03 100644 --- a/crates/eps/src/import/ai/tests.rs +++ b/crates/eps/src/import/ai/tests.rs @@ -8620,6 +8620,224 @@ fn an_envelope_off_the_page_opens_live_when_it_draws_close_to_its_drawn_art() { assert!(r.warnings.is_empty(), "{:?}", r.warnings); } +/// `art` on `%_` lines, as the editing data writer hides it: a line already on one stays as it is. +fn hidden_once(art: &str) -> String { + art.lines().map(|l| if l.starts_with("%_") { format!("{l}\n") } else { format!("%_{l}\n") }).collect() +} + +/// A mesh envelope of `content` shaped by the grid `shape`, with Edit Contents on (`editing`: the +/// contents in the group before the dictionary, the grid in its `AIDeformArtKey` block) or off (the +/// other way round, the contents in a plain group), then `drawn` and the plugin's `XP`. An envelope +/// record in `content` keeps its own `%_` lines. +fn mesh_envelope(shape: &str, editing: bool, content: &str, drawn: &str) -> String { + let keys = ENVELOPE_KEYS.replace("0 /Bool (AIDeformEditModeKey)", &format!("{} /Bool (AIDeformEditModeKey)", u8::from(editing))); + let (before, block) = if editing { (content.to_string(), shape.to_string()) } else { (shape.to_string(), format!("u\n{content}U\n9 () XW\n")) }; + format!( + "{}%_/ArtDictionary :\n{}%_X=\n{}%_X+\n%_ (AIDeformArtKey) ,\n%_2 /Int (AIDeformStyle) ,\n%_;\n%_\n%_9 () XW\nu\n{drawn}U\n9 () XW\n(Adobe Deform Plugin) 1 0 0 XP\n", + hidden_once(&format!("u\n{before}U\n")), + commented(&keys), + hidden_once(&block), + ) +} + +/// The mesh envelope that maps the box 40 20 160 80 onto itself, and what it draws of +/// [`envelope_content`]. +fn flat_mesh() -> (String, vectorcraft_doc::live::EnvelopeKind, String) { + let flat = vectorcraft_doc::live::EnvelopeKind::Mesh { + rows: 1, + cols: 1, + points: vec![Point::new(40.0, 20.0), Point::new(160.0, 20.0), Point::new(40.0, 80.0), Point::new(160.0, 80.0)], + handles: vec![], + }; + let drawn = drawn_content(&flat); + (straight_mesh(1, 1, (120.0, 60.0)), flat, drawn) +} + +/// An envelope inside another envelope's contents opens as a live envelope inside the live outer +/// one, with Edit Contents on or off at either level: each keeps its own kind, contents and Edit +/// Contents, and neither leaves a copy of its contents drawn beside it. +#[test] +fn an_envelope_in_an_envelope_s_contents_opens_live_inside_it() { + use vectorcraft_doc::live::EnvelopeKind; + let (shape, flat, drawn) = flat_mesh(); + let EnvelopeKind::Mesh { points, .. } = &flat else { panic!("{flat:?}") }; + for (outer_editing, inner_editing) in [(false, false), (false, true), (true, false), (true, true)] { + let why = format!("outer editing {outer_editing}, inner editing {inner_editing}"); + let inner = mesh_envelope(&shape, inner_editing, &envelope_content(), &drawn); + let r = read_ok(&layer(&mesh_envelope(&shape, outer_editing, &hidden_once(&inner), &format!("u\n{drawn}U\n")))); + let a = art(&r.doc); + assert_eq!(a.len(), 1, "{why}: {a:#?}"); + let flat_points = |kind: &EnvelopeKind| match kind { + EnvelopeKind::Mesh { rows: 1, cols: 1, points, .. } => Some(points.clone()), + _ => None, + }; + let (content, kind, _, editing, _) = live_envelope(&a[0]); + assert_eq!((flat_points(kind), editing), (Some(points.clone()), outer_editing), "{why}: {kind:?}"); + assert_eq!(content.len(), 1, "{why}: {content:#?}"); + let (inner, kind, _, editing, _) = live_envelope(&content[0]); + assert_eq!((flat_points(kind), editing), (Some(points.clone()), inner_editing), "{why}: {kind:?}"); + assert_eq!(inner.len(), 1, "{why}: {inner:#?}"); + assert!(matches!(inner[0].kind, NodeKind::Path { .. }), "{why}: {inner:#?}"); + assert_eq!(fill(&inner[0]).color(), Some(Color::rgb(1.0, 0.0, 0.0)), "{why}"); + assert!(r.warnings.is_empty(), "{why}: {:?}", r.warnings); + } +} + +/// An outer envelope, Edit Contents off, whose `AIDeformArtKey` block holds `block` as it is, with no +/// plain group around it, shaped by the grid `shape` and drawn as `drawn`. +fn bare_block_envelope(shape: &str, block: &str, drawn: &str) -> String { + format!( + "{}%_/ArtDictionary :\n{}%_X=\n{}%_X+\n%_ (AIDeformArtKey) ,\n%_2 /Int (AIDeformStyle) ,\n%_;\n%_\n%_9 () XW\nu\n{drawn}U\n9 () XW\n(Adobe Deform Plugin) 1 0 0 XP\n", + hidden_once(&format!("u\n{shape}U\n")), + commented(ENVELOPE_KEYS), + hidden_once(block), + ) +} + +/// An envelope record straight in another envelope's `AIDeformArtKey` block, with no group around +/// it, opens live inside the live outer envelope too. Two of them there are both its contents. +#[test] +fn an_envelope_straight_in_an_envelope_s_block_opens_live_inside_it() { + let (shape, _, drawn) = flat_mesh(); + let inner = hidden_once(&mesh_envelope(&shape, false, &envelope_content(), &drawn)); + let r = read_ok(&layer(&bare_block_envelope(&shape, &inner, &format!("u\n{drawn}U\n")))); + let a = art(&r.doc); + assert_eq!(a.len(), 1, "{a:#?}"); + let (content, ..) = live_envelope(&a[0]); + assert_eq!(content.len(), 1, "{content:#?}"); + let (inner_content, ..) = live_envelope(&content[0]); + assert!(matches!(inner_content[0].kind, NodeKind::Path { .. }), "{inner_content:#?}"); + assert!(r.warnings.is_empty(), "{:?}", r.warnings); + // Two envelopes side by side in the block: both are the outer envelope's contents, each live. + let two = format!("{inner}{inner}"); + let r = read_ok(&layer(&bare_block_envelope(&shape, &two, &format!("u\n{drawn}U\nu\n{drawn}U\n")))); + let a = art(&r.doc); + assert_eq!(a.len(), 1, "{a:#?}"); + let (content, ..) = live_envelope(&a[0]); + assert_eq!(content.len(), 2, "{content:#?}"); + content.iter().for_each(|c| _ = live_envelope(c)); + assert!(r.warnings.is_empty(), "{:?}", r.warnings); +} + +/// An object's own dictionary straight in an envelope's block, with no group around them, names it +/// there. +#[test] +fn an_object_s_dictionary_straight_in_an_envelope_s_block_names_it() { + let (shape, _, drawn) = flat_mesh(); + let block = format!("{}/ArtDictionary :\n(Box) /String (AIArtName) ,\n;\n", envelope_content()); + let r = read_ok(&layer(&bare_block_envelope(&shape, &block, &format!("u\n{drawn}U\n")))); + let a = art(&r.doc); + assert_eq!(a.len(), 1, "{a:#?}"); + let (content, ..) = live_envelope(&a[0]); + assert_eq!(content.len(), 1, "{content:#?}"); + assert_eq!(content[0].name.as_deref(), Some("Box"), "{content:#?}"); +} + +/// An inner envelope that doesn't open live keeps its drawn art inside the live outer envelope, +/// drawn once, without its own source art: one whose drawn art differs from what it draws, and one +/// whose record doesn't read (a style number past the list). The outer envelope maps that drawn +/// art's box onto its grid. An outer envelope that doesn't match its drawn art keeps that drawn +/// art, and nothing of its record is drawn. +#[test] +fn a_nested_envelope_that_doesn_t_open_live_keeps_its_drawn_art() { + let (shape, _, drawn) = flat_mesh(); + let shifted = format!("{RED}{}f\n", rect_path(42.0, 20.0, 162.0, 80.0)); + let unread = mesh_envelope(&shape, false, &envelope_content(), &drawn).replace("%_2 /Int (AIDeformStyle)", "%_9 /Int (AIDeformStyle)"); + for (why, inner) in [("drawn elsewhere", mesh_envelope(&shape, false, &envelope_content(), &shifted)), ("unread", unread)] { + for bare in [false, true] { + let inner = hidden_once(&inner); + let outer_drawn = format!("u\n{drawn}U\n"); + let body = if bare { bare_block_envelope(&shape, &inner, &outer_drawn) } else { mesh_envelope(&shape, false, &inner, &outer_drawn) }; + let r = read_ok(&layer(&body)); + let a = art(&r.doc); + assert_eq!(a.len(), 1, "{why}, bare {bare}: {a:#?}"); + assert!(matches!(a[0].kind, NodeKind::Envelope { .. }), "{why}, bare {bare}: {:?}", r.warnings); + let (content, ..) = live_envelope(&a[0]); + assert_eq!(content.len(), 1, "{why}, bare {bare}: {content:#?}"); + // The inner envelope's drawn art: its group of one path, or, straight in the block, the + // path, as the plain group around a block's only object is the block's own. + let drawn_path = if bare { + &content[0] + } else { + let kids = content[0].children().unwrap_or_else(|| panic!("{why}: {content:#?}")); + assert_eq!(kids.len(), 1, "{why}: {content:#?}"); + &kids[0] + }; + assert!(matches!(drawn_path.kind, NodeKind::Path { .. }), "{why}, bare {bare}: {content:#?}"); + assert_eq!(r.warnings, [super::envelope::DRAWN], "{why}, bare {bare}"); + } + } + // The outer envelope's drawn art lies elsewhere: it stays, as one group, the inner record with it. + let inner = hidden_once(&mesh_envelope(&shape, false, &envelope_content(), &drawn)); + for bare in [false, true] { + let outer_drawn = format!("u\n{shifted}U\n"); + let body = if bare { bare_block_envelope(&shape, &inner, &outer_drawn) } else { mesh_envelope(&shape, false, &inner, &outer_drawn) }; + let r = read_ok(&layer(&body)); + let a = art(&r.doc); + assert_eq!(a.len(), 1, "bare {bare}: {a:#?}"); + assert!(matches!(a[0].kind, NodeKind::Group { .. }), "bare {bare}: {a:#?}"); + assert_eq!(a[0].geometric_bounds(), Some(Rect::new(42.0, 20.0, 162.0, 80.0)), "bare {bare}"); + assert_eq!(r.warnings, [super::envelope::DRAWN], "bare {bare}"); + } +} + +/// Broken nested records read without taking another record's place: an inner `X=` with no `X+` +/// (the inner envelope still opens live), an inner dictionary with no group before it (its drawn art +/// stays), and an `XP` repeated after the inner one. The outer envelope, whose own record is whole, +/// still opens live around what the inner one left. +#[test] +fn broken_nested_envelope_records_keep_the_outer_envelope() { + let (shape, _, drawn) = flat_mesh(); + let inner = hidden_once(&mesh_envelope(&shape, false, &envelope_content(), &drawn)); + let no_end = inner.replacen("%_X+\n", "", 1); + let no_group = inner.replacen(&hidden_once(&format!("u\n{shape}U\n")), "", 1); + let twice = inner.replace("%_(Adobe Deform Plugin) 1 0 0 XP\n", "%_(Adobe Deform Plugin) 1 0 0 XP\n%_(Adobe Deform Plugin) 1 0 0 XP\n"); + for (why, inner, live) in [("no X+", no_end, true), ("no group", no_group, false), ("XP twice", twice, true)] { + for bare in [false, true] { + let outer_drawn = format!("u\n{drawn}U\n"); + let body = if bare { bare_block_envelope(&shape, &inner, &outer_drawn) } else { mesh_envelope(&shape, false, &inner, &outer_drawn) }; + let r = read_ok(&layer(&body)); + let a = art(&r.doc); + assert_eq!(a.len(), 1, "{why}, bare {bare}: {a:#?}"); + let (content, ..) = live_envelope(&a[0]); + assert_eq!(content.len(), 1, "{why}, bare {bare}: {content:#?}"); + assert_eq!(matches!(content[0].kind, NodeKind::Envelope { .. }), live, "{why}, bare {bare}: {content:#?}"); + } + } +} + +/// An inner envelope whose `XP` is missing stops waiting when the outer envelope's dictionary ends: +/// a stray `XP` in a later group leaves that group's art as it is. +#[test] +fn an_inner_envelope_without_its_xp_does_not_take_later_art() { + let (shape, _, drawn) = flat_mesh(); + let inner = hidden_once(&mesh_envelope(&shape, false, &envelope_content(), &drawn)).replace("%_(Adobe Deform Plugin) 1 0 0 XP\n", ""); + let later = format!("u\nu\n{drawn}U\n(Adobe Deform Plugin) 1 0 0 XP\nU\n"); + let r = read_ok(&layer(&format!("{}{later}", bare_block_envelope(&shape, &inner, &format!("u\n{drawn}U\n"))))); + let a = art(&r.doc); + assert_eq!(a.len(), 2, "{a:#?}"); + live_envelope(&a[0]); + let kids = a[1].children().unwrap_or_else(|| panic!("{a:#?}")); + assert!(matches!(kids[0].kind, NodeKind::Group { .. }), "{a:#?}"); + assert_eq!(r.warnings, [super::envelope::DRAWN], "{:?}", r.warnings); +} + +/// An envelope in an opacity mask's art reads: its block's `X+` ends the block, not the mask's art. +#[test] +fn an_envelope_in_an_opacity_mask_s_art_reads() { + let (shape, _, drawn) = flat_mesh(); + let owner = format!("{RED}{}f\n", rect_path(10.0, 20.0, 170.0, 90.0)); + let envelope = hidden_once(&mesh_envelope(&shape, false, &envelope_content(), &drawn)); + let mask = format!( + "%_/ArtDictionary :\n%_/Mask :\n%_1 /Bool (Linked) ,\n%_X=\n%_u\n{envelope}%_U\n%_X+\n%_(Art) ,\n%_1 /Bool (Clipping) ,\n%_0 /Bool (Inverted) ,\n%_0 /Bool (Disabled) ,\n%_; (AI9_Mask) ,\n%_;\n" + ); + let r = read_ok(&layer(&format!("{owner}{mask}"))); + let a = art(&r.doc); + assert_eq!(a.len(), 1, "{a:#?}"); + let m = a[0].extras().mask.as_ref().unwrap_or_else(|| panic!("{a:#?}")); + assert_eq!(m.art.geometric_bounds(), Some(Rect::new(40.0, 20.0, 160.0, 80.0)), "{:#?}", m.art); +} + #[test] fn a_gradient_mesh_reads_as_a_patch() { let corner = |rgb: &str, x: i32, y: i32, next: (i32, i32), prev: (i32, i32)| { diff --git a/docs/target-app-parity.md b/docs/target-app-parity.md index f08f2b9f19..beb435f288 100644 --- a/docs/target-app-parity.md +++ b/docs/target-app-parity.md @@ -1837,6 +1837,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur A `.ai` or EPS opened from its editing data with one object on a shown layer that the reader can't read (an unknown operator inside a group, a pattern fill whose pattern it doesn't have, a symbol instance, a placed file without a usable preview) keeps its layers and groups: in that object's place, its layer gets a group of the page's objects inside the object's box, named after it, with a warning that lists what came in as the page's art and an export or Save As to its own path asking first (`acknowledgeLoss`). The file still opens from its page or PDF part when no box can be found, when live art or another unread object shares the page art inside it, when the object spans two artboards or lies on an artboard that has no page, when no page art lies inside its box, when that page art is inside a page group with a clip, mask, transparency or appearance, when the object holds a frame of threaded type, when the page art uses a pattern, image or swatch that the page lacks or that differs from the editing data's, or when the page art doesn't carry the object's transparency (#4806; `crates/eps/src/import/tests_native.rs`, `crates/engine/src/cmd/fileio/tests_aiimport.rs`). A `.ai` or EPS opened from its editing data with one object on a shown layer that the reader can't read (an unknown operator inside a group, a pattern fill whose pattern it doesn't have, a symbol instance, a placed file without a usable preview) keeps its layers and groups: in that object's place, its layer gets a group of the page's objects inside the object's box, named after it, with a warning that lists what came in as the page's art and an export or Save As to its own path asking first (`acknowledgeLoss`). The file still opens from its page or PDF part when no box can be found, when live art or another unread object shares the page art inside it, when the object spans two artboards, or when the page art doesn't carry the object's transparency (#4806; `crates/eps/src/import/tests_native.rs`, `crates/engine/src/cmd/fileio/tests_aiimport.rs`). In a `.ai` opened from its editing data, an envelope made with a warp, a mesh or a top object opens as a live envelope of its contents, with its warp style, bend and distortions, its mesh points and handles or its top object's outline, its Envelope Options, its Fidelity and whether its contents are being edited. When Distort Appearance is on and the size the file saves for the contents matches their bounds with strokes, the contents map from that stroked box, until an edit in Edit Contents changes how far the strokes reach; VectorCraft's new envelopes map from the geometric box, as do contents with no width or no height, such as a single straight line. An envelope that can't be read, or that VectorCraft draws differently from the file's drawn art, keeps that drawn art, with a note. An envelope of type opens live, its type laid out from the text document as its contents, where VectorCraft's own envelope of that type draws as the drawn art the file saves (compared as for the other envelopes, seen on white and on black, so white type counts, and only where the layers with it are compared with the page and draw as it, the envelope weighed as the art it draws; where they don't, even with the drawn art of every live envelope put back, and are used all the same, trusted, with the objects that differ coming in as the page's art, or opened alone, it stays live where, in the pixels it and its drawn art ink (drawn together without the rest of the layers), the layers with it differ from the page in no more than its own check allows and the envelopes that stay differ there in no more than half of what the page check allows, and nothing above it clips, masks or moves it); elsewhere, and where the type's font isn't available, a text or visibility variable binds the type, or the envelope is in a symbol's or a pattern's art, it keeps its drawn art, with its type hidden and a note that says the type isn't editable (#3179; `crates/eps/src/import/ai/tests.rs`, `crates/eps/src/import/tests_native.rs`). A warp of type whose own warp, VectorCraft's, doesn't draw as its drawn art is tried again as a mesh envelope on the grid the file saves for it: its type stays editable and keeps the file's shape, but the warp's settings (style, bend) are lost, as the document has no place for them on a mesh. Type with strokes in an envelope with Distort Appearance on is also tried mapped from its glyphs grown by as far as its strokes paint along a side (half the weight of a centred stroke), without what miter joins add at corners, where the size the file saves for it doesn't already give that box; after those tries (and before a grid moved onto its drawn art), it is also tried mapped from the box of what it paints, where that is another box, as users' files map some such type: its glyph outlines with their fills and strokes baked as the envelope bakes them, so geometry effects on them count (a stroke a Transform effect moves paints where it is moved to), each measured by its visual bounds (exact for miter joins, a tip counting where the miter limit keeps it, and for round joins; an upper bound for bevel joins, miters the limit cuts, width profiles and square-capped dashes), and a contour of zero length, which paints nothing, left out; effects on the type object itself are left out, as VectorCraft applies them after the envelope bends the type. The drawn-art check decides which one opens. An envelope's grid reads its way round from all its rows and columns, so a mesh bent far round reads the way it runs. While an envelope's contents are edited, its mesh is kept in its dictionary, and in some files that mesh lies away from the drawn art by a move, the contents with the drawn art (as if the mesh hadn't followed the envelope when it moved): a mesh or top object saved in Edit Contents whose mesh draws the contents as large as the drawn art but elsewhere is tried again, after every other way, with its mesh moved onto that art, and opens there where it then draws as it. The drawn art of these files strokes type in an envelope along its bent outlines at the stroke's own weight, Distort Appearance on or off, and VectorCraft draws it so (#6644): an envelope of stroked type whose grid squeezes or stretches it opens live where it draws as that art. + In a `.ai` opened from its editing data, an envelope in another envelope's contents opens as a live envelope inside the live outer one, with Edit Contents on or off at either level, also where its record lies straight in the outer envelope's `AIDeformArtKey` block with no group around it; each level edits its own contents without removing its distortion, as the help page describes for one envelope (https://helpx.adobe.com/illustrator/desktop/manage-objects/reshape-transform-objects/edit-the-contents-of-envelopes.html). Each envelope is compared with its own drawn art: an inner one that doesn't match keeps its drawn art as the outer envelope's contents, and an outer one that doesn't match keeps its own drawn art. An inner record left without its `XP` stops waiting when the outer dictionary ends, and an envelope in an opacity mask's art reads as part of the mask (`an_envelope_in_an_envelope_s_contents_opens_live_inside_it` and the tests after it in `crates/eps/src/import/ai/tests.rs`). In a `.ai` opened from its editing data, Convert to Shape on a whole path or type object opens as a live Rectangle, Rounded Rectangle or Ellipse effect with its Absolute or Relative size, its corner radius, its place among the object's effects and whether it is hidden. A relative size in the file is added to each side. A relative Ellipse, or a setting VectorCraft doesn't read, keeps the drawn art (`crates/eps/src/import/ai/style/tests.rs`, `crates/eps/src/import/ai/tests.rs`, `crates/eps/src/import/tests_native.rs`). In a `.ai` opened from its editing data, Bas Relief in an art style takes its Detail, Smoothness and Light direction from the style's settings block, and an object other than type with it opens as itself with the live effect. A settings block that VectorCraft can't read (malformed, another effect's, a value out of range or a key it doesn't know) keeps the object's drawn look (part of #637; [raster effects reference](https://helpx.adobe.com/illustrator/desktop/special-effects-styles/apply-filter-effects/raster-effects-reference.html), [Photoshop File Formats Specification](https://www.adobe.com/devnet-apps/photoshop/fileformatashtml/), [scripting the filter gallery](https://community.adobe.com/questions-712/scripting-support-for-the-filter-gallery-1064646); `crates/eps/src/import/ai/style/tests.rs`, `crates/eps/src/import/ai/tests.rs`). PDF export and `.ai` saves write a page with a side longer than 14,400 pt with a `UserUnit` (PDF 1.6), its longer side divided by 14,400 and rounded up to the next thousandth, so that no side is longer than 14,400 units (ISO 32000-1, Annex C.2); a PDF 1.5 file with such a page is written as PDF 1.6 with PDF 1.5's encryption, PDF/X files and PDF 1.3 and 1.4 files write it in points (in PDF 1.3 and 1.4, a page past 32,767 pt that needs a real number past 32,767 is refused with an error naming it), a page whose short side is 3 pt or longer but would be under 3 units at that `UserUnit` gets a smaller one that keeps the short side at 3 units, and the export's warnings give each such page's size and how it is written (#6382). The Save PDF dialog's "Write pages longer than 200 in at full size in points" (`longPagesInPoints`, on by default; `.ai` saves leave it off) writes such pages in points without a `UserUnit` at every PDF version, in files without a PDF/X or PDF/A standard, so readers that ignore `UserUnit` show them at full size.